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 <noreply@paperclip.ing>
This commit is contained in:
Barkley Trimsworth
2026-03-31 18:38:33 +00:00
committed by Barkley Trimsworth
parent 2fd86d0636
commit 3c5394abef
+20 -16
View File
@@ -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,