Files
api/UAT_PLAYBOOK.md
T
Flea Flicker 8721f0b63c
CI / Test (push) Successful in 24s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Images (push) Successful in 35s
dev → uat: GRO-2154 geocoding endpoints (Phase 1.3) (#171)
2026-06-08 12:06:43 +00:00

38 KiB

UAT Playbook — GroomBook API

Overview

GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet grooming management platform. Handles authentication, client/pet management, appointment scheduling, invoicing, payments, staff management, and the customer portal.

Environments

Environment URL
Dev dev.groombook.dev
UAT uat.groombook.dev
Prod demo.groombook.app

Pre-conditions

  • UAT environment accessible and healthy
  • Test accounts seeded (manager, staff, client personas)
  • OIDC authentication provider configured
  • Seed data present (clients, pets, services, staff)

Source of truth for UAT passwords (GRO-2000)

The UAT_SUPER_PASSWORD / UAT_GROOMER_PASSWORD / UAT_TESTER_PASSWORD / UAT_CUSTOMER_PASSWORD env vars the test orchestrator uses must be pulled from the live seed-uat-passwords Secret in the UAT cluster — never from a captured shell value, a previous run's .env, or a copy of the SealedSecret committed before the latest rotation.

Canonical recipe (works from any host with kubectl + cluster credentials):

SUPER=$(kubectl get secret seed-uat-passwords -n groombook-uat \
  -o jsonpath='{.data.super-password}' | base64 -d)
GROOMER=$(kubectl get secret seed-uat-passwords -n groombook-uat \
  -o jsonpath='{.data.groomer-password}' | base64 -d)
TESTER=$(kubectl get secret seed-uat-passwords -n groombook-uat \
  -o jsonpath='{.data.tester-password}' | base64 -d)
CUSTOMER=$(kubectl get secret seed-uat-passwords -n groombook-uat \
  -o jsonpath='{.data.customer-password}' | base64 -d)

Why: the Bitnami SealedSecret apps/overlays/uat/ss-seed-uat-passwords.yaml (in groombook/infra) is the single source of truth. The UAT reset-demo-data CronJob re-hashes these values into the account table on every run (idempotent — GRO-1977). A captured env var from a previous generation will not match the current hash, producing 401 INVALID_EMAIL_OR_PASSWORD. If the live login still 401s after pulling from the SealedSecret, the seed Job is stale — trigger kubectl create job --from=cronjob/reset-demo-data -n groombook-uat manual-seed-$$ and retry.

How to apply: at the start of every UAT run that touches TC-API-1.4 / 1.5 / 1.6 / 1.7 / 3.18 / 3.21 / 3.23, refresh these four env vars from the cluster before issuing the sign-in request.

rbac auto-provision for Better-Auth customers (GRO-2052)

Applies to TC-API-3.16 / 3.19a / 3.19b / 3.19c (customer-as-owner profile-summary paths) and any future case where the test user authenticates via Better-Auth email/password and the route relies on resolveStaffMiddleware to resolve a staff row.

Pre-condition (rbac auto-provision): The test user must have a row in the Better-Auth user table (email/password sign-in creates this automatically — see TC-API-1.6 / 1.7). On first authenticated call, resolveStaffMiddleware (./src/middleware/rbac.ts) auto-provisions a groomer staff row keyed by staff.user_id = user.id (Better-Auth branch fires before the legacy OIDC account branch).

Verify the auto-provision fired by querying the DB after the first authenticated call:

SELECT user_id, role FROM staff WHERE user_id = '<test-user-id>';

Expected: one row, role = 'groomer'. If zero rows return, the request hit the OIDC account branch and 403'd, or the user has no user row — fix the test sign-in path before re-running.

Why this matters: without the auto-provision branch, Better-Auth email/password customers (e.g. uat-customer@groombook.dev) have no account row for the OIDC providers, so resolveStaffMiddleware falls through to 403 "Forbidden: no staff record found for authenticated user" before pets.ts can run the owner-bypass added in GRO-2013. The owner-bypass code is unreachable unless the auto-provision has fired. A green TC-API-3.19a therefore implicitly proves the auto-provision worked; if 3.19a fails with the pre-fix 403, the auto-provision branch is missing from the deployed ./src tree (see GRO-2052).

How to apply: for every run of TC-API-3.16 / 3.19a / 3.19b / 3.19c, sign in via TC-API-1.6 (email+password) first to guarantee the user row exists, then run the profile-summary call, then assert the staff row above before declaring pass.

Test Cases

4.0 Health Check

# Scenario Steps Expected
TC-API-0.1 Unauthenticated health check GET /api/health 200 OK, {"status":"ok"}

Note (GRO-1544): Health endpoint registered on api basePath before auth middleware at /api/health. The old path /health was incorrect (routed to web pod via HTTPRoute /* rule).

4.1 Authentication

# Scenario Steps Expected
TC-API-1.1 Login via OIDC POST to OIDC provider callback, verify JWT token issued 200 OK, JWT returned with valid claims
TC-API-1.4 Email+password login (UAT) POST /api/auth/sign-in/email with uat-super@groombook.dev + SEED_UAT_SUPER_PASSWORD 200 OK, session cookie returned
TC-API-1.5 Email+password login — groomer POST /api/auth/sign-in/email with uat-groomer@groombook.dev + SEED_UAT_GROOMER_PASSWORD 200 OK, session cookie returned
TC-API-1.6 Email+password login — customer POST /api/auth/sign-in/email with uat-customer@groombook.dev + SEED_UAT_CUSTOMER_PASSWORD 200 OK, session cookie returned
TC-API-1.7 Email+password login — tester POST /api/auth/sign-in/email with uat-tester@groombook.dev + SEED_UAT_TESTER_PASSWORD 200 OK, session cookie returned
TC-API-1.8 Email+password — invalid password POST /api/auth/sign-in/email with wrong password 400 Bad Request, error returned
TC-API-1.9 Email+password — unknown user POST /api/auth/sign-in/email with non-existent email 400 Bad Request, error returned
TC-API-1.10 Auto-provision on first OIDC login First login as a Better-Auth user with no existing staff record 200 OK, access granted; groomer staff record auto-created with name/email from user table

Note (GRO-1977): Seed credential provisioning is idempotent — re-running the seed with updated SEED_UAT_*_PASSWORD env vars rotates stored credential hashes. TC-API-1.4 through TC-API-1.7 now return 200 for all 4 UAT personas (previously returned 401 due to frozen-hash bug). | TC-API-1.11 | Existing staff unaffected by OIDC login | Login as uat-groomer@groombook.dev (email+password), then GET /api/staff to find that record | 200 OK, staff record unchanged — no duplicate created, original role and isSuperUser preserved | | TC-API-1.12 | Auto-provisioned role and superUser flags | After TC-API-1.10, GET /api/staff and inspect the auto-created record | role = "groomer", isSuperUser = false, active = true | | TC-API-1.13 | Name fallback — user.name present | Auto-provision where Better-Auth user has name set | Staff name = user.name value from user table | | TC-API-1.14 | Name fallback — no name, email present | Auto-provision where Better-Auth user has name = null, email = "test@example.com" | Staff name = "test" (email prefix before @) | | TC-API-1.15 | Name fallback — no name, no email | Auto-provision where Better-Auth user has name = null, email = null | Staff name = "Unknown" | | TC-API-1.16 | OIDC login — Terraform-provisioned user | Initiate OIDC login as any UAT persona (uat-super, uat-groomer, uat-customer, uat-tester), complete authentik callback | 200 OK, session created — no account_not_linked error |

SSO Login Journey (Authentik OIDC end-to-end)

# Scenario Steps Pass Criteria Fail Criteria
TC-API-1.17 SSO redirect to Authentik Navigate to app → sign-in page shown → click "Sign in with SSO" Redirected to Authentik at auth.farh.net 403 error, redirect loop, no SSO button
TC-API-1.18 Authenticate with valid OIDC credentials At Authentik login page, enter valid credentials and authenticate Redirected back to app with valid session Redirect loop, 403, missing session cookie
TC-API-1.19 SSO user auto-provisioned as groomer Complete SSO login as a user with no pre-existing staff record 200 response; groomer staff record auto-created; session active 403 Forbidden, staff record not created
TC-API-1.20 Existing staff record resolves correctly Complete SSO login as uat-groomer (pre-existing staff) 200 OK, correct staff identity resolved, no duplicate record created 403, duplicate record, wrong staff data
TC-API-1.21 SSO session grants dashboard access After TC-API-1.18 SSO login, GET /api/staff/me 200 OK, valid staff record returned, correct role displayed 401/403, missing session, wrong identity

OOBE Flow Post-Login

# Scenario Steps Pass Criteria Fail Criteria
TC-API-1.22 Fresh DB reports needsSetup On a fresh DB (no super user), GET /api/setup/status needsSetup: true returned needsSetup: false when it should be true
TC-API-1.23 Configure OIDC via auth-provider endpoint POST /api/setup/auth-provider with valid OIDC config 200 OK, auth provider configured, no 403 403, setup blocked, invalid config rejected
TC-API-1.24 Complete setup creates super user POST /api/setup with business name (after TC-API-1.23) First user becomes super user, setup completes Setup errors, 403 on admin endpoints
TC-API-1.25 Super user accesses admin features After TC-API-1.24, GET /api/staff/me and verify isSuperUser: true isSuperUser: true, admin endpoints accessible 403 on admin, isSuperUser: false
TC-API-1.26 Auto-provision skipped during OOBE During fresh setup (needsSetup: true), complete OIDC login — verify no duplicate staff record created before setup completes No duplicate staff, OOBE completes successfully Duplicate staff record, 403 before setup, auto-provision interferes with OOBE

4.2 Client Management

# Scenario Steps Expected
TC-API-2.1 List clients GET /api/clients 200 OK, list of active clients returned
TC-API-2.2 Get client details GET /api/clients/{id} 200 OK, client details returned
TC-API-2.3 Create client POST /api/clients with valid data 201 Created, client record created
TC-API-2.4 Update client PATCH /api/clients/{id} with updated fields 200 OK, client updated
TC-API-2.5 Disable client PATCH /api/clients/{id} with status: "disabled" 200 OK, client marked as disabled
TC-API-2.6 Delete client DELETE /api/clients/{id}?confirm=true 200 OK, client deleted (if no appointments)

Client Geocoding — Route Optimization (GRO-2154, Phase 1.3)

Geocoding turns a client's street address into latitude/longitude + geocodedAt. Provider is driven by businessSettings.routeOptimizationProvider (default Nominatim/OpenStreetMap, 1 req/sec; optional Google fallback). All explicit geocode endpoints are manager-only.

# Scenario Steps Expected
TC-API-2.7 Geocode single client (success) As manager, POST /api/clients/{id}/geocode for a client with a valid, real address (e.g. a seed client) 200 OK; body { status: "geocoded", latitude, longitude, geocodedAt, formattedAddress, provider }. Subsequent GET /api/clients/{id} shows the same non-null latitude/longitude/geocodedAt persisted
TC-API-2.8 Geocode single client — no address As manager, POST /api/clients/{id}/geocode for a client whose address is null/blank 422; { status: "no_address", message: "...no address on file..." } (clear, actionable)
TC-API-2.9 Geocode single client — unresolvable/ambiguous address As manager, set a nonsense address (e.g. "asdkjhqweoui 99999") then POST /api/clients/{id}/geocode 422; { status: "unresolved", message: "Address could not be resolved..." } so groomers/managers know to correct it
TC-API-2.10 Geocode single client — not found As manager, POST /api/clients/00000000-0000-0000-0000-000000000000/geocode 404 { error: "Not found" }
TC-API-2.11 Geocode endpoint is manager-only As groomer or receptionist, POST /api/clients/{id}/geocode 403 Forbidden (role not permitted)
TC-API-2.12 Batch geocode un-geocoded clients As manager, POST /api/clients/geocode-batch?limit=10 on a DB with un-geocoded clients 200 OK; body { provider, processed, geocoded, unresolved, errors, remaining, outcomes[] }. processed ≤ 10; remaining reflects un-geocoded clients beyond this batch. Re-run while remaining > 0 to finish (throttled to provider rate limit)
TC-API-2.13 Batch geocode — invalid limit As manager, POST /api/clients/geocode-batch?limit=0 (or non-numeric) 400 { error: "limit must be a positive integer" }
TC-API-2.14 Batch geocode — manager-only As groomer/receptionist, POST /api/clients/geocode-batch 403 Forbidden
TC-API-2.15 Auto-geocode on create As manager/receptionist, POST /api/clients with a valid address 201 Created; response includes a geocoding object (status: "geocoded" for a resolvable address) and the persisted client carries latitude/longitude/geocodedAt. Creating without an address succeeds with no geocoding field
TC-API-2.16 Auto-geocode on address update As manager/receptionist, PATCH /api/clients/{id} changing address to a new valid value 200 OK; response includes a geocoding object and refreshed coordinates. Patching unrelated fields (e.g. name) does NOT re-geocode (no geocoding field)
TC-API-2.17 Clearing address drops coordinates As manager/receptionist, PATCH /api/clients/{id} with address: "" 200 OK; latitude/longitude/geocodedAt reset to null (no stale pin)

4.3 Pet Management

# Scenario Steps Expected
TC-API-3.1 List pets GET /api/pets 200 OK, list of pets returned
TC-API-3.2 Get pet details GET /api/pets/{id} 200 OK, pet details including history returned
TC-API-3.3 Add pet POST /api/pets with valid pet data 201 Created, pet record created
TC-API-3.4 Update pet PATCH /api/pets/{id} with updated fields 200 OK, pet updated
TC-API-3.5 Delete pet DELETE /api/pets/{id} 200 OK, pet deleted
TC-API-3.6 Upload pet photo POST /api/pets/{id}/photo/upload-url, then confirm 200 OK, photo uploaded and key stored
TC-API-3.7 View pet photo GET /api/pets/{id}/photo 200 OK, presigned URL returned
TC-API-3.8 Create pet with extended fields POST /api/pets with coatType, temperamentScore, temperamentFlags, medicalAlerts, preferredCuts 201 Created, all extended fields stored and returned
TC-API-3.9 Update pet extended fields PATCH /api/pets/{id} with coatType, temperamentScore, medicalAlerts 200 OK, extended fields updated
TC-API-3.10 Reject invalid coatType POST /api/pets with coatType: "smooth" 400 Bad Request, invalid coatType rejected
TC-API-3.11 Reject out-of-range temperamentScore POST /api/pets with temperamentScore: 0 or 6 400 Bad Request, score out of range rejected
TC-API-3.12 Reject invalid medicalAlert severity POST /api/pets with medicalAlerts severity: "critical" 400 Bad Request, invalid severity rejected
TC-API-3.13 Reject too many temperamentFlags POST /api/pets with 21 temperamentFlags 400 Bad Request, max 20 flags enforced
TC-API-3.14 Reject too many preferredCuts POST /api/pets with 21 preferredCuts 400 Bad Request, max 20 cuts enforced
TC-API-3.15 Reject too many medicalAlerts POST /api/pets with 51 medicalAlerts 400 Bad Request, max 50 alerts enforced
TC-API-3.16 Get pet profile summary GET /api/pets/{id}/profile-summary 200 OK, aggregated profile with grooming history, visit count, upcoming appointment
TC-API-3.17 Get pet profile summary — groomer restricted GET /api/pets/{id}/profile-summary as groomer with no pet linkage 403 Forbidden
TC-API-3.18 Get pet profile summary — visitCount returns full count GET /api/pets/{id}/profile-summary with 2+ completed appointments visitCount >= 2 (not capped at 1)
TC-API-3.19 Get pet profile summary — upcomingAppointment excludes past GET /api/pets/{id}/profile-summary with a past confirmed/scheduled appointment upcomingAppointment is null (past appointments filtered by startTime >= now)
TC-API-3.19a Get pet profile summary — customer owner-bypass (GRO-2013) Sign in as uat-customer@groombook.dev; POST /api/portal/session-from-auth; then GET /api/pets/{ownPetId}/profile-summary with header X-Impersonation-Session-Id: {sessionId} for either of the customer's seeded pets (c0000001-0000-0000-0000-000000000002 UAT Pup Alpha, c0000001-0000-0000-0000-000000000003 UAT Pup Beta) 200 OK, aggregated profile returned (owner-bypass: customer with valid portal session for pet's clientId is allowed even though rbac.ts auto-provisions them as a groomer staff row with no appointment linkage)
TC-API-3.19b Get pet profile summary — customer cross-tenant blocked (GRO-2013) Sign in as uat-customer@groombook.dev; reuse the customer's sessionId from TC-API-3.19a; GET /api/pets/{otherClientPetId}/profile-summary for a pet owned by a different client (c0000002-... or any non-customer pet) 403 Forbidden (owner-bypass requires session.clientId === pet.clientId)
TC-API-3.19c Get pet profile summary — customer without portal session header Same as TC-API-3.19a but omit the X-Impersonation-Session-Id header 403 Forbidden (no owner-bypass without valid portal session)
TC-API-3.19d Get pet profile summary — owner-bypass writes audit row (GRO-2063) Same setup as TC-API-3.19a (sign in as uat-customer@groombook.dev, establish a portal session for the customer's own clientId, call GET /api/pets/{ownPetId}/profile-summary with X-Impersonation-Session-Id: {sessionId} and a 200 OK response). Then call GET /api/impersonation/sessions/{sessionId}/audit-log and confirm there is exactly one entry with action === "read_profile_summary", pageVisited matching the profile-summary path, and metadata containing petId and actorStaffId for the customer. Repeat TC-API-3.19b (cross-tenant attempt) and confirm NO new read_profile_summary row was written for the cross-tenant attempt. 200 OK on the profile-summary call AND an audit log entry is present with the correct shape (defense-in-depth audit row; bypass attempts against other clients must NOT log)
TC-UAT-2 Groomer accesses linked pet profile summary (GRO-2100) Sign in as uat-groomer@groombook.dev; GET /api/pets/c0000001-0000-0000-0000-000000000002/profile-summary (UAT Pup Alpha — linked via deterministic completed appointment a0000001-0000-0000-0000-000000000001, service b0000001-…-0001 "Bath & Brush", startTime ~7 days ago) 200 OK, recentGroomingHistory[] non-empty (>=1 entry), visitCount >= 1, upcomingAppointment null (the seeded appointment is in the past)
TC-UAT-3 Groomer blocked from unlinked pet profile summary (GRO-2100) Sign in as uat-groomer@groombook.dev; GET /api/pets/c0000001-0000-0000-0000-000000000003/profile-summary (UAT Pup Beta — intentionally UNLINKED; no appointment row references this pet's clientId+groomerId combo) 403 Forbidden (RBAC groomer role lacks the appointment-linkage grant for this pet). NOTE: if 404 is returned instead of 403, file a separate RBAC defect (not against the seed) — see GRO-2100 verification note
TC-API-3.29 Get pet profile summary — unknown UUID returns 404 (GRO-2014) GET /api/pets/00000000-0000-0000-0000-000000000001/profile-summary while authenticated (any role) 404 Not Found with body {"error":"Not found"} (was empty-body 500 in GRO-2014)
TC-API-3.30 Get pet profile summary — malformed UUID returns 404 (GRO-2014) GET /api/pets/not-a-uuid/profile-summary while authenticated 404 Not Found with body {"error":"Not found"} (was empty-body 500 in GRO-2014 — Postgres uuid cast failure)
TC-API-3.31 Get pet profile summary — never empty-body 500 (GRO-2014) GET /api/pets/{anyId}/profile-summary across the test sweep No response has status 500 with an empty body. Any 500 must include a JSON body {"error":"Internal Server Error"}

Seed Data Verification (GRO-1898)

As of PR #98, UAT seed data populates all 5 extended profile fields for every pet, including the 5 deterministic UAT test client pets (Alpha, Bravo, Charlie, Delta, Echo). This enables manual verification of extended profile rendering without requiring a DB reset.

# Scenario Steps Expected
TC-API-3.20 GET /api/clients returns seed data GET /api/clients 200 OK, array with 1+ clients (UAT seed creates 500 + 5 deterministic UAT clients)
TC-API-3.21 GET /api/pets/{id} returns extended fields for seed pet Pick any pet ID from UAT test clients (uat-alpha through uat-echo pet names: TestBuddy, TestMax, TestCooper, TestRocky, TestDuke) and GET /api/pets/{id} 200 OK; coatType, temperamentScore, temperamentFlags, medicalAlerts, preferredCuts all non-null
TC-API-3.22 Verify medicalAlerts shape GET /api/pets/{id} for any pet with non-empty medicalAlerts medicalAlerts is an array; each entry has type, description, severity
TC-API-3.23 Verify UAT test pet Charlie has behavioral alert GET /api/pets/{id} where name = "TestCooper" (pet for uat-charlie@groombook.dev) medicalAlerts includes an entry with type: "behavioral", severity: "low" or "high"
TC-API-3.24 Verify UAT test pet Delta has skin alert GET /api/pets/{id} where name = "TestRocky" (pet for uat-delta@groombook.dev) medicalAlerts includes an entry with type: "skin"
TC-API-3.25 Verify 30+ total pets in UAT DB GET /api/pets then count total 30+ pets returned (UAT seed creates 500 random-pool + 5 UAT test clients + 2 UAT customer = 507 total)
TC-API-3.26 Verify 25-35% medicalAlerts distribution GET /api/pets (first 30 pets), count how many have non-empty medicalAlerts Ratio is 25-35% (seed uses rand() < 0.3 for ~30% distribution)
TC-API-3.27 Verify coat_type enum has all seed values After UAT seed completes, inspect the coat_type enum on the UAT DB — it must contain: short, medium, long, double, wire, silky, curly, hairless UAT seed jobs (reset-demo-data, seed-test-data) complete 1/1 with no enum_in error; coat_type includes all 8 values used by seed.ts coatTypePool
TC-API-3.28 Verify pet_size_category enum has all seed values After UAT seed completes, inspect the pet_size_category enum on the UAT DB — it must contain: small, medium, large, extra_large UAT seed jobs (reset-demo-data, seed-test-data) complete 1/1 with no enum_in error; pet_size_category includes all 4 values used by seed.ts petSizeCategoryPool (regression for GRO-1999, mirrors TC-API-3.27)
TC-API-3.29 Verify reset-demo-data CronJob does not fail with FK 23503 on invoice_tip_splits (GRO-2123) Trigger the CronJob manually: kubectl create job --from=cronjob/reset-demo-data verify-gro2123 -n groombook-uat. Wait for pod to terminate. Inspect logs: kubectl logs -n groombook-uat -l job-name=verify-gro2123 Pod reaches Completed state; logs show ✓ Acquired seed advisory lock and ✓ Released seed advisory lock from seed.ts; no PostgresError: … violates foreign key constraint "invoice_tip_splits_invoice_id_invoices_id_fk" (code 23503); final counts unchanged (500 clients, ~4000 invoices)

4.4 Appointment Scheduling

# Scenario Steps Expected
TC-API-4.1 List appointments GET /api/appointments 200 OK, list of appointments returned
TC-API-4.2 Get appointment details GET /api/appointments/{id} 200 OK, appointment details returned
TC-API-4.3 Create single appointment POST /api/appointments with valid data 201 Created, appointment created
TC-API-4.4 Create recurring appointment POST /api/appointments with recurrence object 201 Created, series of appointments created
TC-API-4.5 Update appointment PATCH /api/appointments/{id} with updated fields 200 OK, appointment updated
TC-API-4.6 Reschedule with cascade PATCH /api/appointments/{id} with cascadeMode: "this_and_future" 200 OK, future appointments updated
TC-API-4.7 Cancel appointment DELETE /api/appointments/{id} 200 OK, appointment marked as cancelled
TC-API-4.8 Confirm appointment POST /api/appointments/{id}/confirm 200 OK, confirmation status set to confirmed
TC-API-4.9 Cancel confirmation POST /api/appointments/{id}/cancel 200 OK, confirmation cancelled
TC-API-4.10 Conflict detection POST /api/appointments with conflicting time 409 Conflict, error message returned

4.5 Services

# Scenario Steps Expected
TC-API-5.1 List services GET /api/services 200 OK, list of active services returned
TC-API-5.2 Get service details GET /api/services/{id} 200 OK, service details returned
TC-API-5.3 Create service POST /api/services with valid data 201 Created, service created
TC-API-5.4 Update service PATCH /api/services/{id} with updated fields 200 OK, service updated
TC-API-5.5 Delete service DELETE /api/services/{id} 200 OK, service deleted

4.5.1 Seed/Reset idempotency (GRO-2064)

Services seeding is now keyed on the deterministic services.id (not name) and the reset path now TRUNCATEs services alongside the other dynamic tables. This means:

  • Running the seed Job twice in a row (no reset in between) converges to the same catalogue — no services_pkey collision.
  • A pnpm reset followed by pnpm seed (or a CronJob reset fire) leaves the catalogue exactly matching servicesDef (10 rows, ids b0000001-…-001…-00a), regardless of any stale rows that were present beforehand.
  • Mixed seedKnownUsers + full seed() invocations are safe — the demoSvcs subset (Bath & Brush, Full Groom Small/Medium, Nail Trim) is keyed on ids …-001, …-002, …-003, …-005 and the upsert target is services.id, so the same-id / different-name collision that broke GRO-2033 (id …-004 = "Nail Trim" vs servicesDef …-004 = "Full Groom — Large") cannot recur.

UAT regression (verify after a new image is rolled out):

# Scenario Steps Expected
TC-SEED-1 Reset → seed converges kubectl -n groombook exec deploy/api -- pnpm reset && pnpm seed Seed completes 1/1, services count = 10, all ids match servicesDef
TC-SEED-2 Idempotent re-seed Re-run pnpm seed without reset Seed completes 1/1, no services_pkey errors, services count still 10
TC-SEED-3 Catalogue matches servicesDef psql -c "SELECT id, name FROM services ORDER BY id" Rows …-001…-00a with names "Bath & Brush"…"Sanitary Trim" exactly as in servicesDef
TC-SEED-4 Demo subset coexists Run seedKnownUsers then full seed No collision, demo subset (4 services) ends up with the same rows the full seed would write

4.6 Staff Management

# Scenario Steps Expected
TC-API-6.1 List staff GET /api/staff 200 OK, list of active staff returned
TC-API-6.2 Get staff details GET /api/staff/{id} 200 OK, staff details returned
TC-API-6.3 Create staff POST /api/staff with valid data 201 Created, staff created
TC-API-6.4 Update staff PATCH /api/staff/{id} with updated fields 200 OK, staff updated
TC-API-6.5 Delete staff DELETE /api/staff/{id} 200 OK, staff deleted (if no appointments)
TC-API-6.6 RBAC check Access manager-only endpoint as groomer 403 Forbidden, error message returned

4.7 Invoicing & Payments

# Scenario Steps Expected
TC-API-7.1 List invoices GET /api/invoices 200 OK, list of invoices returned
TC-API-7.2 Get invoice details GET /api/invoices/{id} 200 OK, invoice with line items returned
TC-API-7.3 Create invoice POST /api/invoices with line items 201 Created, invoice created
TC-API-7.4 Create from appointment POST /api/invoices/from-appointment/{appointmentId} 201 Created, invoice created from appointment
TC-API-7.5 Update invoice PATCH /api/invoices/{id} with status and payment method 200 OK, invoice updated
TC-API-7.6 Process payment via Stripe POST /api/invoices/{id}/pay with Stripe data 200 OK, payment intent created
TC-API-7.7 Save tip splits POST /api/invoices/{id}/tip-splits with splits array 201 Created, tip splits saved
TC-API-7.8 Process refund POST /api/invoices/{id}/refund with amount 200 OK, refund processed

4.8 Customer Portal

# Scenario Steps Expected
TC-API-8.1 Access portal GET /api/portal/me with valid session token 200 OK, client profile returned
TC-API-8.2 View portal appointments GET /api/portal/appointments 200 OK, list of client's appointments returned
TC-API-8.3 Confirm appointment via portal POST /api/portal/appointments/{id}/confirm 200 OK, appointment confirmed
TC-API-8.4 Cancel appointment via portal POST /api/portal/appointments/{id}/cancel 200 OK, appointment cancelled
TC-API-8.5 Add waitlist entry POST /api/portal/waitlist with pet and service 201 Created, waitlist entry created
TC-API-8.6 View portal invoices GET /api/portal/invoices 200 OK, list of client's invoices returned
TC-API-8.7 Pay multiple invoices POST /api/portal/invoices/pay-multiple with invoice IDs 200 OK, payment intent created
TC-API-8.8 SSO bridge — valid Better Auth session POST /api/portal/session-from-auth with valid Better Auth session cookie (authenticated SSO user with matching client email) 201 Created, {sessionId, clientId, clientName} returned
TC-API-8.9 SSO bridge — no Better Auth session POST /api/portal/session-from-auth without Better Auth session cookie 401 Unauthorized
TC-API-8.10 SSO bridge — no matching client POST /api/portal/session-from-auth with valid Better Auth session for a user with no client record 404 Not Found, error "No client record found for this user"
TC-API-8.11 SSO bridge — returned session works on portal routes After TC-API-8.8, use returned sessionId as X-Impersonation-Session-Id header on GET /api/portal/me 200 OK, client profile returned
TC-API-8.12 Portal GET pets returns extended fields (GRO-2187) Establish a portal session (TC-API-8.8), then GET /api/portal/pets with X-Impersonation-Session-Id 200 OK; each pet includes coatType, petSizeCategory, healthAlerts, preferredCuts, medicalAlerts (in addition to id/name/breed/weight/birthDate/photoUrl/notes)
TC-API-8.13 Portal pet update — owner success + persistence (GRO-2187, fixes GRO-1480 §5.23) With a portal session for the pet's owner, PATCH /api/portal/pets/{petId} with body { "name": "...", "breed": "...", "weightKg": 18.25, "healthAlerts": "...", "coatType": "double", "petSizeCategory": "xlarge", "preferredCuts": ["teddy bear"], "medicalAlerts": [{"type":"allergy","description":"oatmeal","severity":"medium"}] } 200 OK; response reflects the update with petSizeCategory: "extra_large" (web xlarge → DB extra_large). A follow-up GET /api/portal/pets shows the persisted values
TC-API-8.14 Portal pet update — non-owner blocked (GRO-2187) PATCH /api/portal/pets/{petId} for a pet owned by a different client, using another client's portal session 403 Forbidden (or 404 if pet id is unknown); no mutation persisted
TC-API-8.15 Portal pet update — invalid enum rejected (GRO-2187) PATCH /api/portal/pets/{petId} with coatType: "fluffy" or petSizeCategory: "gigantic" 422 Unprocessable Entity; pet unchanged

4.9 Waitlist

# Scenario Steps Expected
TC-API-9.1 List waitlist GET /api/waitlist 200 OK, list of waitlist entries returned
TC-API-9.2 Add to waitlist POST /api/waitlist with client, pet, service 201 Created, entry added
TC-API-9.3 Promote from waitlist Create appointment from waitlist entry 201 Created, appointment created, waitlist updated
# Scenario Steps Expected
TC-API-10.1 Global search clients GET /api/search?q={client_name} 200 OK, matching clients returned
TC-API-10.2 Global search pets GET /api/search?q={pet_name} 200 OK, matching pets with owners returned
TC-API-10.3 Search by email GET /api/search?q={email} 200 OK, matching client returned
TC-API-10.4 Search by phone GET /api/search?q={phone} 200 OK, matching client returned

4.11 Reports

# Scenario Steps Expected
TC-API-11.1 Revenue summary GET /api/reports/summary?from={date}&to={date} 200 OK, revenue KPIs returned
TC-API-11.2 Revenue by period GET /api/reports/revenue?groupBy=day 200 OK, daily revenue breakdown returned
TC-API-11.3 Appointment analytics GET /api/reports/appointments 200 OK, appointment stats returned
TC-API-11.4 Service popularity GET /api/reports/services 200 OK, service usage stats returned
TC-API-11.5 Client retention GET /api/reports/clients 200 OK, new/returning/churn client data returned
TC-API-11.6 Tip splits report GET /api/reports/tip-splits 200 OK, tip earnings per staff returned
TC-API-11.7 Export revenue CSV GET /api/reports/export.csv?type=revenue 200 OK, CSV file downloaded

4.12 Impersonation

# Scenario Steps Expected
TC-API-12.1 Start impersonation session POST /api/impersonation/sessions with clientId 201 Created, session token returned
TC-API-12.2 Get session details GET /api/impersonation/sessions/{id} 200 OK, session details returned
TC-API-12.3 Extend session POST /api/impersonation/sessions/{id}/extend 200 OK, session expiry extended
TC-API-12.4 End session POST /api/impersonation/sessions/{id}/end 200 OK, session marked as ended
TC-API-12.5 Log audit entry POST /api/impersonation/sessions/{id}/log 201 Created, audit log entry created
TC-API-12.6 View audit log GET /api/impersonation/sessions/{id}/audit-log 200 OK, audit trail returned

4.13 Settings & Setup

# Scenario Steps Expected
TC-API-13.1 Get business settings GET /api/admin/settings 200 OK, business settings returned
TC-API-13.2 Update business settings PATCH /api/admin/settings with updated values 200 OK, settings updated
TC-API-13.3 Upload logo POST /api/admin/settings/logo/upload with file 200 OK, logo uploaded and stored
TC-API-13.4 View logo GET /api/admin/settings/logo 200 OK, logo image returned
TC-API-13.5 Delete logo DELETE /api/admin/settings/logo 200 OK, logo removed
TC-API-13.6 Check setup status GET /api/setup/status 200 OK, setup needs returned
TC-API-13.7 Complete setup POST /api/setup with business name 201 Created, super user created
TC-API-13.8 Configure auth provider POST /api/setup/auth-provider with OIDC config 201 Created, auth provider configured
TC-API-13.9 Test auth provider POST /api/setup/auth-provider/test with issuer URL 200 OK, OIDC discovery successful

4.14 Appointment Groups

# Scenario Steps Expected
TC-API-14.1 List appointment groups GET /api/appointment-groups 200 OK, list of groups returned
TC-API-14.2 Get group details GET /api/appointment-groups/{id} 200 OK, group with appointments returned
TC-API-14.3 Create group booking POST /api/appointment-groups with client and pets 201 Created, group and appointments created
TC-API-14.4 Update group notes PATCH /api/appointment-groups/{id} with notes 200 OK, notes updated
TC-API-14.5 Cancel group DELETE /api/appointment-groups/{id} 200 OK, all appointments cancelled

4.15 Buffer Rules

# Scenario Steps Expected
TC-API-15.1 List buffer rules GET /api/admin/buffer-rules 200 OK, list of active buffer rules returned
TC-API-15.2 Create buffer rule POST /api/admin/buffer-rules with service, species, sizeCategory, bufferMinutes 201 Created, buffer rule created
TC-API-15.3 Update buffer rule PATCH /api/admin/buffer-rules/{id} with updated bufferMinutes 200 OK, buffer rule updated
TC-API-15.4 Delete buffer rule DELETE /api/admin/buffer-rules/{id} 200 OK, buffer rule removed
TC-API-15.5 Reject invalid bufferMinutes POST /api/admin/buffer-rules with bufferMinutes: -5 400 Bad Request, invalid bufferMinutes rejected
TC-API-15.6 Reject missing required fields POST /api/admin/buffer-rules with service only 400 Bad Request, species and sizeCategory required
TC-API-15.7 Booking uses buffer Book appointment for pet with sizeCategory; verify duration reflects buffer 201 Created, appointment duration includes buffer time

Pass/Fail Criteria

Pass:

  • All test cases execute without errors
  • Expected results match actual results
  • No regressions in previously working features
  • API responses have correct status codes and data structures
  • Authentication and authorization enforced correctly
  • Business rules (conflicts, validations) work as expected

Fail:

  • Any unexpected result or error
  • API returns incorrect status codes
  • Data integrity issues
  • Authentication/authorization bypass
  • Business rules not enforced
  • Severity documented with steps to reproduce and screenshot

Update Policy

Any PR that changes user-facing behaviour MUST update this file. Test cases must be added, modified, or removed to reflect the new behaviour. The PR description must reference which playbook section was updated (e.g., "Updated UAT_PLAYBOOK.md §4.4 — new appointment rescheduling flow").