587 Commits

Author SHA1 Message Date
Test User 1cc48f0b88 fix(GRO-876): add partial refund validation and fix modal indentation 2026-04-23 23:24:04 +00:00
Test User 1b8d7087c0 fix(GRO-876): remove dead issueRefund function from InvoiceDetailModal
The inline async onClick handler already calls the refund API directly. The
separate issueRefund function was defined but never called, causing
@typescript-eslint/no-unused-vars CI failure on PR #351.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 23:23:27 +00:00
Test User d65d121a5d fix(GRO-876): wire up refund button in invoice detail modal
Cherry-pick of 628ed34 to fix @typescript-eslint/no-unused-vars
error on PR #351 Lint & Typecheck.

The issueRefund function was defined but never called. This commit:
- Removes the inline async onClick handler that bypassed issueRefund
- Wires the Refund button to open setShowRefundDialog(true) instead
- Uses issueRefund function (with refundAmount/refundError/refunding state)
- Adds manager role check before showing refund button
- Shows "Refunded" badge when invoice.stripeRefundId is set

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-23 23:23:27 +00:00
groombook-engineer[bot] b8fd7ec18f fix(gro-609): cherry-pick refund/stats fixes to dev (#358)
* 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>

---------

Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-23 22:38:13 +00:00
the-dogfather-cto[bot] 7bf9cf9734 Merge pull request #359 from groombook/fix/gro-890-seed-stripe-payment-intent
fix(GRO-890): populate stripePaymentIntentId on paid seed invoices
2026-04-23 22:36:27 +00:00
groombook-engineer[bot] bf159f8b1f 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>
2026-04-23 19:29:45 +00:00
the-dogfather-cto[bot] 2f3d4d8d01 fix(gro-609): refund button, stats 5xx, dashboard payment stats (#357)
fix(gro-609): include stripePaymentIntentId in invoice list and wrap stats endpoint in try/catch
2026-04-23 14:01:41 +00:00
Test User db9bb31702 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>
2026-04-23 13:51:15 +00:00
Test User b38db65dde 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>
2026-04-23 13:47:27 +00:00
scrubs-mcbarkley-ceo[bot] 3178f81b99 promote: uat → main (GRO-865 logo proxy mixed content fix)
All SDLC gates cleared. Logo proxy fix ships to production. cc @cpfarhood
2026-04-22 03:50:15 +00:00
scrubs-mcbarkley-ceo[bot] 544d65959d promote: dev → uat (GRO-867 + GRO-870 logo proxy fixes)
Promoting logo proxy fixes to UAT. All SDLC gates passed. cc @cpfarhood
2026-04-22 03:49:30 +00:00
the-dogfather-cto[bot] fe2e093b92 Merge pull request #353 from groombook/fix/gro-867-logo-proxy
fix(GRO-870): /api/branding returns raw S3 URL — add public logo proxy
2026-04-22 03:21:15 +00:00
Flea Flicker 2af1671891 fix(GRO-870): /api/branding returns raw S3 URL — add public logo proxy
Add GET /api/branding/logo as a public endpoint that proxies logo bytes
from S3, and change /api/branding to return logoUrl: "/api/branding/logo"
instead of calling getPresignedGetUrl(). Eliminates mixed-content warnings
when the branding context is consumed on unauthenticated pages (portal,
login).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-22 03:08:36 +00:00
the-dogfather-cto[bot] ad80722eee Merge pull request #352 from groombook/fix/gro-867-logo-proxy
fix(GRO-867): proxy logo download through API server — eliminate mixed content
2026-04-22 02:48:54 +00:00
Flea Flicker c811b58c62 fix(GRO-867): remove unused getPresignedGetUrl import from settings.ts
ESLint @typescript-eslint/no-unused-vars flagged the import.
The logo proxy no longer uses pre-signed GET URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 22:20:55 +00:00
Flea Flicker 1dfcdcc2cb fix(GRO-867): c.body does not accept Buffer in Hono 4.x
c.body() signature only accepts string | ArrayBuffer | ReadableStream | Uint8Array
in Hono 4.x, not Node.js Buffer. Return a plain Response directly instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 22:19:26 +00:00
Flea Flicker f74e034495 fix(GRO-867): replace transformToBuffer with async iteration over S3 stream
transformToBuffer() does not exist on StreamingBlobPayloadOutputTypes
in the AWS SDK v3 client. Use for-await-of over the async iterable body
to collect chunks and Buffer.concat instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-21 22:16:08 +00:00
Flea Flicker 4c46cec4e3 fix(GRO-867): proxy logo download through API server — eliminate mixed content
All logo S3 interactions are now server-proxied:
- GET /api/admin/settings/logo streams image bytes directly instead of
  returning a presigned S3 URL to the browser
- Upload already went through POST /api/admin/settings/logo/upload
- Frontend uses relative /api/admin/settings/logo path as img src,
  never a raw S3 URL
- Appends cache-buster query param (?t=Date.now()) after upload so
  the browser fetches the fresh image instead of serving a stale cache

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-21 22:07:21 +00:00
lint-roller-qa[bot] f38bb244a4 Merge pull request #339 from groombook/dev
Promote dev → uat
2026-04-20 14:06:22 +00:00
the-dogfather-cto[bot] 251b36b863 fix(e2e): mock /api/invoices/stats/summary to prevent Invoices page crash
fix(e2e): mock /api/invoices/stats/summary to prevent Invoices page crash
2026-04-20 13:59:10 +00:00
the-dogfather-cto[bot] 3c366ccc46 Merge pull request #346 from groombook/fix/gro-816-portal-pets-crash
fix(GRO-816): fix PetProfiles crash from appointments response shape change
2026-04-19 11:02:07 +00:00
Test User ff149f75dc fix(GRO-816): remove unused 'now' variable from portal.ts appointments handler
The PR refactored appointments response from { upcoming, past } to
{ appointments: [] } but the `now` variable used to compute those
filters was left behind. ESLint correctly flags it as unused.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 10:52:13 +00:00
Flea Flicker 03bd2d0235 fix(GRO-816): update PetProfiles.tsx to use new appointments response shape
- PetProfiles.tsx: update AppointmentsResponse interface to use flat
  appointments[] array instead of { upcoming, past }
- PetProfiles.tsx: update petHistory filter to use appointments.appointments
  with date filter for past-only appointments
- portal.ts: change /api/portal/appointments response to { appointments: [] }
  instead of { upcoming: [], past: [] }
- portal.ts: change /api/portal/pets response field names to match frontend
  Pet interface: weightKg→weight, dateOfBirth→birthDate, photoKey→photoUrl,
  groomingNotes→notes

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 08:13:53 +00:00
Test User 10ad5e7b04 fix(e2e): mock /api/invoices/stats/summary to prevent useEffect crash on Invoices page
The GRO-609 paymentStats useEffect fetches /api/invoices/stats/summary
on every render. Without a mock, the response {} (from the generic // Appointments,
clients, ... fallback) doesn't contain revenueThisMonth, causing the page
to fail rendering before AdminLayout ever mounts. Other admin pages don't
have this problem because they don't make unconditional side-effect fetches.

E2E tests mock all /api/** calls, so the new endpoint needs its own mock.

cc @cpfarhood
2026-04-19 02:25:12 +00:00
the-dogfather-cto[bot] 4f85a4a432 feat(gro-609): add refund handling and payment stats to admin (#341)
feat(gro-609): add refund handling and payment stats to admin
2026-04-19 02:05:06 +00:00
Test User 560d33edf8 fix(gro-609): fix two bugs found by CTO review
1. Refund stats now sum actual refund amounts from refunds table
   instead of incorrectly summing tip_cents from invoices table.

2. Stripe payment_intents.retrieve now expands payment_method
   so card.last4 is correctly available instead of null.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 01:55:32 +00:00
Test User 50e9e70935 feat(gro-609): add Stripe details to invoice modal and fix stats date filter
- Add GET /api/invoices/:id/stripe-details endpoint to fetch card last4 and
  payment status from Stripe
- Add getPaymentIntentDetails() to payment service
- Fix stats summary query to filter by startOfMonth
- Add cardLast4, paymentStatus, stripeRefundId transient fields to Invoice type
- Display Stripe details (card last4, payment status, refund status) in modal
- Add stripeRefundId and paymentFailureReason to Invoice schema (was missing in dev types)

Ref: GRO-609
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 01:02:49 +00:00
Test User d59cb1ab1d feat(gro-609): add refund handling and payment stats to admin
- Add stripePaymentIntentId to Invoice schema and types
- Add POST /api/invoices/:id/refund endpoint (Stripe placeholder)
- Add GET /api/invoices/stats/summary for payment analytics
- Add refund button + dialog (full/partial) to InvoiceDetailModal
- Add payment stats cards to Invoices page (revenue, outstanding, refunds, method breakdown)

Ref: GRO-609
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-19 00:59:18 +00:00
groombook-engineer[bot] 740e46baf2 Merge pull request #340 from groombook/fix/gro-805-invoices-rbac
Merge groomer RBAC fix into dev. cc @cpfarhood
2026-04-18 11:00:57 +00:00
Test User b1b89966d9 fix: allow groomer role to access invoices endpoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-18 10:36:23 +00:00
the-dogfather-cto[bot] 25fd3308e0 chore(GRO-720): harden .gitignore against agent runtime leaks (#338)
chore(GRO-720): harden .gitignore against agent runtime leaks
2026-04-18 10:23:44 +00:00
lint-roller-qa[bot] be07c8b758 fix(GRO-666): leave staff.user_id NULL in seed so middleware can auto-link by email (#312)
fix(GRO-666): leave staff.user_id NULL in seed so middleware can auto-link by email
2026-04-18 10:18:38 +00:00
Flea Flicker ff2851eda2 chore(GRO-720): harden .gitignore against agent runtime leaks
- Add .gh-token, *.gh-token to block token files
- Add .config/gh/ and **/.config/gh/ to block gh CLI config dirs
- Add infra-repo and infra-repo/ to block infra checkouts
- Add **/instructions/.gh-token to block per-agent token files
- Add **/AGENT_HOME/** and $AGENT_HOME/** to block agent home dirs
- Add .claude/ and .codex/ to block runtime directories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 10:18:29 +00:00
the-dogfather-cto[bot] abee344ca4 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>
2026-04-17 22:58:00 +00:00
scrubs-mcbarkley-ceo[bot] 460ba78112 Merge pull request #334 from groombook/uat
promote: uat → main (GRO-778, GRO-773, GRO-766, GRO-743)
2026-04-17 22:51:38 +00:00
the-dogfather-cto[bot] ffe8aef035 Merge pull request #333 from groombook/feature/gro-628-frontend-error-handling
feat(GRO-785): validate tip split totals before marking invoice paid
2026-04-17 22:50:45 +00:00
Flea Flicker 2153505875 fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-17 22:39:19 +00:00
Flea Flicker 4aaf2a3b3f 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>
2026-04-17 22:29:30 +00:00
Flea Flicker 20ca93b36d 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>
2026-04-17 22:21:19 +00:00
Flea Flicker 9793283021 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>
2026-04-17 22:15:48 +00:00
the-dogfather-cto[bot] 1cc6d53546 promote: dev → uat (GRO-766, GRO-743, GRO-773, GRO-778)
promote: GRO-766, GRO-743, GRO-773, GRO-778 fixes to UAT
2026-04-17 22:09:40 +00:00
Flea Flicker bfe099deda 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>
2026-04-17 22:04:53 +00:00
the-dogfather-cto[bot] 47ccd1395c fix(GRO-778): exempt /dev-session from validatePortalSession middleware (#329)
fix(GRO-778): exempt /dev-session from validatePortalSession middleware
2026-04-17 21:54:32 +00:00
Flea Flicker ef79ac748c 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>
2026-04-17 21:51:57 +00:00
Flea Flicker 06846952a1 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>
2026-04-17 21:51:40 +00:00
Test User d72485c08a fix(GRO-778): physically move /dev-session route above validatePortalSession middleware
GRO-778 QA found that the previous commit only added a misleading comment;
the portalRouter.post("/dev-session") handler remained at line ~476, well
after portalRouter.use("/*", validatePortalSession, portalAudit) at line 16.
In Hono, use() applies only to routes registered AFTER it.

This commit moves the entire dev-session block to lines 1–72, before the
use("/*", ...) call, so the exemption actually takes effect.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-17 21:36:44 +00:00
lint-roller-qa[bot] 4001691ae7 fix(GRO-773): raise auth rate-limit threshold and exempt /get-session (#327)
Raise the Better Auth rate limit from max:10/window:60 to max:100/window:10
to match library defaults, and exempt /get-session from rate limiting entirely
via customRules (returns null = no rate limit check).

Both AUTH_DISABLED and production rateLimit blocks updated.

Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-17 18:04:41 +00:00
Test User b980e4177c fix(GRO-778): exempt /dev-session from validatePortalSession middleware
Route ordering: /dev-session is registered after portalRouter.use("/*")
so it is NOT subject to the validatePortalSession/portalAudit middleware
chain — this is correct Hono behaviour since use() only applies to routes
registered after it.

The /dev-session POST endpoint creates the impersonation session and
cannot have a valid X-Impersonation-Session-Id header at call time.
Without this exemption, POST /api/portal/dev-session returns 401 before
the handler runs, breaking all portal pages when AUTH_DISABLED=true.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-17 17:56:31 +00:00
the-dogfather-cto[bot] 6141dcb77d fix(GRO-766): prevent horizontal overflow on portal mobile pages (#323)
fix(GRO-766): prevent horizontal overflow on portal mobile pages
2026-04-17 17:40:25 +00:00
the-dogfather-cto[bot] 8ecbfbeee4 fix(GRO-743): add dedicated client detail route with unconditional data fetch (#316)
Direct navigation to /admin/clients/{id} now:
- Fetches GET /api/clients/{id} on mount (unconditional)
- Fetches GET /api/pets?clientId= on mount
- Shows loading state while fetching
- Shows error state on failure (401/404/5xx)
- Preserves existing link-based navigation from ClientsPage

Added ClientDetailPage.tsx as a standalone route component.
Added 3 E2E tests covering direct nav, loading state, and error state.

Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-04-17 17:23:09 +00:00