Promote: dev → uat (GRO-1913 extended pet profile seed + hotfixes) (#103)
Promotes GRO-1913 + GRO-1915 + GRO-1917 from dev to uat. Approved by gb_lint on b5f964c1. CI run #2044 green.
This commit was merged in pull request #103.
This commit is contained in:
@@ -32,7 +32,9 @@ jobs:
|
|||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: pnpm --filter @groombook/api typecheck
|
run: |
|
||||||
|
pnpm --filter @groombook/api typecheck
|
||||||
|
pnpm --filter @groombook/db typecheck
|
||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: pnpm --filter @groombook/api lint
|
run: pnpm --filter @groombook/api lint
|
||||||
|
|||||||
@@ -50,4 +50,5 @@ CMD ["pnpm", "--filter", "@groombook/db", "seed"]
|
|||||||
|
|
||||||
# Reset stage — drops all tables, re-runs migrations, and re-seeds
|
# Reset stage — drops all tables, re-runs migrations, and re-seeds
|
||||||
FROM builder AS reset
|
FROM builder AS reset
|
||||||
|
RUN corepack enable && corepack prepare pnpm@9.15.4 --activate
|
||||||
CMD ["pnpm", "--filter", "@groombook/db", "reset"]
|
CMD ["pnpm", "--filter", "@groombook/db", "reset"]
|
||||||
|
|||||||
@@ -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.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.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
|
### 4.4 Appointment Scheduling
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|
|||||||
@@ -178,6 +178,9 @@ vi.mock("../db/index.js", () => {
|
|||||||
const staff = new Proxy({ _name: "staff" }, { get: (t, p) => p === "_name" ? "staff" : {} });
|
const staff = new Proxy({ _name: "staff" }, { get: (t, p) => p === "_name" ? "staff" : {} });
|
||||||
const services = new Proxy({ _name: "services" }, { get: (t, p) => p === "_name" ? "services" : {} });
|
const services = new Proxy({ _name: "services" }, { get: (t, p) => p === "_name" ? "services" : {} });
|
||||||
|
|
||||||
|
// Tracks { [tableName]: { [alias]: SQLExpression } } for the current select() call
|
||||||
|
let selectedColumns: Record<string, Record<string, unknown>> = {};
|
||||||
|
|
||||||
function makeChainable(rows: unknown[]) {
|
function makeChainable(rows: unknown[]) {
|
||||||
const arr = rows as unknown[];
|
const arr = rows as unknown[];
|
||||||
return new Proxy(arr, {
|
return new Proxy(arr, {
|
||||||
@@ -188,25 +191,67 @@ vi.mock("../db/index.js", () => {
|
|||||||
if (prop === Symbol.iterator) {
|
if (prop === Symbol.iterator) {
|
||||||
return function* () { for (const v of target) yield v; };
|
return function* () { for (const v of target) yield v; };
|
||||||
}
|
}
|
||||||
|
if (prop === Symbol.asyncIterator) {
|
||||||
|
return async function* () { for (const v of target) yield v; };
|
||||||
|
}
|
||||||
// @ts-expect-error proxy
|
// @ts-expect-error proxy
|
||||||
return target[prop];
|
return target[prop];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sql mock: returns an object with .as() so drizzle's select() can alias it
|
||||||
|
function sqlMock(_strings: TemplateStringsArray, ..._params: unknown[]) {
|
||||||
|
const queryString = _strings[0];
|
||||||
|
const asFn = (alias: string) => ({
|
||||||
|
sql: { queryChunks: [_strings[0]] },
|
||||||
|
fieldAlias: alias,
|
||||||
|
getSQL() { return this.sql; },
|
||||||
|
});
|
||||||
|
return { queryChunks: [queryString], as: asFn };
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getDb: () => ({
|
getDb: () => ({
|
||||||
select: () => ({
|
select: (cols?: Record<string, unknown>) => {
|
||||||
from: (table: unknown) => {
|
selectedColumns = {};
|
||||||
const name = (table as { _name?: string })._name;
|
if (cols) {
|
||||||
if (name === "pets") return makeChainable(mock.pets);
|
// Inspect cols to find sql-aliased expressions and their aliases
|
||||||
if (name === "appointments") return makeChainable(mock.appointments);
|
for (const [alias, expr] of Object.entries(cols)) {
|
||||||
if (name === "groomingVisitLogs") return makeChainable(mock.groomingLogs);
|
if (expr && typeof expr === "object" && "as" in expr && typeof (expr as Record<string, unknown>).as === "function") {
|
||||||
if (name === "staff") return makeChainable(mock.staffMembers);
|
const aliased = (expr as { as: (a: string) => { fieldAlias: string; sql: unknown } }).as(alias);
|
||||||
if (name === "services") return makeChainable(mock.services);
|
// Detect count(*) queries
|
||||||
return makeChainable([]);
|
if (typeof aliased.sql === "object" && aliased.sql !== null && "queryChunks" in (aliased.sql as Record<string, unknown>) && String((aliased.sql as { queryChunks?: unknown[] }).queryChunks).includes("count")) {
|
||||||
},
|
// Store count query intent — we'll resolve it in from()
|
||||||
}),
|
if (!selectedColumns["appointments"]) selectedColumns["appointments"] = {};
|
||||||
|
selectedColumns["appointments"][alias] = { _isCountQuery: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
from: (table: unknown) => {
|
||||||
|
const name = (table as { _name?: string })._name;
|
||||||
|
const tableCols = selectedColumns[name] || {};
|
||||||
|
// If this table has a count query, return computed count result
|
||||||
|
const countQueryEntry = Object.entries(tableCols).find(([, v]) =>
|
||||||
|
typeof v === "object" && v !== null && "_isCountQuery" in v
|
||||||
|
);
|
||||||
|
if (countQueryEntry) {
|
||||||
|
const [countAlias] = countQueryEntry;
|
||||||
|
const count = (name === "appointments" ? mock.appointments : [])
|
||||||
|
.filter((row: Record<string, unknown>) => row.status === "completed").length;
|
||||||
|
return makeChainable([{ [countAlias]: count }]);
|
||||||
|
}
|
||||||
|
if (name === "pets") return makeChainable(mock.pets);
|
||||||
|
if (name === "appointments") return makeChainable(mock.appointments);
|
||||||
|
if (name === "groomingVisitLogs") return makeChainable(mock.groomingLogs);
|
||||||
|
if (name === "staff") return makeChainable(mock.staffMembers);
|
||||||
|
if (name === "services") return makeChainable(mock.services);
|
||||||
|
return makeChainable([]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
insert: () => ({ values: () => ({ returning: () => [{}] }) }),
|
insert: () => ({ values: () => ({ returning: () => [{}] }) }),
|
||||||
update: () => ({ set: () => ({ where: () => ({ returning: () => [{}] }) }) }),
|
update: () => ({ set: () => ({ where: () => ({ returning: () => [{}] }) }) }),
|
||||||
delete: () => ({ where: () => ({ returning: () => [{}] }) }),
|
delete: () => ({ where: () => ({ returning: () => [{}] }) }),
|
||||||
@@ -222,7 +267,7 @@ vi.mock("../db/index.js", () => {
|
|||||||
exists: vi.fn(() => true),
|
exists: vi.fn(() => true),
|
||||||
gte: vi.fn((a: unknown, b: unknown) => ({ col: a, val: b })),
|
gte: vi.fn((a: unknown, b: unknown) => ({ col: a, val: b })),
|
||||||
or: vi.fn((a: unknown, b: unknown) => [a, b]),
|
or: vi.fn((a: unknown, b: unknown) => [a, b]),
|
||||||
sql: vi.fn((str: string) => str),
|
sql: sqlMock,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+73
-7
@@ -20,6 +20,7 @@ import postgres from "postgres";
|
|||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import * as schema from "./schema.js";
|
import * as schema from "./schema.js";
|
||||||
|
import type { MedicalAlert, MedicalAlertSeverity } from "./schema.js";
|
||||||
|
|
||||||
// ── Seed profile configuration ─────────────────────────────────────────────
|
// ── Seed profile configuration ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -252,6 +253,38 @@ const appointmentNotes = [
|
|||||||
"Client running late, pushed start by 15min",
|
"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 = [
|
const visitLogNotes = [
|
||||||
null, null,
|
null, null,
|
||||||
"Coat in great condition",
|
"Coat in great condition",
|
||||||
@@ -872,6 +905,11 @@ async function seed() {
|
|||||||
cutStyle: pick(cutStyles),
|
cutStyle: pick(cutStyles),
|
||||||
shampooPreference: pick(shampoos),
|
shampooPreference: pick(shampoos),
|
||||||
specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null,
|
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: {},
|
customFields: {},
|
||||||
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
||||||
});
|
});
|
||||||
@@ -907,6 +945,11 @@ async function seed() {
|
|||||||
cutStyle: pet.cutStyle,
|
cutStyle: pet.cutStyle,
|
||||||
shampooPreference: pet.shampooPreference,
|
shampooPreference: pet.shampooPreference,
|
||||||
specialCareNotes: pet.specialCareNotes,
|
specialCareNotes: pet.specialCareNotes,
|
||||||
|
coatType: pet.coatType,
|
||||||
|
temperamentScore: pet.temperamentScore,
|
||||||
|
temperamentFlags: pet.temperamentFlags,
|
||||||
|
medicalAlerts: pet.medicalAlerts,
|
||||||
|
preferredCuts: pet.preferredCuts,
|
||||||
customFields: pet.customFields,
|
customFields: pet.customFields,
|
||||||
image: pet.image,
|
image: pet.image,
|
||||||
},
|
},
|
||||||
@@ -929,13 +972,18 @@ async function seed() {
|
|||||||
petId: string;
|
petId: string;
|
||||||
petName: string;
|
petName: string;
|
||||||
petBreed: string;
|
petBreed: string;
|
||||||
|
petCoatType: string;
|
||||||
|
petTemperamentScore: number;
|
||||||
|
petTemperamentFlags: string[];
|
||||||
|
petMedicalAlerts: MedicalAlert[];
|
||||||
|
petPreferredCuts: string[];
|
||||||
}
|
}
|
||||||
const uatClients: UatClient[] = [
|
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 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" },
|
{ 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" },
|
{ 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" },
|
{ 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" },
|
{ 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) {
|
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 })
|
.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 } });
|
.onConflictDoUpdate({ target: schema.clients.id, set: { name: uc.name, email: uc.email, phone: uc.phone, address: uc.address } });
|
||||||
await db.insert(schema.pets)
|
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) })
|
.values({
|
||||||
.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) } });
|
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
|
// Create one completed appointment for this client
|
||||||
const apptId = uuid();
|
const apptId = uuid();
|
||||||
const svcIdx = 0;
|
const svcIdx = 0;
|
||||||
|
|||||||
+114
-2
@@ -20,6 +20,7 @@ import postgres from "postgres";
|
|||||||
import { drizzle } from "drizzle-orm/postgres-js";
|
import { drizzle } from "drizzle-orm/postgres-js";
|
||||||
import { eq, and, sql } from "drizzle-orm";
|
import { eq, and, sql } from "drizzle-orm";
|
||||||
import * as schema from "./schema.js";
|
import * as schema from "./schema.js";
|
||||||
|
import type { MedicalAlert } from "@groombook/types";
|
||||||
|
|
||||||
// ── Seed profile configuration ─────────────────────────────────────────────
|
// ── Seed profile configuration ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -243,6 +244,55 @@ const groomingNotes = [
|
|||||||
"Previous clipper burn — be gentle on belly",
|
"Previous clipper burn — be gentle on belly",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// ── Extended pet profile pools ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const temperamentFlagPool: string[] = [
|
||||||
|
"friendly",
|
||||||
|
"anxious-with-strangers",
|
||||||
|
"good-with-kids",
|
||||||
|
"leash-reactive",
|
||||||
|
"vocal",
|
||||||
|
"high-energy",
|
||||||
|
"calm-on-table",
|
||||||
|
"treat-motivated",
|
||||||
|
];
|
||||||
|
|
||||||
|
const medicalAlertPool: MedicalAlert[] = [
|
||||||
|
{ id: "", type: "allergies", description: "Seasonal allergies — monitor skin", severity: "low" },
|
||||||
|
{ id: "", type: "allergies", description: "Chicken allergy — avoid poultry-based treats", severity: "high" },
|
||||||
|
{ id: "", type: "joint", description: "Hip dysplasia — handle with care", severity: "medium" },
|
||||||
|
{ id: "", type: "joint", description: "Arthritis — anti-inflammatory medication on file", severity: "medium" },
|
||||||
|
{ id: "", type: "dental", description: "Dental disease — extractions in history", severity: "medium" },
|
||||||
|
{ id: "", type: "dental", description: "Baby teeth retained — vet monitor", severity: "low" },
|
||||||
|
{ id: "", type: "heart", description: "Heart murmur grade II — avoid stress", severity: "high" },
|
||||||
|
{ id: "", type: "heart", description: "Murmur cleared by vet last year", severity: "low" },
|
||||||
|
{ id: "", type: "other", description: "Eye ulcer history — be careful around face", severity: "medium" },
|
||||||
|
{ id: "", type: "other", description: "Seizure history — avoid flashing lights", severity: "high" },
|
||||||
|
{ id: "", type: "other", description: "Luxating patella — short walks only", severity: "medium" },
|
||||||
|
{ id: "", type: "other", description: "Ear infections — dry thoroughly after bath", severity: "low" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const preferredCutPool: string[] = [
|
||||||
|
"Puppy Cut",
|
||||||
|
"Teddy Bear Cut",
|
||||||
|
"Lion Cut",
|
||||||
|
"Breed Standard",
|
||||||
|
"Summer Shave",
|
||||||
|
"Kennel Cut",
|
||||||
|
"Lamb Cut",
|
||||||
|
"Continental Clip",
|
||||||
|
"Sporting Clip",
|
||||||
|
"Sanitary Trim",
|
||||||
|
"Face & Feet Trim",
|
||||||
|
"Full Groom",
|
||||||
|
];
|
||||||
|
|
||||||
|
type CoatType = (typeof schema.coatTypeEnum.enumValues)[number];
|
||||||
|
type PetSizeCategory = (typeof schema.petSizeCategoryEnum.enumValues)[number];
|
||||||
|
|
||||||
|
const coatTypePool: CoatType[] = ["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"];
|
||||||
|
const petSizeCategoryPool: PetSizeCategory[] = ["small", "medium", "large", "extra_large"];
|
||||||
|
|
||||||
const appointmentNotes = [
|
const appointmentNotes = [
|
||||||
null, null, null, null,
|
null, null, null, null,
|
||||||
"Client requested extra brushing",
|
"Client requested extra brushing",
|
||||||
@@ -853,6 +903,18 @@ async function seed() {
|
|||||||
specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null,
|
specialCareNotes: rand() < 0.1 ? "Vet clearance required before grooming" : null,
|
||||||
customFields: {},
|
customFields: {},
|
||||||
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
image: petIndex < 250 ? pick(puggleImages) : pick(demoPetImages),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
});
|
});
|
||||||
|
|
||||||
petRecords.push({ id: petId, clientId });
|
petRecords.push({ id: petId, clientId });
|
||||||
@@ -888,6 +950,12 @@ async function seed() {
|
|||||||
specialCareNotes: pet.specialCareNotes,
|
specialCareNotes: pet.specialCareNotes,
|
||||||
customFields: pet.customFields,
|
customFields: pet.customFields,
|
||||||
image: pet.image,
|
image: pet.image,
|
||||||
|
temperamentScore: pet.temperamentScore,
|
||||||
|
temperamentFlags: pet.temperamentFlags,
|
||||||
|
medicalAlerts: pet.medicalAlerts,
|
||||||
|
preferredCuts: pet.preferredCuts,
|
||||||
|
coatType: pet.coatType,
|
||||||
|
petSizeCategory: pet.petSizeCategory,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -922,8 +990,52 @@ async function seed() {
|
|||||||
.values({ id: uc.id, name: uc.name, email: uc.email, phone: uc.phone, address: uc.address })
|
.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 } });
|
.onConflictDoUpdate({ target: schema.clients.id, set: { name: uc.name, email: uc.email, phone: uc.phone, address: uc.address } });
|
||||||
await db.insert(schema.pets)
|
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) })
|
.values({
|
||||||
.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) } });
|
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),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
|
})
|
||||||
|
.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),
|
||||||
|
temperamentScore: randInt(1, 5),
|
||||||
|
temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)),
|
||||||
|
medicalAlerts: (() => {
|
||||||
|
if (rand() < 0.3) {
|
||||||
|
const count = rand() < 0.7 ? 1 : 2;
|
||||||
|
return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})(),
|
||||||
|
preferredCuts: pickN(preferredCutPool, randInt(1, 2)),
|
||||||
|
coatType: pick(coatTypePool),
|
||||||
|
petSizeCategory: pick(petSizeCategoryPool),
|
||||||
|
},
|
||||||
|
});
|
||||||
// Create one completed appointment for this client
|
// Create one completed appointment for this client
|
||||||
const apptId = uuid();
|
const apptId = uuid();
|
||||||
const svcIdx = 0;
|
const svcIdx = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user