Pet interface: added sizeCategory and coatType (nullable strings).
Service interface: added defaultBufferMinutes.
Appointment interface: added bufferMinutes.
These fields are referenced by Book.tsx, cascade.ts, buffer.ts, appointment
routes, and other type-annotated consuming code. Without them, any file that
imports these interfaces and accesses the fields causes a TypeScript error.
cc @cpfarhood
Co-Authored-By: Flea Flicker <noreply@paperclip.ing>
Fix 'stat apps/groombook/overlays/dev/kustomization.yaml: no such file'
error by correcting paths from apps/groombook/overlays/dev to apps/overlays/dev
and apps/groombook/base to apps/base.
GRO-1289
* 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.
Two root causes fixed:
1. VITE_API_URL was empty in .env.production, so Better-Auth's client
had no baseURL and could not correctly route the OAuth callback.
2. OAuth callbackURL was window.location.origin (root path), causing
Better-Auth to redirect to / instead of /admin after login — since
unauthenticated users at / are redirected to /login, this created a
loop that appeared as 'session not persisting.'
With VITE_API_URL=https://uat.groombook.dev and callbackURL=/admin,
the callback lands on /admin which renders the admin layout and
correctly establishes the session cookie.
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
- Wrap conversation mocks in { items, nextCursor } response shape
(loadConversations reads json.items, bare array caused undefined.length crash)
- Guard scrollIntoView with ?. (jsdom doesn't implement it)
- Use getAllByText for text appearing in both preview and thread
Co-Authored-By: Paperclip <noreply@paperclip.ing>
1. **Remove duplicate staffReadAt** in `packages/db/src/schema.ts`
(TS1117 duplicate identifier — merge conflict artifact)
2. **Add count to db index exports** in `packages/db/src/index.ts`
(`count` from drizzle-orm was used in conversations.ts but not exported)
3. **Use dev version of conversations.ts** (no type errors, sql\`count(*)\`)
— PR branch version had incompatible type errors (staff.businessId,
count, optedOutAt fields not in schema)
4. **Remove duplicate conversationsRouter import** in `apps/api/src/index.ts`
All 289 tests pass, 0 lint errors.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Replace incorrect `apps/groombook/` path prefix with `apps/` in both
promote-to-uat.yml and promote-prod.yml. The infra repo structure uses
`apps/` directly without a `groombook/` level.
GRO-1248
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extract Conversation interface fields to match API response:
replace lastMessageBody with lastMessage object, externalNumber with
clientPhone, remove staffReadAt
- loadConversations(): extract json.items array instead of raw array
- loadMessages(): extract json.items and reverse() for chronological order
- Update test mocks to use { items, nextCursor } response shape
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Adds staff conversations API (GET /api/conversations, GET /api/conversations/:id/messages, POST /api/conversations/:id/messages) with auth scoping and cross-tenant protection
- Adds staffReadAt column to conversations table for unread tracking
- Adds staff Messages page with two-column inbox layout (thread list + conversation view + composer)
- Adds Messages entry to staff sidebar navigation
- Includes tests for the MessagesPage component
Part of GRO-106 (SMS/MMS integration) Phase 1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add and() + lt() imports from @groombook/db
- Apply businessId to conversation WHERE clause for cross-tenant isolation
(GET /portal/conversation: clientId AND businessId both scoped)
- Fix cursor pagination: apply lt(messages.createdAt, cursorMsg.createdAt)
to the cursor WHERE clause so pages actually paginate
- Add UAT_PLAYBOOK.md §4.9.1 Communication tab test cases:
TC-APP-4.9.6 message history with conversation
TC-APP-4.9.7 empty state (no conversation yet)
TC-APP-4.9.8 composer disabled with tooltip
TC-APP-4.9.9 cross-tenant isolation
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Added GET /portal/conversation and GET /portal/conversation/messages endpoints
- Created Communication.api.ts with typed fetchers and React hooks
- Rewired Communication.tsx to use real API, removed mock data
- Added composer-disabled bar with "Reply from your phone" tooltip
- Added conversation route tests to portal.test.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add impersonationAuditLogs table mock and db.insert() method to the
@groombook/db mock in portal.test.ts to resolve "No 'impersonationAuditLogs'
export is defined" errors. The portalAudit middleware calls db.insert()
on every request, which was missing from the mock.
Passes all 26 portal tests.
The telnyx webhook handler at /api/webhooks/telnyx/messaging was
returning 401 for all requests including those with valid signatures.
This was caused by the authMiddleware being applied to all /api/*
routes via api.use("*", authMiddleware) after the webhook route was
registered at the app level.
authMiddleware already skips /api/auth/ paths; adding the same skip
for /api/webhooks/* fixes the issue — webhook endpoints use their own
signature validation and do not require Better-Auth session auth.
Root cause: authMiddleware was applied to webhook routes that were
registered at the app level before the api sub-app middleware, but
the skip condition only covered /api/auth/, not /api/webhooks/.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0031_steady_veda has no corresponding SQL file — caused Drizzle migration
runner to exit 1 in E2E. Renumber 0032_staff_read_at to idx 31.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add `staffReadAt` column to conversations table schema
- Add migration 0032_staff_read_at.sql for the new column
- Create /api/conversations router with GET / (list), GET /:id/messages (paginated), POST /:id/messages (send)
- Mark conversations as read (staffReadAt = NOW()) when staff fetches messages
- Return 409 when client has opted out of SMS
- 404 on cross-tenant access
- Add conversations.test.ts covering all 5 acceptance criteria
Co-Authored-By: Paperclip <noreply@paperclip.ing>
- Add custom rate limit rules for /sign-in/social, /sign-in/email, and /sign-up/email
- Override default Better Auth limits (3 req/10s) with more permissive limits
- Apply rules to both placeholder and real auth configs
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-106): messaging schema + migrations
- Add conversations, messages, message_attachments, message_consent_events tables
- Add messagingChannelEnum, messageDirectionEnum, messageStatusEnum, messageConsentKindEnum
- Extend business_settings with messagingPhoneNumber and telnyxMessagingProfileId columns
- Add required indexes and unique constraints with cascade-on-delete FKs
- Add migration 0030_messaging.sql
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-981): restore journal entries and add DESC to indexes
- _journal.json: restore idx 28 (0028_sms_reminders), add idx 29
(0029_db_indexes_constraints), renumber 0030_messaging to idx 30
(was missing 0028 and 0029 entries — they were silently skipped)
- schema.ts: add .desc() to conversations.lastMessageAt and
messages.createdAt indexes per spec
- 0030_messaging.sql: add DESC to both generated index statements
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* feat(GRO-106): inbound Telnyx webhook + persistence
- Add POST /api/webhooks/telnyx/messaging route with HMAC signature verification
- Add services/messaging/inbound.ts: findOrCreateConversation, upsertMessage (idempotent on providerMessageId), delivery receipt handling
- Register telnyxWebhooksRouter in index.ts (before auth middleware)
- Add unit tests for signature validation, find-or-create, idempotent insert, delivery receipt
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-982): address all QA blocking failures
- #7: Extract validateTelnyxSignature in sms.ts as standalone exported fn,
reuse in TelnyxProvider.validateWebhookSignature and telnyx.ts route
- #1: Replace uuid v4 import with crypto.randomUUID() (built-in, no dep)
- #2: Remove updatedAt from messages update in handleMessageFinalized
(no such column exists)
- #3: Fix test import path ../../ → ../../../ for telnyx route import
- #4: validateTelnyxSignature accepts string | undefined | null to match
Hono c.req.header() return type
- #5&6: Add null guards for .returning() results in findOrCreateConversation
and upsertMessage
- #8: Remove dead buildFindOrCreateConversationParams function
- #9: Remove unused imports (messageDirectionEnum, messageStatusEnum,
resolveBusinessIdByMessagingNumber in test)
- #10: Wrap upsertMessage insert in try/catch; unique violation returns
{isNew: false} instead of crashing
- #11: Add EOF newlines to all modified files
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* chore: add uuid dependency for messaging services
* fix(GRO-982): address 5 test failures in inbound webhook
- Fix signature route tests: use /messaging not full mount path
- Fix handleMessageReceived mock order: business lookup first
- Fix stale mock state: add full mockReset in handleMessageFinalized beforeEach
- Fix delivery logic: set delivered for all message.finalized events
- Deduplicate test that was accidentally added twice
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-982): look up or create client by phone before inserting conversation
Fixes FK constraint violation where clientId was set to businessSettings.id
or a random UUID. Now looks up clients.phone = clientPhone first; if no match,
creates a placeholder client with phone as name and a placeholder email.
* fix(GRO-982): address QA round 4 blocking failures
- Fix URL in signature tests: use /messaging not full path
- Reorder mocks: businessSettings first, then conversations, clients, messages
- Add mockDb.mockReset in handleMessageFinalized beforeEach
- Remove direction guard: set delivered for any message.finalized
* fix(GRO-982): add missing message insert mock in handleMessageReceived test
* fix(GRO-982): simplify test mocks to match actual code flow
---------
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* docs(GRO-106): 10DLC pilot registration runbook
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-106): address QA review feedback
- Change business_vertical from FINANCE_INSURANCE_BANKING to PROFESSIONAL_SERVICES
- Fix broken internal issue links (GRO-106, GRO-981) to plain text
- Add owner stamp alongside last-updated date
- Fix phone placeholder in SQL and API example to use +1XXXXXXXXXX
- Add trailing newline to both runbook files
Co-Authored-By: Paperclip <noreply@paperclip.ing>
---------
Co-authored-by: Chris Farhood <chris@farhood.org>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* 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 <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>
* 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>
* fix: allow groomer role to access invoices endpoint
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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 <noreply@paperclip.ing>
* 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>
* 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>
* 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>
* 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>
* 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>
* 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 <noreply@paperclip.ing>
* 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>
* 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>
* 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/🆔 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 <paperclip@noreply.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
* fix(E2E): remove duplicate invoices/stats/summary block after general /api/invoices check
Co-Authored-By: Paperclip <noreply@paperclip.ing>
* fix(GRO-980): restore 4-space indent on /api/invoices route handler
---------
Co-authored-by: Test User <test@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Flea Flicker <fleaflicker@groombook.farh.net>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: groombook-engineer[bot] <269742240+groombook-engineer[bot]@users.noreply.github.com>
Co-authored-by: Paperclip <paperclip@noreply.com>
Co-authored-by: Chris Farhood <chris@farhood.org>