diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b37a76a..dfb785b 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main, dev] + branches: [main, dev, uat] pull_request: - branches: [main, dev] + branches: [main, dev, uat] workflow_dispatch: inputs: ref: diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index 166cf68..2cb3bab 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -103,6 +103,18 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet | 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) | +#### 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" | + ### 4.4 Appointment Scheduling | # | Scenario | Steps | Expected | diff --git a/apps/api/src/db/seed.ts b/apps/api/src/db/seed.ts index 566da17..fc65098 100644 --- a/apps/api/src/db/seed.ts +++ b/apps/api/src/db/seed.ts @@ -20,6 +20,7 @@ import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import { eq, and, sql } from "drizzle-orm"; import * as schema from "./schema.js"; +import type { MedicalAlert, MedicalAlertSeverity } from "./schema.js"; // ── Seed profile configuration ───────────────────────────────────────────── @@ -252,6 +253,38 @@ const appointmentNotes = [ "Client running late, pushed start by 15min", ]; +const temperamentScores = [3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9]; + +const temperamentFlags = [ + [], ["anxious"], ["friendly"], ["nippy"], ["anxious", "sensitive"], + ["friendly", "calm"], ["nippy", "territorial"], ["calm"], ["sensitive"], + ["friendly", "nippy"], ["anxious", "territorial"], +]; + +const medicalAlertsList = [ + [] as MedicalAlert[], + [] as MedicalAlert[], + [{ type: "skin", description: "Sensitive skin — avoid harsh shampoos", severity: "medium" as MedicalAlertSeverity }], + [{ type: "ear", description: "Ear infection prone — dry ears thoroughly", severity: "medium" as MedicalAlertSeverity }], + [{ type: "mobility", description: "Hip dysplasia — handle with care", severity: "high" as MedicalAlertSeverity }], + [{ type: "behavioral", description: "Anxious — needs slow approach", severity: "low" as MedicalAlertSeverity }], + [{ type: "medical", description: "Seizure history — avoid stress triggers", severity: "high" as MedicalAlertSeverity }], + [{ type: "skin", description: "Skin allergies — use hypoallergenic products only", severity: "medium" as MedicalAlertSeverity }], + [{ type: "behavioral", description: "Aggressive when nails trimmed — muzzle required", severity: "high" as MedicalAlertSeverity }], + [{ type: "cardiac", description: "Heart murmur — monitor during grooming", severity: "high" as MedicalAlertSeverity }], + [{ type: "dietary", description: "Diabetic — owner brings treats", severity: "medium" as MedicalAlertSeverity }], +]; + +const preferredCutsList = [ + [], ["Puppy Cut"], ["Teddy Bear Cut"], ["Breed Standard"], + ["Puppy Cut", "Sanitary Trim"], ["Full Groom"], ["Lion Cut"], + ["Kennel Cut", "Face & Feet Trim"], ["Teddy Bear Cut", "Sanitary Trim"], + ["Breed Standard", "Sanitary Trim"], ["Summer Shave"], + ["Puppy Cut", "Face & Feet Trim", "Sanitary Trim"], +]; + +const coatTypes: string[] = ["short", "medium", "long", "curly", "wire", "double", "silky"]; + const visitLogNotes = [ null, null, "Coat in great condition", @@ -872,6 +905,11 @@ async function seed() { cutStyle: pick(cutStyles), shampooPreference: pick(shampoos), specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null, + coatType: pick(coatTypes), + temperamentScore: pick(temperamentScores), + temperamentFlags: pick(temperamentFlags), + medicalAlerts: pick(medicalAlertsList), + preferredCuts: pick(preferredCutsList), customFields: {}, image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages), }); @@ -907,6 +945,11 @@ async function seed() { cutStyle: pet.cutStyle, shampooPreference: pet.shampooPreference, specialCareNotes: pet.specialCareNotes, + coatType: pet.coatType, + temperamentScore: pet.temperamentScore, + temperamentFlags: pet.temperamentFlags, + medicalAlerts: pet.medicalAlerts, + preferredCuts: pet.preferredCuts, customFields: pet.customFields, image: pet.image, }, @@ -929,13 +972,18 @@ async function seed() { petId: string; petName: string; petBreed: string; + petCoatType: string; + petTemperamentScore: number; + petTemperamentFlags: string[]; + petMedicalAlerts: MedicalAlert[]; + petPreferredCuts: string[]; } const uatClients: UatClient[] = [ - { id: uuid(), name: "UAT Test Alpha", email: "uat-alpha@groombook.dev", phone: "(555) 100-0001", address: "100 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestBuddy", petBreed: "Golden Retriever" }, - { id: uuid(), name: "UAT Test Bravo", email: "uat-bravo@groombook.dev", phone: "(555) 100-0002", address: "200 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestMax", petBreed: "Labrador Retriever" }, - { id: uuid(), name: "UAT Test Charlie", email: "uat-charlie@groombook.dev", phone: "(555) 100-0003", address: "300 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestCooper", petBreed: "Poodle" }, - { id: uuid(), name: "UAT Test Delta", email: "uat-delta@groombook.dev", phone: "(555) 100-0004", address: "400 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestRocky", petBreed: "French Bulldog" }, - { id: uuid(), name: "UAT Test Echo", email: "uat-echo@groombook.dev", phone: "(555) 100-0005", address: "500 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestDuke", petBreed: "Beagle" }, + { id: uuid(), name: "UAT Test Alpha", email: "uat-alpha@groombook.dev", phone: "(555) 100-0001", address: "100 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestBuddy", petBreed: "Golden Retriever", petCoatType: "double", petTemperamentScore: 7, petTemperamentFlags: ["calm", "friendly"], petMedicalAlerts: [] as MedicalAlert[], petPreferredCuts: ["Breed Standard"] }, + { id: uuid(), name: "UAT Test Bravo", email: "uat-bravo@groombook.dev", phone: "(555) 100-0002", address: "200 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestMax", petBreed: "Labrador Retriever", petCoatType: "short", petTemperamentScore: 8, petTemperamentFlags: ["friendly"], petMedicalAlerts: [] as MedicalAlert[], petPreferredCuts: ["Bath & Brush", "Sanitary Trim"] }, + { id: uuid(), name: "UAT Test Charlie", email: "uat-charlie@groombook.dev", phone: "(555) 100-0003", address: "300 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestCooper", petBreed: "Poodle", petCoatType: "curly", petTemperamentScore: 9, petTemperamentFlags: ["calm"], petMedicalAlerts: [{ type: "behavioral", description: "Anxious — needs slow approach", severity: "low" as MedicalAlertSeverity }], petPreferredCuts: ["Teddy Bear Cut"] }, + { id: uuid(), name: "UAT Test Delta", email: "uat-delta@groombook.dev", phone: "(555) 100-0004", address: "400 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestRocky", petBreed: "French Bulldog", petCoatType: "short", petTemperamentScore: 6, petTemperamentFlags: ["nippy"], petMedicalAlerts: [{ type: "skin", description: "Sensitive skin — avoid harsh shampoos", severity: "medium" as MedicalAlertSeverity }], petPreferredCuts: ["Puppy Cut"] }, + { id: uuid(), name: "UAT Test Echo", email: "uat-echo@groombook.dev", phone: "(555) 100-0005", address: "500 Test Lane, Springfield, CA 90210", petId: uuid(), petName: "TestDuke", petBreed: "Beagle", petCoatType: "short", petTemperamentScore: 7, petTemperamentFlags: ["friendly", "energetic"], petMedicalAlerts: [] as MedicalAlert[], petPreferredCuts: ["Full Groom", "Nail Trim"] }, ]; for (const uc of uatClients) { @@ -943,8 +991,26 @@ async function seed() { .values({ id: uc.id, name: uc.name, email: uc.email, phone: uc.phone, address: uc.address }) .onConflictDoUpdate({ target: schema.clients.id, set: { name: uc.name, email: uc.email, phone: uc.phone, address: uc.address } }); await db.insert(schema.pets) - .values({ id: uc.petId, clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), image: pick(demoPetImages) }) - .onConflictDoUpdate({ target: schema.pets.id, set: { clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), image: pick(demoPetImages) } }); + .values({ + id: uc.petId, clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, + weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), + coatType: uc.petCoatType, + temperamentScore: uc.petTemperamentScore, + temperamentFlags: uc.petTemperamentFlags, + medicalAlerts: uc.petMedicalAlerts, + preferredCuts: uc.petPreferredCuts, + image: pick(demoPetImages), + }) + .onConflictDoUpdate({ target: schema.pets.id, set: { + clientId: uc.id, name: uc.petName, species: "Dog", breed: uc.petBreed, + weightKg: "25.00", dateOfBirth: new Date("2021-03-15T00:00:00Z"), + coatType: uc.petCoatType, + temperamentScore: uc.petTemperamentScore, + temperamentFlags: uc.petTemperamentFlags, + medicalAlerts: uc.petMedicalAlerts, + preferredCuts: uc.petPreferredCuts, + image: pick(demoPetImages), + } }); // Create one completed appointment for this client const apptId = uuid(); const svcIdx = 0; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a108283..ac304d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -970,66 +970,79 @@ packages: resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.3': resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.3': resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.3': resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.3': resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.3': resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.3': resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.3': resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.3': resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.3': resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.3': resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.3': resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.3': resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.60.3': resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==}