fix(GRO-624): input validation across 5 API routes #305
Reference in New Issue
Block a user
Delete Branch "fix/gro-624-input-validation"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Test plan
pnpm --filter api testcc @cpfarhood
Greptile Summary
This PR adds the five targeted input validations described in the PR description (invoice query params, booking future-time check, recurrence 1-year cap, 8-hour service duration cap, and Stripe UUID guard), all of which are correct. However, the diff includes a much wider set of behavioral changes that are not mentioned in the description, and several of them introduce regressions:
groomingLogs.ts):appointmentIdis destructured out of the request body but never included in the DB insert. Every grooming log created after this lands will have anullappointment link regardless of what the caller provides.invoices.ts): The refund endpoint lost its idempotency-key guard and transaction-backedrefundstable insert. A network retry or duplicate request will callprocessRefundtwice, resulting in two Stripe refunds.invoices.ts): TheALLOWED_TRANSITIONSstate machine was replaced with a void-only check. Paid invoices can now be moved back todraftorpendingthrough the API.appointments.ts):updateFields.batherStaffIdwas removed fromneedsConflictCheck, so a PATCH that only changes the bather staff member bypasses the scheduling conflict block entirely.appointments.ts): The per-appointment staffId/batherStaffId conflict queries were removed from the cascade update loop, allowing a cascaded reschedule to silently double-book a groomer across an entire series.invoices.ts):POST /:id/tip-splitsnow returns a bare array instead of the enriched invoice object, which will break any client expecting the previous shape.Confidence Score: 1/5
Not safe to merge — two P0 bugs (data loss on grooming logs, double-refund risk) and three P1 regressions need to be resolved first.
The five advertised validations are all correct, but the PR bundles a wide set of undescribed behavioral changes. Two are critical:
appointmentIdis permanently dropped from every new grooming log (silent data corruption), and the refund endpoint lost its idempotency guard (financial double-charge risk on any retry). Three further P1 bugs — invalid invoice status transitions, bather-only PATCH bypassing conflict checks, and cascade updates losing all conflict detection — add scheduling reliability concerns. The PR needs targeted fixes before landing.apps/api/src/routes/groomingLogs.ts(appointmentId drop),apps/api/src/routes/invoices.ts(refund safety, status transitions, tip-splits response),apps/api/src/routes/appointments.ts(cascade and bather conflict checks)Important Files Changed
appointmentIdis destructured out of the body but never re-added to the DB insert, so every new grooming log is silently stored with a null appointment link.parsed.dataunused,invoiceId.trim()called twice instead..max(480)ondurationMinutes; no issues.startTimeis correct. Confirm/cancel token handlers simplified from atomic WHERE clause to ID-based update with pre-checks; idempotent in practice but introduces a TOCTOU window.hasAccessintermediate variable; logic preserved. One removedif (!updated) return 404after an already-checked existence query — benign.Sequence Diagram
Comments Outside Diff (6)
apps/api/src/routes/groomingLogs.ts, line 59-105 (link)appointmentIdsilently dropped from every new grooming logappointmentIdis destructured out ofreston line 59 but is never re-added to the.values(...)call (lines 99–105). Every new grooming visit log will be stored with anullappointment linkage, regardless of what the caller supplies. The original code passed it explicitly asappointmentId: appointmentId ?? null.Fix: re-include
appointmentIdin the insert:Prompt To Fix With AI
apps/api/src/routes/invoices.ts, line 382-385 (link)The previous implementation:
idempotencyKeyand short-circuited on duplicates (preventing network-retry double refunds).refundstable inside a transaction for audit purposes.Both safety mechanisms were removed. Now a transient network error or client retry will call
processRefundtwice, issuing two refunds to Stripe. There is also no database record of issued refunds.Consider restoring at minimum the idempotency guard — even a simple
refundstable insert with anON CONFLICT DO NOTHINGonstripeRefundIdprovides a meaningful safety net.Prompt To Fix With AI
apps/api/src/routes/invoices.ts, line 318-320 (link)The
ALLOWED_TRANSITIONSguard was replaced with only a void check, so the following transitions are now silently accepted by the API:paid → draft/paid → pending(un-paying a settled invoice)pending → draft(already-sent invoice rolled back to draft)The previous state machine modelled real business constraints. At a minimum,
paidinvoices should not be moveable todraftorpending, which could create incorrect financial state.Prompt To Fix With AI
apps/api/src/routes/appointments.ts, line 424-428 (link)updateFields.batherStaffId !== undefinedwas removed fromneedsConflictCheck. When a client sends a PATCH that only changes the bather (e.g.{ "batherStaffId": "uuid" }),needsConflictCheckisfalseand the entire conflict-check transaction block is skipped — including the bather conflict query on lines 492–516. A groomer/bather can be double-booked through this path.Prompt To Fix With AI
apps/api/src/routes/appointments.ts, line 380-408 (link)The previous implementation ran both
staffIdandbatherStaffIdconflict queries for each appointment being shifted in the series, and the 409 response path in the catch block was present to surface them. Both the per-appointment conflict queries and the 409 error handler were removed from this PR.A cascaded reschedule (e.g. "shift all future appointments in this series one hour earlier") can now silently move an entire series into time slots already occupied by other appointments, double-booking the groomer and/or bather across potentially dozens of appointments at once.
Prompt To Fix With AI
apps/api/src/routes/invoices.ts, line 173-178 (link)POST /:id/tip-splitsThe endpoint previously returned
{ ...updatedInvoice, lineItems, tipSplits }— the full enriched invoice object. It now returns a bare array of split rows. Any frontend or integration client that destructures the old shape (e.g.result.lineItems,result.status) will silently receiveundefinedfor every field.Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "fix(stripe-webhooks): validate invoice I..." | Re-trigger Greptile
parsed.dataunused —invoiceId.trim()called twiceAfter the
safeParseguard on line 48,parsed.dataalready holds the trimmed, validated UUID. Creating a secondinvoiceIdTrimmedvariable by calling.trim()again is redundant. Useparsed.datadirectly:The same pattern appears in the
payment_intent.payment_failedhandler (lines 76–78).Prompt To Fix With AI
CTO Review — Changes Requested
Validation fixes: APPROVED ✅
All 5 input validation fixes are correctly implemented:
listInvoicesQuerySchema+zValidator)startTime.refine())frequencyWeeks * count <= 52).max(480), inherits via.partial())safeParseguard)Merge conflict: BLOCKING ❌
This branch contains an out-of-scope commit from GRO-623/GRO-635:
That work was already merged to
devvia PR #285. The duplicate commit causes merge conflicts inappointmentGroups.ts,groomingLogs.ts, andappointments.ts.Required fix
Rebase
fix/gro-624-input-validationontodevand drop commit5e24678. The 5 validation commits (369c2cethrough203b600) should rebase cleanly once the GRO-635 commit is removed.After force-push, CI will re-run and the merge conflict should be resolved.
cc @cpfarhood