From 7033cd1d5c1fc25fefa2fb54e76ae98cac1f9f3e Mon Sep 17 00:00:00 2001 From: "groombook-engineer[bot]" <3141748+groombook-engineer[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:23:11 +0000 Subject: [PATCH] fix(db): remove dedup DELETE and use ON CONFLICT (name) for idempotent services seed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dedup DELETE was causing two problems: 1. FK violation (GRO-365) — deleting services referenced by appointments 2. Duplicate services (GRO-301) — MIN(id) per name could delete the wrong row, causing ON CONFLICT (id) to create duplicates on re-run Fix: - Remove the dedup DELETE entirely - Keep TRUNCATE of downstream tables (appointments, invoices, etc.) to clear stale data from prior runs - Change ON CONFLICT target from `id` to `name` with a unique constraint on name column — deterministic IDs in servicesDef ensure idempotency Co-Authored-By: Paperclip --- packages/db/src/schema.ts | 2 +- packages/db/src/seed.ts | 17 +++++++---------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/packages/db/src/schema.ts b/packages/db/src/schema.ts index 3c75c9f..285caec 100644 --- a/packages/db/src/schema.ts +++ b/packages/db/src/schema.ts @@ -141,7 +141,7 @@ export const pets = pgTable("pets", { export const services = pgTable("services", { id: uuid("id").primaryKey().defaultRandom(), - name: text("name").notNull(), + name: text("name").notNull().unique(), description: text("description"), basePriceCents: integer("base_price_cents").notNull(), durationMinutes: integer("duration_minutes").notNull(), diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 3c625cd..641105a 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -423,17 +423,14 @@ async function seed() { } console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`); - // Truncate downstream tables before services dedup to avoid FK violation + // Truncate downstream tables before services upsert — clears stale appointments + // from prior seed runs so the FK constraint on service_id is never violated await db.execute(sql`TRUNCATE appointments, invoices, invoice_line_items, invoice_tip_splits, grooming_visit_logs CASCADE`); // ── Services ── - // Deduplicate existing services (keep lowest id per name) before inserting. - await db.execute(sql` - DELETE FROM services WHERE id NOT IN ( - SELECT (MIN(id::text))::uuid FROM services GROUP BY name - ) - `); - + // Upsert services using name as unique key. With deterministic IDs in + // servicesDef and TRUNCATE clearing downstream tables first, this is + // idempotent: first run inserts, subsequent runs update existing rows. const serviceIds: string[] = []; for (const s of servicesDef) { serviceIds.push(s.id); @@ -447,8 +444,8 @@ async function seed() { active: true, }) .onConflictDoUpdate({ - target: schema.services.id, - set: { name: s.name, description: s.desc, basePriceCents: s.price, durationMinutes: s.dur, active: true }, + target: schema.services.name, + set: { description: s.desc, basePriceCents: s.price, durationMinutes: s.dur, active: true }, }); } console.log(`✓ Created ${servicesDef.length} services`);