Parameterize seed script with SEED_PROFILE env var
Adds SEED_PROFILE env var accepting 'dev', 'uat', or 'demo' values: - dev: 4 staff (1 manager, 1 receptionist, 2 groomers), 100 clients, 7d/30d appointment window, ~1000 invoices, no UAT clients - uat: 8 staff (1 manager, 1 receptionist, 3 groomers, 3 bathers), 500 clients, 30d/90d window, ~4000 invoices, includes UAT clients - demo: same volume as uat Unset SEED_PROFILE defaults to 'uat' for backwards compatibility. SEED_KNOWN_USERS_ONLY=true path unchanged. All appointment dates computed relative to NOW() at seed time. Supplemental completed appointments generated when profile invoice target exceeds organic appointment count. Closes groombook/groombook#247 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
+182
-51
@@ -21,6 +21,54 @@ import { drizzle } from "drizzle-orm/postgres-js";
|
|||||||
import { eq, sql } from "drizzle-orm";
|
import { eq, sql } from "drizzle-orm";
|
||||||
import * as schema from "./schema.js";
|
import * as schema from "./schema.js";
|
||||||
|
|
||||||
|
// ── Seed profile configuration ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
type SeedProfile = "dev" | "uat" | "demo";
|
||||||
|
|
||||||
|
interface ProfileConfig {
|
||||||
|
staffCount: { manager: number; receptionist: number; groomer: number; bather: number };
|
||||||
|
clientCount: number;
|
||||||
|
appointmentsBackDays: number;
|
||||||
|
appointmentsForwardDays: number;
|
||||||
|
invoiceCount: number;
|
||||||
|
includeUatClients: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profiles: Record<SeedProfile, ProfileConfig> = {
|
||||||
|
dev: {
|
||||||
|
staffCount: { manager: 1, receptionist: 1, groomer: 2, bather: 0 },
|
||||||
|
clientCount: 100,
|
||||||
|
appointmentsBackDays: 7,
|
||||||
|
appointmentsForwardDays: 30,
|
||||||
|
invoiceCount: 1000,
|
||||||
|
includeUatClients: false,
|
||||||
|
},
|
||||||
|
uat: {
|
||||||
|
staffCount: { manager: 1, receptionist: 1, groomer: 3, bather: 3 },
|
||||||
|
clientCount: 500,
|
||||||
|
appointmentsBackDays: 30,
|
||||||
|
appointmentsForwardDays: 90,
|
||||||
|
invoiceCount: 4000,
|
||||||
|
includeUatClients: true,
|
||||||
|
},
|
||||||
|
demo: {
|
||||||
|
staffCount: { manager: 1, receptionist: 1, groomer: 3, bather: 3 },
|
||||||
|
clientCount: 500,
|
||||||
|
appointmentsBackDays: 30,
|
||||||
|
appointmentsForwardDays: 90,
|
||||||
|
invoiceCount: 4000,
|
||||||
|
includeUatClients: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function getProfile(): SeedProfile {
|
||||||
|
const raw = process.env.SEED_PROFILE?.toLowerCase();
|
||||||
|
if (raw === "dev" || raw === "uat" || raw === "demo") {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
return "uat";
|
||||||
|
}
|
||||||
|
|
||||||
// ── Deterministic PRNG (Mulberry32) ──────────────────────────────────────────
|
// ── Deterministic PRNG (Mulberry32) ──────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -415,44 +463,32 @@ async function seed() {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lean prod/demo seed — known users only, no large dataset
|
|
||||||
if (process.env.SEED_KNOWN_USERS_ONLY === "true") {
|
if (process.env.SEED_KNOWN_USERS_ONLY === "true") {
|
||||||
await seedKnownUsers();
|
await seedKnownUsers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const profile = getProfile();
|
||||||
|
const cfg = profiles[profile];
|
||||||
const client = postgres(url, { max: 5 });
|
const client = postgres(url, { max: 5 });
|
||||||
const db = drizzle(client, { schema });
|
const db = drizzle(client, { schema });
|
||||||
|
|
||||||
console.log("Seeding Groom Book database...\n");
|
console.log(`Seeding Groom Book database (profile: ${profile})...\n`);
|
||||||
|
|
||||||
// ── Staff ──
|
// ── Staff ──
|
||||||
// Deterministic staff IDs so they can be referenced in scripts/tests
|
const managerStaff = Array.from({ length: cfg.staffCount.manager }, (_, i) =>
|
||||||
const managerStaff = [
|
({ id: uuid(), name: `Manager ${i + 1}`, email: `manager${i + 1}@groombook.dev`, role: "manager" as const, isSuperUser: false })
|
||||||
{ id: uuid(), name: "Jordan Lee", email: "jordan@groombook.dev", role: "manager" as const, isSuperUser: false },
|
);
|
||||||
];
|
const receptionistStaff = Array.from({ length: cfg.staffCount.receptionist }, (_, i) =>
|
||||||
|
({ id: uuid(), name: `Receptionist ${i + 1}`, email: `receptionist${i + 1}@groombook.dev`, role: "receptionist" as const, isSuperUser: false })
|
||||||
|
);
|
||||||
|
const groomers = Array.from({ length: cfg.staffCount.groomer }, (_, i) =>
|
||||||
|
({ id: uuid(), name: `Groomer ${i + 1}`, email: `groomer${i + 1}@groombook.dev`, role: "groomer" as const, isSuperUser: false })
|
||||||
|
);
|
||||||
|
const bathers = Array.from({ length: cfg.staffCount.bather }, (_, i) =>
|
||||||
|
({ id: uuid(), name: `Bather ${i + 1}`, email: `bather${i + 1}@groombook.dev`, role: "groomer" as const, isSuperUser: false })
|
||||||
|
);
|
||||||
|
|
||||||
const receptionistStaff = [
|
|
||||||
{ id: uuid(), name: "Sam Rivera", email: "sam@groombook.dev", role: "receptionist" as const, isSuperUser: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
const groomers = [
|
|
||||||
{ id: uuid(), name: "Sarah Mitchell", email: "sarah@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
{ id: uuid(), name: "James Park", email: "james@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
{ id: uuid(), name: "Maria Gonzalez", email: "maria@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Bathers are groomers by role but serve as the secondary staff (bather) on appointments
|
|
||||||
const bathers = [
|
|
||||||
{ id: uuid(), name: "Tyler Johnson", email: "tyler@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
{ id: uuid(), name: "Ashley Chen", email: "ashley@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
{ id: uuid(), name: "Devon Williams", email: "devon@groombook.dev", role: "groomer" as const, isSuperUser: false },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Truncate downstream tables before staff upsert — clears stale impersonation
|
|
||||||
// sessions from prior seed runs so the FK constraint on staff_id is never
|
|
||||||
// violated when ON CONFLICT DO UPDATE touches staff rows that still have
|
|
||||||
// impersonation_sessions references.
|
|
||||||
await db.execute(sql`TRUNCATE impersonation_sessions, impersonation_audit_logs, appointments, invoices, invoice_line_items, invoice_tip_splits, grooming_visit_logs CASCADE`);
|
await db.execute(sql`TRUNCATE impersonation_sessions, impersonation_audit_logs, appointments, invoices, invoice_line_items, invoice_tip_splits, grooming_visit_logs CASCADE`);
|
||||||
|
|
||||||
const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers];
|
const allStaff = [...managerStaff, ...receptionistStaff, ...groomers, ...bathers];
|
||||||
@@ -471,7 +507,10 @@ async function seed() {
|
|||||||
set: { id: s.id, name: s.name, role: s.role, isSuperUser: s.isSuperUser, active: true },
|
set: { id: s.id, name: s.name, role: s.role, isSuperUser: s.isSuperUser, active: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(`✓ Created ${allStaff.length} staff (1 manager, 1 receptionist, 3 groomers, 3 bathers)`);
|
const staffLabel = cfg.staffCount.bather > 0
|
||||||
|
? `${allStaff.length} staff (${cfg.staffCount.manager} manager, ${cfg.staffCount.receptionist} receptionist, ${cfg.staffCount.groomer} groomers, ${cfg.staffCount.bather} bathers)`
|
||||||
|
: `${allStaff.length} staff (${cfg.staffCount.manager} manager, ${cfg.staffCount.receptionist} receptionist, ${cfg.staffCount.groomer} groomers)`;
|
||||||
|
console.log(`✓ Created ${staffLabel}`);
|
||||||
|
|
||||||
// ── SEED_ADMIN_EMAIL admin ──
|
// ── SEED_ADMIN_EMAIL admin ──
|
||||||
const adminEmail = process.env.SEED_ADMIN_EMAIL;
|
const adminEmail = process.env.SEED_ADMIN_EMAIL;
|
||||||
@@ -519,8 +558,10 @@ async function seed() {
|
|||||||
|
|
||||||
// ── Clients & Pets ──
|
// ── Clients & Pets ──
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const oneYearAgo = new Date(now);
|
const appointmentsBackDate = new Date(now);
|
||||||
oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);
|
appointmentsBackDate.setDate(appointmentsBackDate.getDate() - cfg.appointmentsBackDays);
|
||||||
|
const appointmentsForwardDate = new Date(now);
|
||||||
|
appointmentsForwardDate.setDate(appointmentsForwardDate.getDate() + cfg.appointmentsForwardDays);
|
||||||
|
|
||||||
interface ClientRecord { id: string; name: string }
|
interface ClientRecord { id: string; name: string }
|
||||||
interface PetRecord { id: string; clientId: string }
|
interface PetRecord { id: string; clientId: string }
|
||||||
@@ -528,9 +569,8 @@ async function seed() {
|
|||||||
const clientRecords: ClientRecord[] = [];
|
const clientRecords: ClientRecord[] = [];
|
||||||
const petRecords: PetRecord[] = [];
|
const petRecords: PetRecord[] = [];
|
||||||
|
|
||||||
// Batch insert clients and pets
|
|
||||||
const clientBatchSize = 50;
|
const clientBatchSize = 50;
|
||||||
for (let batch = 0; batch < 500 / clientBatchSize; batch++) {
|
for (let batch = 0; batch < Math.ceil(cfg.clientCount / clientBatchSize); batch++) {
|
||||||
const clientBatch: (typeof schema.clients.$inferInsert)[] = [];
|
const clientBatch: (typeof schema.clients.$inferInsert)[] = [];
|
||||||
const petBatch: (typeof schema.pets.$inferInsert)[] = [];
|
const petBatch: (typeof schema.pets.$inferInsert)[] = [];
|
||||||
|
|
||||||
@@ -617,22 +657,23 @@ async function seed() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✓ Created 500 clients with ${petRecords.length} pets`);
|
console.log(`✓ Created ${cfg.clientCount} clients with ${petRecords.length} pets`);
|
||||||
|
|
||||||
// ── UAT test clients (guaranteed pending invoices) ─────────────────────────────
|
// ── UAT test clients (guaranteed pending invoices) ─────────────────────────────
|
||||||
// These 5 clients are deterministic and documented in Shedward AGENTS.md so
|
// These 5 clients are deterministic and documented in Shedward AGENTS.md so
|
||||||
// UAT can reliably find billing test data without searching.
|
// UAT can reliably find billing test data without searching.
|
||||||
interface UatClient {
|
if (cfg.includeUatClients) {
|
||||||
id: string;
|
interface UatClient {
|
||||||
name: string;
|
id: string;
|
||||||
email: string;
|
name: string;
|
||||||
phone: string;
|
email: string;
|
||||||
address: string;
|
phone: string;
|
||||||
petId: string;
|
address: string;
|
||||||
petName: string;
|
petId: string;
|
||||||
petBreed: string;
|
petName: string;
|
||||||
}
|
petBreed: 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" },
|
||||||
{ 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" },
|
||||||
{ 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" },
|
||||||
@@ -651,12 +692,14 @@ async function seed() {
|
|||||||
const apptId = uuid();
|
const apptId = uuid();
|
||||||
const svcIdx = 0;
|
const svcIdx = 0;
|
||||||
const svc = servicesDef[svcIdx]!;
|
const svc = servicesDef[svcIdx]!;
|
||||||
const completedTime = randDate(oneYearAgo, now);
|
const completedTime = randDate(appointmentsBackDate, now);
|
||||||
completedTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
completedTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
||||||
const endTime = new Date(completedTime.getTime() + svc.dur * 60 * 1000);
|
const endTime = new Date(completedTime.getTime() + svc.dur * 60 * 1000);
|
||||||
|
const uatGroomer = groomers[0]!;
|
||||||
|
const uatBather = bathers.length > 0 ? bathers[0]! : uatGroomer;
|
||||||
await db.insert(schema.appointments).values({
|
await db.insert(schema.appointments).values({
|
||||||
id: apptId, clientId: uc.id, petId: uc.petId, serviceId: serviceIds[svcIdx]!, staffId: groomers[0]!.id,
|
id: apptId, clientId: uc.id, petId: uc.petId, serviceId: serviceIds[svcIdx]!, staffId: uatGroomer.id,
|
||||||
batherStaffId: bathers[0]!.id, status: "completed" as const, startTime: completedTime, endTime, notes: null, priceCents: svc.price,
|
batherStaffId: uatBather.id, status: "completed" as const, startTime: completedTime, endTime, notes: null, priceCents: svc.price,
|
||||||
});
|
});
|
||||||
// Create a PENDING invoice for that appointment
|
// Create a PENDING invoice for that appointment
|
||||||
const invoiceId = uuid();
|
const invoiceId = uuid();
|
||||||
@@ -674,8 +717,9 @@ async function seed() {
|
|||||||
id: uuid(), petId: uc.petId, appointmentId: apptId, staffId: groomers[0]!.id,
|
id: uuid(), petId: uc.petId, appointmentId: apptId, staffId: groomers[0]!.id,
|
||||||
cutStyle: null, productsUsed: null, notes: null, groomedAt: endTime,
|
cutStyle: null, productsUsed: null, notes: null, groomedAt: endTime,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
console.log(`✓ Created ${uatClients.length} UAT test clients with guaranteed pending invoices`);
|
||||||
}
|
}
|
||||||
console.log(`✓ Created ${uatClients.length} UAT test clients with guaranteed pending invoices`);
|
|
||||||
|
|
||||||
// ── Appointments, Invoices, Visit Logs ──
|
// ── Appointments, Invoices, Visit Logs ──
|
||||||
// Generate ~5 appointments per client on average = ~2500 total
|
// Generate ~5 appointments per client on average = ~2500 total
|
||||||
@@ -742,12 +786,12 @@ async function seed() {
|
|||||||
const bather = rand() < 0.6 ? pick(bathers) : null;
|
const bather = rand() < 0.6 ? pick(bathers) : null;
|
||||||
const status = pick(statuses);
|
const status = pick(statuses);
|
||||||
|
|
||||||
// Schedule within the past year, or next 2 weeks for upcoming
|
// Schedule within the configured appointment window
|
||||||
let startTime: Date;
|
let startTime: Date;
|
||||||
if (status === "scheduled" || status === "confirmed") {
|
if (status === "scheduled" || status === "confirmed") {
|
||||||
startTime = randDate(now, new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000));
|
startTime = randDate(now, appointmentsForwardDate);
|
||||||
} else {
|
} else {
|
||||||
startTime = randDate(oneYearAgo, now);
|
startTime = randDate(appointmentsBackDate, now);
|
||||||
}
|
}
|
||||||
// Snap to business hours (8am - 5pm)
|
// Snap to business hours (8am - 5pm)
|
||||||
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
||||||
@@ -851,6 +895,93 @@ async function seed() {
|
|||||||
|
|
||||||
console.log(`✓ Created ${appointmentCount} appointments`);
|
console.log(`✓ Created ${appointmentCount} appointments`);
|
||||||
console.log(`✓ Created ${invoiceCount} invoices with line items and tip splits`);
|
console.log(`✓ Created ${invoiceCount} invoices with line items and tip splits`);
|
||||||
|
|
||||||
|
// ── Enforce target invoice count ───────────────────────────────────────────
|
||||||
|
// If current invoice count is below target (due to profile having fewer
|
||||||
|
// clients/appointments than the target ratio), generate supplemental
|
||||||
|
// completed appointments for existing clients to fill the gap.
|
||||||
|
if (invoiceCount < cfg.invoiceCount) {
|
||||||
|
const additionalNeeded = cfg.invoiceCount - invoiceCount;
|
||||||
|
console.log(` → Generating ${additionalNeeded} supplemental completed appointments to meet profile target...`);
|
||||||
|
|
||||||
|
const existingClientIds = clientRecords.map(c => c.id);
|
||||||
|
const apptsToGenerate = Math.min(additionalNeeded, existingClientIds.length * 20);
|
||||||
|
let supplementalCount = 0;
|
||||||
|
let supplementalInvoices = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < apptsToGenerate && supplementalInvoices < additionalNeeded; i++) {
|
||||||
|
const clientId = pick(existingClientIds);
|
||||||
|
const pets = petsByClient.get(clientId) ?? [];
|
||||||
|
if (pets.length === 0) continue;
|
||||||
|
|
||||||
|
const petId = pick(pets);
|
||||||
|
const serviceIdx = randInt(0, serviceIds.length - 1);
|
||||||
|
const serviceId = serviceIds[serviceIdx]!;
|
||||||
|
const svc = servicesDef[serviceIdx]!;
|
||||||
|
const groomer = pick(groomers);
|
||||||
|
const bather = bathers.length > 0 && rand() < 0.6 ? pick(bathers) : null;
|
||||||
|
|
||||||
|
let startTime = randDate(appointmentsBackDate, now);
|
||||||
|
startTime.setHours(randInt(8, 16), pick([0, 15, 30, 45]), 0, 0);
|
||||||
|
const endTime = new Date(startTime.getTime() + svc.dur * 60 * 1000);
|
||||||
|
const effectivePrice = svc.price;
|
||||||
|
|
||||||
|
const apptId = uuid();
|
||||||
|
apptBatch.push({
|
||||||
|
id: apptId, clientId, petId, serviceId,
|
||||||
|
staffId: groomer.id, batherStaffId: bather?.id ?? null,
|
||||||
|
status: "completed", startTime, endTime, notes: null, priceCents: null,
|
||||||
|
});
|
||||||
|
appointmentCount++;
|
||||||
|
supplementalCount++;
|
||||||
|
|
||||||
|
const invoiceId = uuid();
|
||||||
|
const tipCents = rand() < 0.7 ? randInt(200, 3000) : 0;
|
||||||
|
const taxCents = Math.round(effectivePrice * 0.08);
|
||||||
|
const totalCents = effectivePrice + taxCents + tipCents;
|
||||||
|
const paidAt = new Date(endTime.getTime() + randInt(5, 30) * 60 * 1000);
|
||||||
|
|
||||||
|
invoiceBatch.push({
|
||||||
|
id: invoiceId, appointmentId: apptId, clientId,
|
||||||
|
subtotalCents: effectivePrice, taxCents, tipCents, totalCents,
|
||||||
|
status: "paid" as const,
|
||||||
|
paymentMethod: pick(["cash", "card", "card", "card", "check"]) as "cash" | "card" | "check",
|
||||||
|
paidAt, notes: null,
|
||||||
|
});
|
||||||
|
lineItemBatch.push({
|
||||||
|
id: uuid(), invoiceId, description: svc.name, quantity: 1,
|
||||||
|
unitPriceCents: effectivePrice, totalCents: effectivePrice,
|
||||||
|
});
|
||||||
|
if (tipCents > 0) {
|
||||||
|
if (bather) {
|
||||||
|
const groomerShare = Math.round(tipCents * 0.6);
|
||||||
|
const batherShare = tipCents - groomerShare;
|
||||||
|
tipSplitBatch.push(
|
||||||
|
{ id: uuid(), invoiceId, staffId: groomer.id, staffName: groomer.name, sharePct: "60.00", shareCents: groomerShare },
|
||||||
|
{ id: uuid(), invoiceId, staffId: bather.id, staffName: bather.name, sharePct: "40.00", shareCents: batherShare },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tipSplitBatch.push({ id: uuid(), invoiceId, staffId: groomer.id, staffName: groomer.name, sharePct: "100.00", shareCents: tipCents });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visitLogBatch.push({
|
||||||
|
id: uuid(), petId, appointmentId: apptId, staffId: groomer.id,
|
||||||
|
cutStyle: pick(cutStyles), productsUsed: pick(productsUsed),
|
||||||
|
notes: pick(visitLogNotes), groomedAt: endTime,
|
||||||
|
});
|
||||||
|
invoiceCount++;
|
||||||
|
supplementalInvoices++;
|
||||||
|
visitLogCount++;
|
||||||
|
|
||||||
|
if (apptBatch.length >= apptBatchSize) {
|
||||||
|
await flushBatches();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await flushBatches();
|
||||||
|
console.log(` → Added ${supplementalCount} supplemental appointments (${supplementalInvoices} invoices)`);
|
||||||
|
console.log(`✓ Created ${invoiceCount} invoices with line items and tip splits`);
|
||||||
|
}
|
||||||
console.log(`✓ Created ${visitLogCount} grooming visit logs`);
|
console.log(`✓ Created ${visitLogCount} grooming visit logs`);
|
||||||
console.log("\nSeed complete!");
|
console.log("\nSeed complete!");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user