* Promote dev → uat: ARIA modal fix + tip split atomicity (#335)
* feat(GRO-785): validate tip split totals before marking invoice paid
- PATCH /invoices/:id returns 400 when tipCents > 0 but no tip splits
exist or splits don't sum to 100%
- POST /invoices/:id/tip-splits now returns 400 (not 422) on validation
failure via router-level ZodError handler
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-786): add ARIA label attributes to Modal dialog component
- Update Modal component to accept title and titleStyle props
- Add role="dialog", aria-modal="true", and aria-labelledby attributes
- Use useId() to generate stable ID for title heading association
- Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet,
Log Grooming Visit, Permanently Delete Client) with title props
- Delete modal passes titleStyle for red color on warning
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-786): remove duplicate dialog role and restore focus trap
- Remove role="dialog" and aria-modal="true" from outer backdrop div
- Keep ARIA attributes only on inner dialog div (the actual modal)
- Restore useEffect focus management: auto-focus first element,
Tab cycle wrapping, Escape key handler, focus restore on close
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): restore atomic tip split save in PATCH and fix error message
- When body.tipSplits is provided in PATCH /invoices/:id, validate sum
first then atomically replace existing splits (delete + insert)
- When no incoming splits, validate existing DB splits with corrected
message: "Tip splits are required when tip amount is greater than zero"
(previously misleading "must sum to 100%" when no splits existed)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): address invoice tip split regression
- Use body.tipCents ?? current.tipCents for validation condition
so that simultaneous status=paid + tipCents=0 skip split validation
- Use body.tipCents (now aliased as tipCents) instead of current.tipCents
inside the atomic transaction for shareCents calculation
- Add explicit check for empty tipSplits array with appropriate error
message ("Tip splits are required when tip amount is greater than zero")
before the sum-to-100% check
- Destructure tipSplits out of body before spreading into update object
to prevent it from leaking into the invoices table SET clause
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): wrap tip split save + invoice update in single transaction
Both tip split persistence (delete + insert) and the invoice PATCH update
are now inside one db.transaction() block. If the invoice update fails
after splits are written, the entire operation rolls back.
Also removed unnecessary eslint-disable comment on _tipSplits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: the-dogfather-cto[bot] <269737991+the-dogfather-cto[bot]@users.noreply.github.com>
* fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
- Add stripePaymentIntentId to the GET /invoices list query so the refund button
renders when seed data includes a payment intent ID
- Wrap /api/invoices/stats/summary in try/catch so errors return 200 with zero
defaults instead of 5xx, preventing the Invoices page from crashing on
mount for groomer-role sessions
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(gro-609): add payment stats to admin dashboard (AppointmentsPage)
- Fetch /api/invoices/stats/summary on mount and display Revenue/Outstanding/Refunds
summary cards above the calendar view on /admin
- Mirrors the same stats section already on /admin/invoices
- Gracefully handles errors via try/catch on the stats endpoint
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-890): populate stripePaymentIntentId on all paid seed invoices
All paid invoices created by the seed script now get a deterministic
stripePaymentIntentId of the form pi_test_seed_NNNNNN, unblocking the
refund button conditional in Invoices.tsx:514 during UAT.
Pending/draft invoices retain null as before.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-898): update CI to deploy on dev branch pushes
Update the Update Infra Image Tags job condition to also trigger
on pushes to the dev branch, not just main.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix: correct infra paths in promote-to-uat workflow
Remove 'groombook/' prefix from 4 path references in promote-to-uat.yml since the groombook/infra repo has apps/overlays/ and apps/base/ at the root, not under a groombook/ subdirectory.
GRO-1274
---------
Co-authored-by: the-dogfather-cto[bot] <269737991+the-dogfather-cto[bot]@users.noreply.github.com>
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: lint-roller-qa[bot] <269744346+lint-roller-qa[bot]@users.noreply.github.com>
Co-authored-by: scrubs-mcbarkley-ceo[bot] <269735724+scrubs-mcbarkley-ceo[bot]@users.noreply.github.com>
Co-authored-by: Test User <test@example.com>
Co-authored-by: groombook-engineer[bot] <3141748+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Chris Farhood <chris@farhood.org>
Ensures Vite sees VITE_API_URL as an empty string (not undefined) during
pnpm build, so the || window.location.origin fallback fires at runtime
instead of baking in the UAT URL.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Without this, Vite sees VITE_API_URL as undefined (not empty string) at
build time. The ?? operator only replaces null/undefined, not a missing var,
so better-auth receives undefined — which it treats as a relative path and
prepends window.location.origin at build time, resulting in the UAT URL being
baked in.
Explicitly setting ARG VITE_API_URL= (empty string) in the Dockerfile makes
Vite see it as defined with empty value, so the || fallback fires at runtime.
Fixes GRO-1280.
Vite bakes VITE_* vars at build time, so hardcoding a URL in .env.production
breaks CI E2E which runs on localhost. Now falls back to the browser origin
at runtime, which works correctly since nginx reverse-proxies /api to the
local API container.
Fixes GRO-1280.
Conflicts:
- apps/api/src/routes/invoices.ts — kept uat's stripeRefundId field (GRO-818)
- packages/db/src/seed.ts — kept main's deterministic stripePaymentIntentId
population (GRO-890); removed duplicate uat declaration that survived auto-merge
Brings GRO-609 (refund/stats fixes), GRO-890 (seed stripe pi), GRO-898 (CI dev
branch) and prior GRO-865 logo proxy promote from main into uat so the
uat → main promote (GRO-958) becomes mergeable.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Update the Update Infra Image Tags job condition to also trigger
on pushes to the dev branch, not just main.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
All paid invoices created by the seed script now get a deterministic
stripePaymentIntentId of the form pi_test_seed_NNNNNN, unblocking the
refund button conditional in Invoices.tsx:514 during UAT.
Pending/draft invoices retain null as before.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Fetch /api/invoices/stats/summary on mount and display Revenue/Outstanding/Refunds
summary cards above the calendar view on /admin
- Mirrors the same stats section already on /admin/invoices
- Gracefully handles errors via try/catch on the stats endpoint
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add stripePaymentIntentId to the GET /invoices list query so the refund button
renders when seed data includes a payment intent ID
- Wrap /api/invoices/stats/summary in try/catch so errors return 200 with zero
defaults instead of 5xx, preventing the Invoices page from crashing on
mount for groomer-role sessions
Parent: GRO-882
Grandparent: GRO-816
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-785): validate tip split totals before marking invoice paid
- PATCH /invoices/:id returns 400 when tipCents > 0 but no tip splits
exist or splits don't sum to 100%
- POST /invoices/:id/tip-splits now returns 400 (not 422) on validation
failure via router-level ZodError handler
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-786): add ARIA label attributes to Modal dialog component
- Update Modal component to accept title and titleStyle props
- Add role="dialog", aria-modal="true", and aria-labelledby attributes
- Use useId() to generate stable ID for title heading association
- Update all 4 Modal call sites (New/Edit Client, Add/Edit Pet,
Log Grooming Visit, Permanently Delete Client) with title props
- Delete modal passes titleStyle for red color on warning
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-786): remove duplicate dialog role and restore focus trap
- Remove role="dialog" and aria-modal="true" from outer backdrop div
- Keep ARIA attributes only on inner dialog div (the actual modal)
- Restore useEffect focus management: auto-focus first element,
Tab cycle wrapping, Escape key handler, focus restore on close
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): restore atomic tip split save in PATCH and fix error message
- When body.tipSplits is provided in PATCH /invoices/:id, validate sum
first then atomically replace existing splits (delete + insert)
- When no incoming splits, validate existing DB splits with corrected
message: "Tip splits are required when tip amount is greater than zero"
(previously misleading "must sum to 100%" when no splits existed)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): address invoice tip split regression
- Use body.tipCents ?? current.tipCents for validation condition
so that simultaneous status=paid + tipCents=0 skip split validation
- Use body.tipCents (now aliased as tipCents) instead of current.tipCents
inside the atomic transaction for shareCents calculation
- Add explicit check for empty tipSplits array with appropriate error
message ("Tip splits are required when tip amount is greater than zero")
before the sum-to-100% check
- Destructure tipSplits out of body before spreading into update object
to prevent it from leaking into the invoices table SET clause
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): wrap tip split save + invoice update in single transaction
Both tip split persistence (delete + insert) and the invoice PATCH update
are now inside one db.transaction() block. If the invoice update fails
after splits are written, the entire operation rolls back.
Also removed unnecessary eslint-disable comment on _tipSplits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: the-dogfather-cto[bot] <269737991+the-dogfather-cto[bot]@users.noreply.github.com>