From 3c5394abeff53b0915d754feb90b92f7024451af Mon Sep 17 00:00:00 2001 From: Barkley Trimsworth Date: Tue, 31 Mar 2026 18:38:33 +0000 Subject: [PATCH 1/2] fix(db): use deterministic service IDs and add deduplication step Replace random uuid() for service IDs with pre-assigned deterministic UUIDs (b0000001-0000-0000-0000-...) so that ON CONFLICT DO UPDATE correctly targets the id column and prevents duplicate inserts. Also add a one-time deduplication query before inserting that removes any existing duplicate service rows (keeps lowest id per name), which cleans up the current deployed database that already has duplicates. Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 2f01a3b..ea97701 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -18,7 +18,7 @@ import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; -import { eq } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import * as schema from "./schema.js"; // ── Deterministic PRNG (Mulberry32) ────────────────────────────────────────── @@ -234,18 +234,18 @@ const productsUsed = [ ]; // ── Service definitions ────────────────────────────────────────────────────── - +// Deterministic service IDs so seed is idempotent (ON CONFLICT targets id, not name). const servicesDef = [ - { name: "Bath & Brush", desc: "Full bath, blow-dry, brush out, and ear cleaning", price: 4500, dur: 45 }, - { name: "Full Groom — Small", desc: "Complete grooming for dogs under 25 lbs", price: 6500, dur: 60 }, - { name: "Full Groom — Medium", desc: "Complete grooming for dogs 25-50 lbs", price: 8000, dur: 75 }, - { name: "Full Groom — Large", desc: "Complete grooming for dogs over 50 lbs", price: 9500, dur: 90 }, - { name: "Nail Trim", desc: "Nail clipping and filing", price: 1500, dur: 15 }, - { name: "Teeth Brushing", desc: "Dental cleaning with enzymatic toothpaste", price: 1000, dur: 10 }, - { name: "De-shedding Treatment", desc: "Specialised de-shedding bath and blowout", price: 5500, dur: 60 }, - { name: "Puppy First Groom", desc: "Gentle introduction to grooming for puppies under 6 months", price: 4000, dur: 30 }, - { name: "Flea & Tick Treatment", desc: "Medicated bath with flea and tick shampoo", price: 5000, dur: 45 }, - { name: "Sanitary Trim", desc: "Hygienic trim of paw pads, face, and sanitary areas", price: 2500, dur: 20 }, + { id: "b0000001-0000-0000-0000-000000000001", name: "Bath & Brush", desc: "Full bath, blow-dry, brush out, and ear cleaning", price: 4500, dur: 45 }, + { id: "b0000001-0000-0000-0000-000000000002", name: "Full Groom — Small", desc: "Complete grooming for dogs under 25 lbs", price: 6500, dur: 60 }, + { id: "b0000001-0000-0000-0000-000000000003", name: "Full Groom — Medium", desc: "Complete grooming for dogs 25-50 lbs", price: 8000, dur: 75 }, + { id: "b0000001-0000-0000-0000-000000000004", name: "Full Groom — Large", desc: "Complete grooming for dogs over 50 lbs", price: 9500, dur: 90 }, + { id: "b0000001-0000-0000-0000-000000000005", name: "Nail Trim", desc: "Nail clipping and filing", price: 1500, dur: 15 }, + { id: "b0000001-0000-0000-0000-000000000006", name: "Teeth Brushing", desc: "Dental cleaning with enzymatic toothpaste", price: 1000, dur: 10 }, + { id: "b0000001-0000-0000-0000-000000000007", name: "De-shedding Treatment", desc: "Specialised de-shedding bath and blowout", price: 5500, dur: 60 }, + { id: "b0000001-0000-0000-0000-000000000008", name: "Puppy First Groom", desc: "Gentle introduction to grooming for puppies under 6 months", price: 4000, dur: 30 }, + { id: "b0000001-0000-0000-0000-000000000009", name: "Flea & Tick Treatment", desc: "Medicated bath with flea and tick shampoo", price: 5000, dur: 45 }, + { id: "b0000001-0000-0000-0000-00000000000a", name: "Sanitary Trim", desc: "Hygienic trim of paw pads, face, and sanitary areas", price: 2500, dur: 20 }, ]; // ── Known-users-only seed (prod/demo) ─────────────────────────────────────── @@ -424,13 +424,17 @@ async function seed() { console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`); // ── Services ── - const serviceIds: string[] = []; + // Deduplicate existing services (keep lowest id per name) before inserting. + await db.execute(sql` + DELETE FROM services WHERE id NOT IN ( + SELECT MIN(id) FROM services GROUP BY name + ) + `); + for (const s of servicesDef) { - const id = uuid(); - serviceIds.push(id); await db.insert(schema.services) .values({ - id, + id: s.id, name: s.name, description: s.desc, basePriceCents: s.price, From 5dd76aa51f7f66aa64f24b0bb3dae98aa8d59027 Mon Sep 17 00:00:00 2001 From: Barkley Trimsworth Date: Tue, 31 Mar 2026 18:40:51 +0000 Subject: [PATCH 2/2] fix(db): preserve serviceIds array for appointment lookups The serviceIds array is still referenced by later seed code when creating appointments. Restore it (populated from servicesDef) after removing it from the ON CONFLICT path. Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index ea97701..2cb6ffb 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -431,7 +431,9 @@ async function seed() { ) `); + const serviceIds: string[] = []; for (const s of servicesDef) { + serviceIds.push(s.id); await db.insert(schema.services) .values({ id: s.id,