From de16c50040021108b3fc1f3e0842fb636502d7c8 Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Tue, 2 Jun 2026 18:09:31 +0000 Subject: [PATCH 1/3] =?UTF-8?q?fix(seed):=20GRO-2100=20deterministic=20uat?= =?UTF-8?q?-groomer=20=E2=86=94=20UAT=20Pup=20Alpha=20linkage=20(#151)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/db/src/seed.ts | 102 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index c647098..13cf103 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -668,6 +668,108 @@ async function seedUatStaffAccounts(db: ReturnType) { console.log(`✓ Created UAT pet '${pet.name}' with extended fields`); } } + + // ── GRO-2100: deterministic uat-groomer ↔ pet linkage ─────────────────────── + // The UAT groomer (`uat-groomer@groombook.dev`, staffId 00000000-0000-0000-0000-000000000004) + // needs at least one linked pet/appointment or GRO-1987 TC-UAT-2/3 cannot run + // (the pet profile-summary endpoint returns 404 instead of 200/403). + // + // We deterministically link the UAT groomer to the UAT customer's first pet + // ("UAT Pup Alpha") and leave the second pet ("UAT Pup Beta") UNLINKED so + // TC-UAT-2 (200) and TC-UAT-3 (403) can both hardcode the stable petIds. + await seedUatGroomerLinkage(db, uatCustomerClientId); +} + +/** + * GRO-2100: create a deterministic completed appointment linking the UAT groomer + * to "UAT Pup Alpha" (c0000001-0000-0000-0000-000000000002). "UAT Pup Beta" + * (c0000001-0000-0000-0000-000000000003) is intentionally left UNLINKED so + * GRO-1987 TC-UAT-3 can verify the 403 forbidden response. + * + * Idempotent: the deterministic appointment id (`a0000001-…-0001`) is the + * upsert key, so re-running the seed on every reset-demo-data CronJob + * (hourly per apps/overlays/uat/reset-cronjob.yaml) is safe. + */ +async function seedUatGroomerLinkage( + db: ReturnType, + customerClientId: string, +): Promise { + const uatGroomerEmail = "uat-groomer@groombook.dev"; + const LINKED_PET_ID = "c0000001-0000-0000-0000-000000000002"; // UAT Pup Alpha + const APPT_ID = "a0000001-0000-0000-0000-000000000001"; + + // Only run if the UAT groomer staff record actually exists — dev/test seeds + // that don't set SEED_UAT_STAFF_OIDC_SUB should not crash. + const [uatGroomerStaff] = await db + .select({ id: schema.staff.id }) + .from(schema.staff) + .where(eq(schema.staff.email, uatGroomerEmail)) + .limit(1); + if (!uatGroomerStaff) { + return; + } + + // Skip if this exact appointment already exists (idempotent on re-seed). + const [existing] = await db + .select({ id: schema.appointments.id }) + .from(schema.appointments) + .where(eq(schema.appointments.id, APPT_ID)) + .limit(1); + if (existing) { + console.log(`✓ GRO-2100: uat-groomer linkage appointment already exists — skipping`); + return; + } + + // The "Bath & Brush" service id is stable across the reset; falls back to + // any active service if it has not been seeded yet (e.g. seedKnownUsers + // runs in isolation). + const BATH_AND_BRUSH_ID = "b0000001-0000-0000-0000-000000000001"; + const [bathService] = await db + .select({ id: schema.services.id }) + .from(schema.services) + .where(eq(schema.services.id, BATH_AND_BRUSH_ID)) + .limit(1); + + let serviceId: string; + if (bathService) { + serviceId = bathService.id; + } else { + const [fallback] = await db + .select({ id: schema.services.id }) + .from(schema.services) + .where(eq(schema.services.active, true)) + .limit(1); + if (!fallback) { + console.warn(`⚠ GRO-2100: no active services found — skipping uat-groomer linkage`); + return; + } + serviceId = fallback.id; + } + + // Schedule the completed appointment 7 days ago so the profile-summary's + // "recentGroomingHistory" window (last 10) reliably includes it. + const startTime = new Date(); + startTime.setDate(startTime.getDate() - 7); + startTime.setHours(10, 0, 0, 0); + const endTime = new Date(startTime.getTime() + 45 * 60 * 1000); + + await db.insert(schema.appointments).values({ + id: APPT_ID, + clientId: customerClientId, + petId: LINKED_PET_ID, + serviceId, + staffId: uatGroomerStaff.id, + batherStaffId: null, + status: "completed", + startTime, + endTime, + notes: "GRO-2100: deterministic uat-groomer linkage for TC-UAT-2/3.", + priceCents: null, + confirmationStatus: "confirmed", + }); + console.log( + `✓ GRO-2100: linked uat-groomer (${uatGroomerStaff.id}) → UAT Pup Alpha (${LINKED_PET_ID}) via appointment ${APPT_ID}`, + ); } // ── Known-users-only seed (prod/demo) ─────────────────────────────────────── From bd384bdf5c55730d9bec4036f7e7de91e8dcbfae Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 2 Jun 2026 18:24:40 +0000 Subject: [PATCH 2/3] docs(UAT_PLAYBOOK): add TC-UAT-2/3 for uat-groomer linked/unlinked pet profile-summary (GRO-2100) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lint Roller review on PR #152 flagged that the GRO-2100 seed change produces new observable UAT API behavior that the playbook must reflect. Add two deterministic rows pinning the contract GRO-1987 TC-UAT-2/3 will exercise: - TC-UAT-2: uat-groomer + linked pet c0000001-...-002 (UAT Pup Alpha) → 200 - TC-UAT-3: uat-groomer + unlinked pet c0000001-...-003 (UAT Pup Beta) → 403 The 403-vs-404 note in TC-UAT-3 mirrors the verification note in the GRO-2100 issue body so the QA runner knows where to file if the API returns 404 (a separate RBAC defect, not against the seed). --- UAT_PLAYBOOK.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index b8949de..ed52ecd 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -147,6 +147,8 @@ Expected: one row, `role = 'groomer'`. If zero rows return, the request hit the | 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"}` | From d4a4ddce37ba17fccdf4d3e91308f00848d3e936 Mon Sep 17 00:00:00 2001 From: Paperclip Date: Tue, 2 Jun 2026 18:28:17 +0000 Subject: [PATCH 3/3] =?UTF-8?q?ci:=20retrigger=20GRO-2100=20PR=20#152=20Bu?= =?UTF-8?q?ild=20&=20Push=20Docker=20Images=20(Reset=20image=20build=20fai?= =?UTF-8?q?led=20=E2=80=94=20docker=20registry=20flake)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit