Phase 1.2 of Route Optimization. Adds a provider-agnostic geocoding
service layer in the deployed src/ tree:
- GeocodingProvider interface + GeocodeResult type
- NominatimGeocodingProvider (default, free, self-hostable) with an
internal rate limiter enforcing the 1 req/sec Nominatim usage policy
- GoogleGeocodingProvider (optional fallback) keyed by the encrypted
businessSettings.googleMapsApiKey (decrypted via decryptSecret) or
GOOGLE_MAPS_API_KEY env fallback
- resolveGeocodingProvider() selecting on businessSettings.routeOptimizationProvider,
with safe fallback to Nominatim when google is configured but no usable key
- geocodeBatch() throttled batch utility (honors provider rate limit,
captures per-item errors, optional progress callback)
- 20 unit tests covering both providers, selection, throttle spacing, and batch
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds a defense-in-depth audit row to impersonationAuditLogs when the
staff-side owner-bypass path fires. Mirrors the failure-isolation
pattern in src/middleware/portalAudit.ts: insert failures are logged
and swallowed so a working read can never turn into a 500.
- New writeOwnerBypassAudit helper called only when isOwner === true.
- No DB migration; petId + actorStaffId go inside metadata jsonb.
- resolveImpersonationClientId stays pure (no audit side effects).
- Positive + negative tests + a cross-tenant regression test.
- UAT_PLAYBOOK.md §3.19d: TC-API-3.19d documents the audit assertion.
Parent tracking: GRO-2062 (Paperclip).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(rbac): port Better-Auth user auto-provision into legacy ./src tree (GRO-2052)
Ports the Better-Auth user-table auto-provision branch from canonical apps/api into the deployed ./src/middleware/rbac.ts so the owner-bypass in pets.ts is reachable for Better-Auth email/password customers. OIDC account branch retained as backward-compat fallback. Adds 5 rbac.test.ts cases and UAT_PLAYBOOK pre-condition docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Flea Flicker <flea@groombook.dev>
Co-committed-by: Flea Flicker <flea@groombook.dev>
Use sql\`count(*)::int\` instead of selecting appointments.id, which was
causing TS2339 under noUncheckedIndexedAccess (arr[0] is T | undefined).
Import sql from @groombook/db. Use countRow?.count ?? 0 to stay
noUncheckedIndexedAccess-safe.
Matches the working implementation in apps/api/src/routes/pets.ts:365.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds profile-summary endpoint for groombook web to display:
- Basic pet fields (name, species, breed, coatType, etc.)
- Recent grooming history (last 10 completed appointments with staff names)
- Visit count (completed appointments)
- Upcoming appointment (next scheduled/confirmed)
Groomer RBAC: groomers can only see pets they've had appointments with.
Non-groomer staff (admin/super) can see all pets.
Fixes GRO-1802 (UAT regression: profile-summary route never deployed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(gro-1866): add session-from-auth portal endpoint + role scope (#93)
Bridges Better Auth SSO sessions to portal sessions for real customers.
Adds role to genericOAuth scopes for Authentik role propagation.
Closes GRO-1866
Fixes two bugs found in QA review:
- ReferenceError: getAuth not defined in beforeEach - add import
- TypeError: wrong mock chain insert().into().values() vs insert().values()
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds POST /api/portal/session-from-auth which bridges a valid Better Auth
customer session (from SSO login) to a portal impersonation session, so
real SSO customers can access the client portal.
The endpoint is registered before the validatePortalSession catch-all so it
is not subject to that middleware. It validates the Better Auth session
from request cookies, looks up the client by email, creates an active
impersonation session, and returns { sessionId, clientId, clientName }.
Also adds "role" to the genericOAuth scopes so Authentik propagates the
role claim into Better Auth user objects (GRO-1862 root cause fix).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
With noUncheckedIndexedAccess:true, split("@")[0] returns string|undefined,
making `name` typed as string|undefined and failing the notNull staff.name
insert constraint. Fix by using ?? fallback on the array access.
Also add newStaff null guard after .returning() destructure — array
destructuring yields T|undefined with noUncheckedIndexedAccess enabled.
The DB coat_type enum only accepts: smooth, double, wire, curly, long, hairless.
"short" is not a valid value — corrected to "smooth".
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The /api/health endpoint returns 401 on UAT because authMiddleware
was not skipping it — the health check was registered on the Hono app
instance (not the api sub-router), placing it below authMiddleware on
the base app. The fix adds /api/health to the auth skip list alongside
/api/auth/.
The /health endpoint (registered at app level, above all middleware)
correctly returns 200. The /api/health endpoint must also be public
since the task requires confirming it returns 200.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The previous GRO-1544 PR changed /health to /api/health but removed
the /health endpoint entirely. This breaks:
- Dockerfile HEALTHCHECK (curl -f http://localhost:3000/health)
- K8s readinessProbe/livenessProbe (httpGet: path: /health, port: 3000)
Both paths are registered before auth middleware so both remain
publicly accessible without authentication.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The health check was registered on `app` at `/health`, but the HTTPRoute
routes `/api/*` to the API pod. Since auth middleware protects the /api
basePath, GET /api/health fell through to authMiddleware → 401.
Now registered on `api` before auth middleware at /api/health.
Updated UAT_PLAYBOOK.md §GRO-1485 — new health endpoint path.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Root src/routes/ used stale enum values (xlarge→extra_large,
smooth→silky, missing short/medium/silky from coatType) and
sizeCategory→petSizeCategory field name mismatch with the
pets table column.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Betters Auth v1.5.6 link-account.mjs:22 rejects OAuth callbacks when the
genericOAuth provider is not in trustedProviders AND email_verified is
falsy. Adding authentik to trustedProviders bypasses this guard so OIDC
login works for TF-created users whose emails were never verified through
an authentik flow.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Re-implement lost commit from worktree cleanup. PR #12 already has
UAT_PLAYBOOK + factories fix; add all missing core implementation:
- Add petSizeCategoryEnum/coatTypeEnum to schema
- Add bufferRules table with service FK + unique constraint
- Add defaultBufferMinutes column to services table
- Change pets.coatType/petSizeCategory text columns to use enums
- Add routes/buffer-rules.ts: GET/POST/PATCH/DELETE, manager role guard
- Register /api/buffer-rules in index.ts
- Update services.ts PATCH to accept defaultBufferMinutes
- Update pets.ts POST/PATCH to accept sizeCategory/coatType
- Cast coatType/petSizeCategory in book.ts insert to match new enums
- Add 0031_buffer_rules.sql migration
- Fix factories.ts buildService to include defaultBufferMinutes: null
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add petSizeCategory and petCoatType to bookingSchema zod validator (optional)
- Save coatType to pets row on booking creation
- Add coatType and petSizeCategory columns to pets DB schema
- Add coatType and petSizeCategory to Pet interface in @groombook/types
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add petSizeCategory and petCoatType to bookingSchema zod validator (optional)
- Save coatType to pets row on booking creation
- Add coatType and petSizeCategory columns to pets DB schema
- Add coatType and petSizeCategory to Pet interface in @groombook/types
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>