From 2134676f109fbc72ecb1f2293d658bd414b361ba Mon Sep 17 00:00:00 2001 From: "groombook-engineer[bot]" <269742240+groombook-engineer[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:05:39 +0000 Subject: [PATCH] fix(E2E): add missing API mocks for invoices stats and portal billing (#349) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(E2E): add missing API mocks for invoices stats and portal billing navigation.spec.ts: - Add mock for /api/invoices/stats/summary returning the shape { revenueThisMonth, outstanding, refundsThisMonth, methodBreakdown } that InvoicesPage useEffect fetches on mount portal-data.spec.ts billing test: - Replace incorrect /api/billing** mock with correct portal endpoint mocks: /api/portal/config, /api/portal/invoices, /api/portal/payment-methods These are the actual endpoints BillingPayments component calls Both fixes address the E2E failures reported by Lint Roller on PR #348. Co-Authored-By: Paperclip * 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 * 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 * 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 * 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 * 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 * 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 * fix(GRO-785): restore eslint-disable for intentionally unused _tipSplits var Co-Authored-By: Paperclip * 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 * fix: allow groomer role to access invoices endpoint Co-Authored-By: Paperclip * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 * 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 --------- Co-authored-by: Test User Co-authored-by: Paperclip * fix(GRO-766): fix portal mobile overflow at 390px viewport - CustomerPortal.tsx: change main from overflow-x-hidden to overflow-hidden to properly clip child overflow in both axes - BillingPayments.tsx: add overflow-x-auto to tab button row so long button labels scroll instead of causing page-level overflow - PetProfiles.tsx: already has overflow-x-auto on tab row — no change needed Discovered in UAT by Shedward (DEF-2 and DEF-3 on GRO-754). Co-Authored-By: Paperclip * 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 * 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 * fix(GRO-876): add partial refund validation and fix modal indentation * fix(GRO-818): refund button for all paid invoices, inline cardLast4, manual refund for non-Stripe - Backend refund endpoint: allow refunds on paid invoices without stripePaymentIntentId (manual refund path) - Backend GET /invoices/:id: inline fetch cardLast4 + paymentStatus from Stripe when stripePaymentIntentId present - Frontend: show Refund button on all paid invoices for managers (not just Stripe-backed ones) - Seed: add stripePaymentIntentId (pi_test_*) to ~20% of paid invoices for Stripe-path testing cc @cpfarhood * fix(GRO-887): wire OIDC + BETTER_AUTH env vars into API deployment (#369) Wire BETTER_AUTH_URL, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, BETTER_AUTH_SECRET into API deployment. Add conditional OIDC_INTERNAL_BASE env var. Add new values betterAuthUrl + internalBaseUrl in values.yaml. Add authSecretName helper. Cherry-picked from e26718b (original GRO-898 fix). Co-authored-by: Paperclip Co-authored-by: Paperclip * fix(E2E): remove duplicate invoices/stats/summary block after general /api/invoices check Co-Authored-By: Paperclip * fix(GRO-980): restore 4-space indent on /api/invoices route handler --------- Co-authored-by: Test User Co-authored-by: Paperclip Co-authored-by: Flea Flicker Co-authored-by: Claude Opus 4.6 Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com> Co-authored-by: Paperclip Co-authored-by: Chris Farhood --- apps/e2e/tests/portal-data.spec.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/e2e/tests/portal-data.spec.ts b/apps/e2e/tests/portal-data.spec.ts index 5e9b2ed..26ce82c 100644 --- a/apps/e2e/tests/portal-data.spec.ts +++ b/apps/e2e/tests/portal-data.spec.ts @@ -72,9 +72,15 @@ test.describe("Portal Data Integrity", () => { }); test("billing section renders without JS errors", async ({ page }) => { - // Mock billing endpoint - await page.route("**/api/billing**", (route) => - route.fulfill({ json: { invoices: [], balanceCents: 0 } }) + // Mock portal billing endpoints + await page.route("**/api/portal/config**", (route) => + route.fulfill({ json: { stripePublishableKey: "" } }) + ); + await page.route("**/api/portal/invoices**", (route) => + route.fulfill({ json: [] }) + ); + await page.route("**/api/portal/payment-methods**", (route) => + route.fulfill({ json: [] }) ); const consoleErrors: string[] = [];