/** * Music store seed — populates the dev database with music-store-specific data. * Run: bun run src/db/seeds/music-store-seed.ts * * Prerequisites: dev-seed must have been run first (company, accounts, RBAC already exist). * This adds music-specific repair templates, tickets, and batch data. */ import postgres from 'postgres' const DB_URL = process.env.DATABASE_URL ?? 'postgresql://lunarfront:lunarfront@localhost:5432/lunarfront' const COMPANY_ID = 'a0000000-0000-4000-8000-000000000001' const sql = postgres(DB_URL) async function seed() { console.log('Seeding music store data...') // --- Bootstrap: company, location, RBAC, admin user (idempotent) --- const [existingCompany] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}` if (!existingCompany) { await sql`INSERT INTO company (id, name, timezone, phone, email, address) VALUES (${COMPANY_ID}, 'Harmony Music Shop', 'America/Chicago', '555-555-1234', 'info@harmonymusic.com', '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb)` await sql`INSERT INTO location (id, name, tax_rate, service_tax_rate, phone, email, address) VALUES ('a0000000-0000-4000-8000-000000000002', 'Main Store', '0.0825', '0.0825', '555-555-1234', 'info@harmonymusic.com', '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb)` console.log(' Created company and location') } else { await sql`UPDATE company SET name = 'Harmony Music Shop', phone = '555-555-1234', email = 'info@harmonymusic.com', address = '{"street":"123 Main St","city":"San Antonio","state":"TX","zip":"78205"}'::jsonb WHERE id = ${COMPANY_ID}` console.log(' Updated company: Harmony Music Shop') } // RBAC const { SYSTEM_PERMISSIONS, DEFAULT_ROLES } = await import('../seeds/rbac.js') for (const p of SYSTEM_PERMISSIONS) { await sql`INSERT INTO permission (slug, domain, action, description) VALUES (${p.slug}, ${p.domain}, ${p.action}, ${p.description}) ON CONFLICT (slug) DO NOTHING` } const permRows = await sql`SELECT id, slug FROM permission` const permMap = new Map(permRows.map((r: any) => [r.slug, r.id])) for (const roleDef of DEFAULT_ROLES) { const [existingRole] = await sql`SELECT id FROM role WHERE slug = ${roleDef.slug}` if (existingRole) continue const [role] = await sql`INSERT INTO role (name, slug, description, is_system) VALUES (${roleDef.name}, ${roleDef.slug}, ${roleDef.description}, true) RETURNING id` for (const permSlug of roleDef.permissions) { const permId = permMap.get(permSlug) if (permId) await sql`INSERT INTO role_permission (role_id, permission_id) VALUES (${role.id}, ${permId}) ON CONFLICT DO NOTHING` } } console.log(' RBAC seeded') // Admin user with POS PIN const [existingAdmin] = await sql`SELECT id FROM "user" WHERE email = 'admin@harmonymusic.com'` if (!existingAdmin) { const bcrypt = await import('bcryptjs') const hashedPw = await bcrypt.hash('admin1234', 10) const pinHash = await bcrypt.hash('1234', 10) const [user] = await sql`INSERT INTO "user" (email, password_hash, first_name, last_name, role, employee_number, pin_hash) VALUES ('admin@harmonymusic.com', ${hashedPw}, 'Admin', 'User', 'admin', '1001', ${pinHash}) RETURNING id` const [adminRole] = await sql`SELECT id FROM role WHERE slug = 'admin' LIMIT 1` if (adminRole) { await sql`INSERT INTO user_role_assignment (user_id, role_id) VALUES (${user.id}, ${adminRole.id}) ON CONFLICT DO NOTHING` } console.log(' Created admin: admin@harmonymusic.com / admin1234 (POS: 10011234)') } // Default modules const modules = [ { slug: 'inventory', name: 'Inventory', description: 'Product catalog and stock tracking', enabled: true }, { slug: 'pos', name: 'Point of Sale', description: 'Sales, drawer, receipts', enabled: true }, { slug: 'repairs', name: 'Repairs', description: 'Repair tickets and service', enabled: true }, { slug: 'lessons', name: 'Lessons', description: 'Scheduling and instruction', enabled: true }, { slug: 'files', name: 'Files', description: 'File storage', enabled: true }, { slug: 'vault', name: 'Vault', description: 'Password manager', enabled: true }, { slug: 'reports', name: 'Reports', description: 'Business reports', enabled: true }, { slug: 'rentals', name: 'Rentals', description: 'Rental agreements', enabled: false }, { slug: 'email', name: 'Email', description: 'Email campaigns', enabled: false }, ] for (const m of modules) { await sql`INSERT INTO module_config (slug, name, description, licensed, enabled) VALUES (${m.slug}, ${m.name}, ${m.description}, true, ${m.enabled}) ON CONFLICT (slug) DO NOTHING` } console.log(' Modules configured') // Default register await sql`INSERT INTO register (location_id, name) SELECT 'a0000000-0000-4000-8000-000000000002', 'Register 1' WHERE NOT EXISTS (SELECT 1 FROM register WHERE name = 'Register 1' AND location_id = 'a0000000-0000-4000-8000-000000000002')` console.log(' Default register created') // --- Accounts & Members --- const accounts = [ { name: 'Smith Family', email: 'smith@example.com', phone: '555-0101', type: 'family' }, { name: 'Johnson Family', email: 'johnson@example.com', phone: '555-0102', type: 'family' }, { name: 'Garcia Workshop', email: 'carlos@studio.com', phone: '555-0103', type: 'business' }, { name: 'Mike Thompson', email: 'mike.t@email.com', phone: '555-0104', type: 'individual' }, { name: 'Emily Chen', email: 'emily.chen@email.com', phone: '555-0105', type: 'individual' }, { name: 'Lincoln High School', email: 'band@lincoln.edu', phone: '555-0200', type: 'business' }, { name: 'Westside Community Orchestra', email: 'info@westsideorch.org', phone: '555-0300', type: 'business' }, { name: 'Rivera Family', email: 'rivera@email.com', phone: '555-0106', type: 'family' }, { name: 'Patricia Williams', email: 'pwilliams@email.com', phone: '555-0107', type: 'individual' }, { name: 'Oak Elementary PTA', email: 'pta@oakelementary.edu', phone: '555-0201', type: 'business' }, ] const acctIds: Record = {} for (const a of accounts) { const [existing] = await sql`SELECT id FROM account WHERE name = ${a.name}` if (existing) { acctIds[a.name] = existing.id; continue } const num = String(Math.floor(100000 + Math.random() * 900000)) const [row] = await sql`INSERT INTO account (name, email, phone, account_number, billing_mode) VALUES (${a.name}, ${a.email}, ${a.phone}, ${num}, 'consolidated') RETURNING id` acctIds[a.name] = row.id } console.log(` Created ${accounts.length} accounts`) const members = [ // Smith Family — parent + 2 kids learning strings { account: 'Smith Family', firstName: 'David', lastName: 'Smith', email: 'david@example.com', phone: '555-0101' }, { account: 'Smith Family', firstName: 'Sarah', lastName: 'Smith', email: 'sarah@example.com' }, { account: 'Smith Family', firstName: 'Tommy', lastName: 'Smith', isMinor: true, dob: '2015-03-12' }, { account: 'Smith Family', firstName: 'Lily', lastName: 'Smith', isMinor: true, dob: '2017-08-25' }, // Johnson Family — parent + kid learning viola { account: 'Johnson Family', firstName: 'Lisa', lastName: 'Johnson', email: 'lisa.j@example.com', phone: '555-0102' }, { account: 'Johnson Family', firstName: 'Jake', lastName: 'Johnson', isMinor: true, dob: '2013-11-05' }, // Individual musicians { account: 'Mike Thompson', firstName: 'Mike', lastName: 'Thompson', email: 'mike.t@email.com', phone: '555-0104' }, { account: 'Emily Chen', firstName: 'Emily', lastName: 'Chen', email: 'emily.chen@email.com', phone: '555-0105' }, { account: 'Garcia Workshop', firstName: 'Carlos', lastName: 'Garcia', email: 'carlos@studio.com', phone: '555-0103' }, { account: 'Patricia Williams', firstName: 'Patricia', lastName: 'Williams', email: 'pwilliams@email.com', phone: '555-0107' }, // Rivera Family — cellist family { account: 'Rivera Family', firstName: 'Maria', lastName: 'Rivera', email: 'rivera@email.com', phone: '555-0106' }, { account: 'Rivera Family', firstName: 'Sofia', lastName: 'Rivera', isMinor: true, dob: '2014-06-18' }, { account: 'Rivera Family', firstName: 'Diego', lastName: 'Rivera', isMinor: true, dob: '2016-01-30' }, // School contacts { account: 'Lincoln High School', firstName: 'Robert', lastName: 'Hayes', email: 'rhayes@lincoln.edu', phone: '555-0200' }, { account: 'Oak Elementary PTA', firstName: 'Jennifer', lastName: 'Park', email: 'jpark@oakelementary.edu', phone: '555-0201' }, // Orchestra contact { account: 'Westside Community Orchestra', firstName: 'Margaret', lastName: 'Foster', email: 'mfoster@westsideorch.org', phone: '555-0300' }, ] const memberMap: Record = {} for (const m of members) { const accountId = acctIds[m.account] if (!accountId) continue const [existing] = await sql`SELECT id FROM member WHERE first_name = ${m.firstName} AND last_name = ${m.lastName} AND account_id = ${accountId}` if (existing) { memberMap[`${m.firstName} ${m.lastName}`] = existing; continue } const num = String(Math.floor(100000 + Math.random() * 900000)) const [row] = await sql` INSERT INTO member (account_id, first_name, last_name, email, phone, member_number, is_minor, date_of_birth) VALUES (${accountId}, ${m.firstName}, ${m.lastName}, ${m.email ?? null}, ${m.phone ?? null}, ${num}, ${m.isMinor ?? false}, ${m.dob ?? null}) RETURNING id ` memberMap[`${m.firstName} ${m.lastName}`] = row } console.log(` Created ${members.length} members`) // --- Music Repair Service Templates --- const templates = [ // Strings — Violin { name: 'Bow Rehair', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '65.00', cost: '15.00' }, { name: 'Bow Rehair', itemCategory: 'Violin', size: '3/4', itemType: 'flat_rate', price: '55.00', cost: '12.00' }, { name: 'Bow Rehair', itemCategory: 'Violin', size: '1/2', itemType: 'flat_rate', price: '50.00', cost: '10.00' }, { name: 'Bridge Setup', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '40.00', cost: '10.00' }, { name: 'Bridge Replacement', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '75.00', cost: '25.00' }, { name: 'String Change', itemCategory: 'Violin', size: '4/4', itemType: 'flat_rate', price: '35.00', cost: '12.00' }, { name: 'Peg Fitting', itemCategory: 'Violin', size: null, itemType: 'labor', price: '30.00', cost: null }, { name: 'Soundpost Adjustment', itemCategory: 'Violin', size: null, itemType: 'labor', price: '25.00', cost: null }, { name: 'Seam Repair', itemCategory: 'Violin', size: null, itemType: 'labor', price: '45.00', cost: null }, // Strings — Viola { name: 'Bow Rehair', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '65.00', cost: '15.00' }, { name: 'Bridge Setup', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '45.00', cost: '12.00' }, { name: 'String Change', itemCategory: 'Viola', size: null, itemType: 'flat_rate', price: '40.00', cost: '15.00' }, // Strings — Cello { name: 'Bow Rehair', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '80.00', cost: '20.00' }, { name: 'Bridge Setup', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '55.00', cost: '15.00' }, { name: 'String Change', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '50.00', cost: '20.00' }, { name: 'Endpin Repair', itemCategory: 'Cello', size: null, itemType: 'labor', price: '35.00', cost: null }, // Strings — Bass { name: 'Bow Rehair', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '90.00', cost: '25.00' }, { name: 'Bridge Setup', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '65.00', cost: '20.00' }, { name: 'String Change', itemCategory: 'Bass', size: null, itemType: 'flat_rate', price: '60.00', cost: '25.00' }, // Additional string services { name: 'Fingerboard Planing', itemCategory: 'Violin', size: null, itemType: 'labor', price: '85.00', cost: null }, { name: 'Fingerboard Planing', itemCategory: 'Viola', size: null, itemType: 'labor', price: '95.00', cost: null }, { name: 'Fingerboard Planing', itemCategory: 'Cello', size: null, itemType: 'labor', price: '120.00', cost: null }, { name: 'Varnish Touch-Up', itemCategory: 'Violin', size: null, itemType: 'labor', price: '55.00', cost: null }, { name: 'Varnish Touch-Up', itemCategory: 'Cello', size: null, itemType: 'labor', price: '75.00', cost: null }, { name: 'Neck Reset', itemCategory: 'Violin', size: null, itemType: 'labor', price: '200.00', cost: null }, { name: 'Neck Reset', itemCategory: 'Cello', size: null, itemType: 'labor', price: '350.00', cost: null }, { name: 'Bass Bar Replacement', itemCategory: 'Violin', size: null, itemType: 'labor', price: '300.00', cost: null }, { name: 'Tailgut Replacement', itemCategory: 'Violin', size: null, itemType: 'flat_rate', price: '20.00', cost: '5.00' }, { name: 'Tailgut Replacement', itemCategory: 'Cello', size: null, itemType: 'flat_rate', price: '25.00', cost: '8.00' }, // General { name: 'General Cleaning', itemCategory: null, size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' }, { name: 'Diagnostic Evaluation', itemCategory: null, size: null, itemType: 'flat_rate', price: '25.00', cost: null }, ] // Deactivate generic templates first await sql`UPDATE repair_service_template SET is_active = false` console.log(' Deactivated generic templates') for (const t of templates) { const existing = await sql`SELECT id FROM repair_service_template WHERE name = ${t.name} AND COALESCE(item_category, '') = ${t.itemCategory ?? ''} AND COALESCE(size, '') = ${t.size ?? ''}` if (existing.length > 0) continue await sql`INSERT INTO repair_service_template (name, item_category, size, item_type, default_price, default_cost, is_active) VALUES (${t.name}, ${t.itemCategory}, ${t.size}, ${t.itemType}, ${t.price}, ${t.cost}, true)` console.log(` Template: ${t.name} — ${t.itemCategory ?? 'General'} ${t.size ?? ''}`) } // --- Music Repair Tickets --- // First clear any generic tickets await sql`DELETE FROM repair_ticket WHERE id NOT IN (SELECT DISTINCT repair_ticket_id FROM repair_note)` const tickets: { customer: string; item: string; serial: string | null; problem: string; condition: string; status: string; estimate: string | null; lineItems: { type: string; desc: string; qty: number; price: number; cost?: number }[] }[] = [ { customer: 'Mike Thompson', item: 'Jay Haide Cello 4/4', serial: 'JH-C44-1892', problem: 'Endpin mechanism worn, slips during playing. Seam opening near lower bout.', condition: 'fair', status: 'in_progress', estimate: '95.00', lineItems: [ { type: 'labor', desc: 'Endpin repair — remove and refit mechanism', qty: 1, price: 45 }, { type: 'labor', desc: 'Seam repair — lower bout', qty: 1, price: 45 }, { type: 'consumable', desc: 'Hide glue', qty: 1, price: 5, cost: 5 }, ]}, { customer: 'Emily Chen', item: 'Scott Cao Viola 16"', serial: 'SC-VA16-0547', problem: 'Bridge warped, soundpost has shifted. Needs full setup.', condition: 'fair', status: 'pending_approval', estimate: '120.00', lineItems: [ { type: 'flat_rate', desc: 'Bridge replacement — Viola', qty: 1, price: 75, cost: 18 }, { type: 'labor', desc: 'Soundpost adjustment', qty: 1, price: 25 }, { type: 'flat_rate', desc: 'String change — Viola', qty: 1, price: 35, cost: 32 }, ]}, { customer: 'David Smith', item: 'German Workshop Violin 4/4', serial: null, problem: 'Bow needs rehair, bridge slightly warped', condition: 'fair', status: 'ready', estimate: '105.00', lineItems: [ { type: 'flat_rate', desc: 'Bow rehair — Violin 4/4', qty: 1, price: 65, cost: 15 }, { type: 'flat_rate', desc: 'Bridge setup — Violin 4/4', qty: 1, price: 40, cost: 10 }, { type: 'consumable', desc: 'Bow hair — Mongolian white', qty: 1, price: 18, cost: 18 }, ]}, { customer: 'Carlos Garcia', item: 'Eastman VL305 Violin 4/4', serial: 'EA-V305-X42', problem: 'Fingerboard wear near 3rd position, open seam on top plate', condition: 'good', status: 'new', estimate: null, lineItems: [] }, { customer: 'Patricia Williams', item: 'Shen SB100 Bass 3/4', serial: 'SH-B34-0891', problem: 'Bridge feet not fitting soundboard, wolf tone on G string', condition: 'good', status: 'diagnosing', estimate: null, lineItems: [] }, { customer: 'Walk-In Customer', item: 'Student Violin 3/4', serial: null, problem: 'Pegs slipping, E string buzzing against fingerboard', condition: 'fair', status: 'intake', estimate: null, lineItems: [] }, { customer: 'Smith Family', item: 'Suzuki Student Violin 1/2', serial: null, problem: 'Pegs slipping, bridge leaning forward', condition: 'fair', status: 'new', estimate: null, lineItems: [] }, { customer: 'Rivera Family', item: 'Eastman VC80 Cello 3/4', serial: 'EA-VC80-3Q-X01', problem: 'A string peg cracked, needs replacement. Bow rehair overdue.', condition: 'good', status: 'in_progress', estimate: '85.00', lineItems: [ { type: 'part', desc: 'Pegs — Cello Boxwood Set (4)', qty: 1, price: 28, cost: 10 }, { type: 'labor', desc: 'Peg fitting — Cello', qty: 1, price: 35 }, { type: 'flat_rate', desc: 'Bow rehair — Cello', qty: 1, price: 75, cost: 18 }, { type: 'consumable', desc: 'Peg compound', qty: 1, price: 6, cost: 6 }, ]}, ] for (const t of tickets) { const existing = await sql`SELECT id FROM repair_ticket WHERE customer_name = ${t.customer} AND problem_description = ${t.problem}` if (existing.length > 0) continue const num = String(Math.floor(100000 + Math.random() * 900000)) const acctId = acctIds[t.customer] ?? null const [ticket] = await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, item_description, serial_number, problem_description, condition_in, status, estimated_cost) VALUES (${num}, ${t.customer}, ${acctId}, ${t.item}, ${t.serial}, ${t.problem}, ${t.condition}, ${t.status}, ${t.estimate}) RETURNING id` for (const li of t.lineItems) { const total = (li.qty * li.price).toFixed(2) await sql`INSERT INTO repair_line_item (repair_ticket_id, item_type, description, qty, unit_price, total_price, cost) VALUES (${ticket.id}, ${li.type}, ${li.desc}, ${li.qty}, ${li.price.toFixed(2)}, ${total}, ${li.cost?.toFixed(2) ?? null})` } console.log(` Ticket: ${t.customer} — ${t.item} [${t.status}]${t.lineItems.length > 0 ? ` (${t.lineItems.length} items)` : ''}`) } // --- School Orchestra Batch --- const batchExists = await sql`SELECT id FROM repair_batch WHERE contact_name = 'Ms. Park'` if (batchExists.length === 0) { const schoolId = acctIds['Oak Elementary PTA'] if (schoolId) { const batchNum = String(Math.floor(100000 + Math.random() * 900000)) const [batch] = await sql`INSERT INTO repair_batch (batch_number, account_id, contact_name, contact_phone, contact_email, item_count, notes, status) VALUES (${batchNum}, ${schoolId}, 'Ms. Park', '555-0201', 'jpark@oakelementary.edu', 6, 'School orchestra summer maintenance — 6 string instruments before fall semester', 'intake') RETURNING id` const batchTickets = [ { item: 'Student Violin 4/4 — Eastman', problem: 'Bridge warped, pegs slipping, needs full setup', condition: 'fair' }, { item: 'Student Violin 3/4 — Shen', problem: 'Open seam on lower bout, fingerboard wear', condition: 'fair' }, { item: 'Student Violin 1/2 — Eastman', problem: 'Tailpiece gut broken, fine tuners corroded', condition: 'poor' }, { item: 'Student Viola 15" — Eastman', problem: 'Chin rest loose, soundpost leaning', condition: 'good' }, { item: 'Student Cello 3/4 — Eastman', problem: 'Endpin stuck, bridge feet not fitting', condition: 'fair' }, { item: 'Student Cello 1/2 — Shen', problem: 'Bow rehair needed, strings old and fraying', condition: 'fair' }, ] for (const bt of batchTickets) { const num = String(Math.floor(100000 + Math.random() * 900000)) await sql`INSERT INTO repair_ticket (ticket_number, customer_name, account_id, repair_batch_id, item_description, problem_description, condition_in, status) VALUES (${num}, 'Lincoln High School', ${schoolId}, ${batch.id}, ${bt.item}, ${bt.problem}, ${bt.condition}, 'new')` console.log(` Batch ticket: ${bt.item}`) } console.log(' Batch: Oak Elementary — 6 string instruments') } } // --- Enable inventory module --- await sql`UPDATE module_config SET enabled = true WHERE slug = 'inventory'` console.log(' Enabled inventory module') // --- Inventory seed data --- await seedInventory(sql) // --- Enable lessons module --- await sql`UPDATE module_config SET enabled = true WHERE slug = 'lessons'` console.log(' Enabled lessons module') // --- Lesson seed data --- await seedLessons(sql) console.log('\nMusic store seed complete!') await sql.end() } async function seedInventory(sql: any) { console.log('\nSeeding inventory data...') // ── Suppliers ───────────────────────────────────────────────────────────── const supplierDefs = [ { name: 'Shar Music', contactName: 'Sales Desk', email: 'orders@sharmusic.com', phone: '800-248-7427', website: 'sharmusic.com', paymentTerms: 'Net 30' }, { name: 'Southwest Strings', contactName: 'Dealer Rep', email: 'dealer@swstrings.com', phone: '800-528-3430', website: 'swstrings.com', paymentTerms: 'Net 30' }, { name: 'Connolly Music', contactName: 'Account Rep', email: 'wholesale@connollymusic.com', phone: '800-644-5268', website: 'connollymusic.com', paymentTerms: 'Net 60' }, { name: 'D\'Addario', contactName: 'Dealer Services', email: 'dealer@daddario.com', phone: '800-323-2746', website: 'daddario.com', paymentTerms: 'Net 30' }, { name: 'Pirastro', contactName: 'US Distributor', email: 'us@pirastro.com', phone: '800-223-2500', website: 'pirastro.com', paymentTerms: 'Net 30' }, ] const supplierIds: Record = {} for (const s of supplierDefs) { const [existing] = await sql`SELECT id FROM supplier WHERE name = ${s.name}` if (existing) { supplierIds[s.name] = existing.id; continue } const [row] = await sql`INSERT INTO supplier (name, contact_name, email, phone, website, payment_terms) VALUES (${s.name}, ${s.contactName}, ${s.email}, ${s.phone}, ${s.website}, ${s.paymentTerms}) RETURNING id` supplierIds[s.name] = row.id console.log(` Supplier: ${s.name}`) } // ── Categories ───────────────────────────────────────────────────────────── const categoryDefs = [ { name: 'Violins', sortOrder: 1 }, { name: 'Violas', sortOrder: 2 }, { name: 'Cellos', sortOrder: 3 }, { name: 'Basses', sortOrder: 4 }, { name: 'Bows', sortOrder: 5 }, { name: 'Strings & Accessories', sortOrder: 6 }, { name: 'Cases & Bags', sortOrder: 7 }, { name: 'Rosin', sortOrder: 8 }, { name: 'Shoulder Rests', sortOrder: 9 }, { name: 'Sheet Music & Books', sortOrder: 10 }, { name: 'Maintenance & Care', sortOrder: 11 }, ] const catIds: Record = {} for (const c of categoryDefs) { const [existing] = await sql`SELECT id FROM category WHERE name = ${c.name}` if (existing) { catIds[c.name] = existing.id; continue } const [row] = await sql`INSERT INTO category (name, sort_order) VALUES (${c.name}, ${c.sortOrder}) RETURNING id` catIds[c.name] = row.id console.log(` Category: ${c.name}`) } // ── Products ─────────────────────────────────────────────────────────────── // isSerialized=true → track individual units; isRental=true → available for rental type ProductDef = { name: string; sku: string; brand: string; model: string; category: string price: string; minPrice?: string; rentalRateMonthly?: string isSerialized: boolean; isRental: boolean; isDualUseRepair?: boolean qtyOnHand?: number; qtyReorderPoint?: number; description?: string } const productDefs: ProductDef[] = [ // ── Violins ── { name: 'Violin 4/4 — Student Outfit', sku: 'VLN-44-STU', brand: 'Eastman', model: 'VL80', category: 'Violins', price: '399.00', minPrice: '350.00', rentalRateMonthly: '25.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: 'Full-size student violin outfit includes bow, rosin, and lightweight case.', }, { name: 'Violin 3/4 — Student Outfit', sku: 'VLN-34-STU', brand: 'Eastman', model: 'VL80', category: 'Violins', price: '349.00', minPrice: '300.00', rentalRateMonthly: '22.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '3/4 size student violin outfit — ideal for players ages 7–10.', }, { name: 'Violin 1/2 — Student Outfit', sku: 'VLN-12-STU', brand: 'Eastman', model: 'VL80', category: 'Violins', price: '299.00', minPrice: '260.00', rentalRateMonthly: '20.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '1/2 size student violin outfit — ideal for players ages 5–7.', }, { name: 'Violin 1/4 — Student Outfit', sku: 'VLN-14-STU', brand: 'Eastman', model: 'VL80', category: 'Violins', price: '249.00', minPrice: '220.00', rentalRateMonthly: '18.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '1/4 size student violin outfit — ideal for players ages 4–6.', }, { name: 'Violin 4/4 — Intermediate', sku: 'VLN-44-INT', brand: 'Eastman', model: 'VL305', category: 'Violins', price: '895.00', minPrice: '800.00', rentalRateMonthly: '45.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: 'Carved spruce top and maple back/sides. Excellent step-up instrument.', }, { name: 'Violin 4/4 — Advanced', sku: 'VLN-44-ADV', brand: 'Eastman', model: 'VL605', category: 'Violins', price: '2495.00', minPrice: '2200.00', isSerialized: true, isRental: false, isDualUseRepair: false, description: 'Hand-carved workshop violin with Dominant strings. Concert-ready.', }, // ── Violas ── { name: 'Viola 15" — Student Outfit', sku: 'VLA-15-STU', brand: 'Eastman', model: 'VA80', category: 'Violas', price: '449.00', minPrice: '400.00', rentalRateMonthly: '28.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '15-inch student viola outfit with bow and lightweight case.', }, { name: 'Viola 16" — Student Outfit', sku: 'VLA-16-STU', brand: 'Eastman', model: 'VA80', category: 'Violas', price: '449.00', minPrice: '400.00', rentalRateMonthly: '28.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '16-inch student viola outfit with bow and lightweight case.', }, { name: 'Viola 16" — Intermediate', sku: 'VLA-16-INT', brand: 'Eastman', model: 'VA305', category: 'Violas', price: '1095.00', minPrice: '950.00', rentalRateMonthly: '55.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: 'Step-up viola with carved top, warm dark tone. Includes quality bow and case.', }, // ── Cellos ── { name: 'Cello 4/4 — Student Outfit', sku: 'CLO-44-STU', brand: 'Eastman', model: 'VC80', category: 'Cellos', price: '649.00', minPrice: '580.00', rentalRateMonthly: '40.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: 'Full-size student cello outfit includes fiberglass bow and padded bag.', }, { name: 'Cello 3/4 — Student Outfit', sku: 'CLO-34-STU', brand: 'Eastman', model: 'VC80', category: 'Cellos', price: '599.00', minPrice: '530.00', rentalRateMonthly: '36.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '3/4 student cello outfit — ideal for players ages 9–12.', }, { name: 'Cello 1/2 — Student Outfit', sku: 'CLO-12-STU', brand: 'Eastman', model: 'VC80', category: 'Cellos', price: '549.00', minPrice: '490.00', rentalRateMonthly: '32.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '1/2 student cello outfit — ideal for players ages 7–10.', }, { name: 'Cello 4/4 — Intermediate', sku: 'CLO-44-INT', brand: 'Eastman', model: 'VC305', category: 'Cellos', price: '1495.00', minPrice: '1300.00', rentalRateMonthly: '75.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: 'Carved spruce top intermediate cello. Powerful projection, warm tone.', }, // ── Basses ── { name: 'Bass 3/4 — Student Outfit', sku: 'BAS-34-STU', brand: 'Engelhardt', model: 'ES-1', category: 'Basses', price: '1299.00', minPrice: '1150.00', rentalRateMonthly: '65.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '3/4 student laminate bass outfit. Standard school/orchestra size.', }, { name: 'Bass 1/2 — Student', sku: 'BAS-12-STU', brand: 'Eastman', model: 'VB80', category: 'Basses', price: '1099.00', minPrice: '980.00', rentalRateMonthly: '55.00', isSerialized: true, isRental: true, isDualUseRepair: false, description: '1/2 size student double bass — ideal for younger players.', }, // ── Bows ── { name: 'Violin Bow — Student Brazilwood', sku: 'BOW-VLN-STU', brand: 'CodaBow', model: 'Joule', category: 'Bows', price: '89.00', minPrice: '75.00', isSerialized: false, isRental: false, qtyOnHand: 8, qtyReorderPoint: 3, description: 'Brazilwood student violin bow with nickel-silver winding.', }, { name: 'Violin Bow — Carbon Fiber', sku: 'BOW-VLN-CF', brand: 'CodaBow', model: 'Diamond NX', category: 'Bows', price: '295.00', minPrice: '265.00', isSerialized: false, isRental: false, qtyOnHand: 4, qtyReorderPoint: 2, description: 'Lightweight carbon fiber violin bow. Excellent balance and response.', }, { name: 'Viola Bow — Student Brazilwood', sku: 'BOW-VLA-STU', brand: 'CodaBow', model: 'Joule', category: 'Bows', price: '95.00', minPrice: '80.00', isSerialized: false, isRental: false, qtyOnHand: 5, qtyReorderPoint: 2, }, { name: 'Cello Bow — Student Brazilwood', sku: 'BOW-CLO-STU', brand: 'CodaBow', model: 'Joule', category: 'Bows', price: '110.00', minPrice: '95.00', isSerialized: false, isRental: false, qtyOnHand: 5, qtyReorderPoint: 2, }, { name: 'Bass Bow — Student Brazilwood (German)', sku: 'BOW-BAS-STU-G', brand: 'Glasser', model: 'German', category: 'Bows', price: '85.00', minPrice: '70.00', isSerialized: false, isRental: false, qtyOnHand: 4, qtyReorderPoint: 2, }, // ── Strings (also used as repair line items) ── { name: 'Violin Strings — Dominant 4/4 Set', sku: 'STR-VLN-DOM-44', brand: 'Thomastik', model: 'Dominant', category: 'Strings & Accessories', price: '38.00', minPrice: '32.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 20, qtyReorderPoint: 6, description: 'Industry-standard synthetic core violin string set. Full size.', }, { name: 'Violin Strings — Dominant 3/4 Set', sku: 'STR-VLN-DOM-34', brand: 'Thomastik', model: 'Dominant', category: 'Strings & Accessories', price: '38.00', minPrice: '32.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 12, qtyReorderPoint: 4, }, { name: 'Violin Strings — Prelude 4/4 Set', sku: 'STR-VLN-PRE-44', brand: "D'Addario", model: 'Prelude', category: 'Strings & Accessories', price: '18.00', minPrice: '15.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 24, qtyReorderPoint: 8, description: 'Budget-friendly solid steel core strings. Great for beginners.', }, { name: 'Viola Strings — Dominant Set', sku: 'STR-VLA-DOM', brand: 'Thomastik', model: 'Dominant', category: 'Strings & Accessories', price: '52.00', minPrice: '45.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 3, }, { name: 'Cello Strings — Prelude Set', sku: 'STR-CLO-PRE', brand: "D'Addario", model: 'Prelude', category: 'Strings & Accessories', price: '38.00', minPrice: '32.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 12, qtyReorderPoint: 4, }, { name: 'Cello Strings — Larsen Set', sku: 'STR-CLO-LAR', brand: 'Larsen', model: 'Original', category: 'Strings & Accessories', price: '95.00', minPrice: '85.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Bass Strings — Helicore Orchestral 3/4 Set', sku: 'STR-BAS-HEL', brand: "D'Addario", model: 'Helicore Orchestral', category: 'Strings & Accessories', price: '110.00', minPrice: '95.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 2, }, // ── Rosin ── { name: 'Rosin — Hill Light', sku: 'RSN-HILL-L', brand: 'Hill', model: 'Light', category: 'Rosin', price: '8.00', minPrice: '6.50', isSerialized: false, isRental: false, qtyOnHand: 30, qtyReorderPoint: 10, description: 'Light rosin for violin and viola. Low dust.', }, { name: 'Rosin — Hill Dark', sku: 'RSN-HILL-D', brand: 'Hill', model: 'Dark', category: 'Rosin', price: '8.00', minPrice: '6.50', isSerialized: false, isRental: false, qtyOnHand: 20, qtyReorderPoint: 8, description: 'Dark rosin for cello and bass.', }, { name: 'Rosin — Pirastro Goldflex', sku: 'RSN-PIR-GF', brand: 'Pirastro', model: 'Goldflex', category: 'Rosin', price: '14.00', minPrice: '11.00', isSerialized: false, isRental: false, qtyOnHand: 15, qtyReorderPoint: 5, }, // ── Shoulder Rests ── { name: 'Shoulder Rest — Kun Original 4/4', sku: 'SR-KUN-44', brand: 'Kun', model: 'Original', category: 'Shoulder Rests', price: '28.00', minPrice: '23.00', isSerialized: false, isRental: false, qtyOnHand: 12, qtyReorderPoint: 4, description: 'Collapsible violin shoulder rest with rubber feet.', }, { name: 'Shoulder Rest — Kun Collapsible 3/4–4/4', sku: 'SR-KUN-C', brand: 'Kun', model: 'Collapsible', category: 'Shoulder Rests', price: '35.00', minPrice: '29.00', isSerialized: false, isRental: false, qtyOnHand: 8, qtyReorderPoint: 3, }, { name: 'Shoulder Rest — Bon Musica 4/4', sku: 'SR-BON-44', brand: 'Bon Musica', model: '4/4', category: 'Shoulder Rests', price: '42.00', minPrice: '36.00', isSerialized: false, isRental: false, qtyOnHand: 6, qtyReorderPoint: 2, description: 'Curved metal shoulder rest with contoured padding. Great for players with shoulder issues.', }, // ── Maintenance ── { name: 'String Cleaner — Ultra X-Cleaner', sku: 'MNT-CLN-STR', brand: 'Ultra', model: 'X-Cleaner', category: 'Maintenance & Care', price: '12.00', minPrice: '9.00', isSerialized: false, isRental: false, qtyOnHand: 18, qtyReorderPoint: 6, }, { name: 'Instrument Polish — Hill', sku: 'MNT-POL-HILL', brand: 'Hill', model: 'Polish', category: 'Maintenance & Care', price: '10.00', minPrice: '8.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 15, qtyReorderPoint: 5, }, // ── Repair Parts ── { name: 'Bridge — Violin 4/4 Blank', sku: 'RPR-BRG-VLN-44', brand: 'Aubert', model: 'Mirecourt', category: 'Maintenance & Care', price: '18.00', minPrice: '14.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, description: 'Aubert Mirecourt violin bridge blank. Requires fitting.', }, { name: 'Bridge — Violin 3/4 Blank', sku: 'RPR-BRG-VLN-34', brand: 'Aubert', model: 'Mirecourt', category: 'Maintenance & Care', price: '16.00', minPrice: '12.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Bridge — Viola Blank', sku: 'RPR-BRG-VLA', brand: 'Aubert', model: 'Mirecourt', category: 'Maintenance & Care', price: '22.00', minPrice: '17.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Bridge — Cello Blank', sku: 'RPR-BRG-CLO', brand: 'Aubert', model: 'Mirecourt', category: 'Maintenance & Care', price: '32.00', minPrice: '25.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 4, qtyReorderPoint: 2, }, { name: 'Bridge — Bass Blank', sku: 'RPR-BRG-BAS', brand: 'Aubert', model: 'Mirecourt', category: 'Maintenance & Care', price: '48.00', minPrice: '38.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 3, qtyReorderPoint: 1, }, { name: 'Tailpiece — Violin 4/4 Composite', sku: 'RPR-TLP-VLN-44', brand: 'Wittner', model: 'Ultra', category: 'Maintenance & Care', price: '14.00', minPrice: '11.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 3, description: 'Wittner Ultra composite tailpiece with integrated fine tuners.', }, { name: 'Tailpiece — Cello 4/4 Composite', sku: 'RPR-TLP-CLO-44', brand: 'Wittner', model: 'Ultra', category: 'Maintenance & Care', price: '22.00', minPrice: '17.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 4, qtyReorderPoint: 2, }, { name: 'Pegs — Violin Boxwood Set (4)', sku: 'RPR-PEG-VLN-BX', brand: 'Generic', model: 'Boxwood', category: 'Maintenance & Care', price: '12.00', minPrice: '9.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 12, qtyReorderPoint: 4, description: 'Boxwood violin pegs, set of 4. Requires fitting and reaming.', }, { name: 'Pegs — Viola Boxwood Set (4)', sku: 'RPR-PEG-VLA-BX', brand: 'Generic', model: 'Boxwood', category: 'Maintenance & Care', price: '14.00', minPrice: '11.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 3, }, { name: 'Pegs — Cello Boxwood Set (4)', sku: 'RPR-PEG-CLO-BX', brand: 'Generic', model: 'Boxwood', category: 'Maintenance & Care', price: '18.00', minPrice: '14.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Fine Tuner — Single Violin', sku: 'RPR-FTN-VLN', brand: 'Wittner', model: 'Finetune', category: 'Maintenance & Care', price: '4.00', minPrice: '3.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 20, qtyReorderPoint: 8, }, { name: 'Endpin — Cello Replacement', sku: 'RPR-EPN-CLO', brand: 'Wittner', model: 'Finetune', category: 'Maintenance & Care', price: '24.00', minPrice: '18.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 4, qtyReorderPoint: 2, description: 'Replacement cello endpin with aluminum sleeve.', }, { name: 'Endpin — Bass Replacement', sku: 'RPR-EPN-BAS', brand: 'Wittner', model: 'Finetune', category: 'Maintenance & Care', price: '32.00', minPrice: '24.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 3, qtyReorderPoint: 1, }, { name: 'Nut — Violin Ebony Blank', sku: 'RPR-NUT-VLN', brand: 'Generic', model: 'Ebony', category: 'Maintenance & Care', price: '6.00', minPrice: '4.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, }, { name: 'Seam Glue — Hide Glue (4 oz)', sku: 'RPR-GLU-HIDE', brand: 'Franklin', model: 'Hide Glue', category: 'Maintenance & Care', price: '8.00', minPrice: '6.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, description: 'Traditional hide glue for open seam and structural repairs.', }, { name: 'Sound Post — Violin', sku: 'RPR-SPS-VLN', brand: 'Generic', model: 'Spruce', category: 'Maintenance & Care', price: '5.00', minPrice: '3.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 3, }, { name: 'Sound Post — Cello', sku: 'RPR-SPS-CLO', brand: 'Generic', model: 'Spruce', category: 'Maintenance & Care', price: '8.00', minPrice: '6.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 4, qtyReorderPoint: 2, }, { name: 'Bow Hair — Mongolian White (hank)', sku: 'RPR-HAIR-WH', brand: 'Generic', model: 'Mongolian White', category: 'Maintenance & Care', price: '18.00', minPrice: '14.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 3, description: 'Mongolian white bow hair for rehairing violin, viola, cello, or bass bows.', }, { name: 'Peg Compound — Hill Peg Dope', sku: 'RPR-PEG-DOC', brand: 'Hill', model: 'Peg Compound', category: 'Maintenance & Care', price: '6.00', minPrice: '4.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 12, qtyReorderPoint: 4, description: 'Hill peg compound for slipping or sticking pegs.', }, { name: 'Fingerboard Oil — Lemon Oil (2 oz)', sku: 'RPR-OIL-LMN', brand: 'Music Nomad', model: 'Lemon Oil', category: 'Maintenance & Care', price: '9.00', minPrice: '7.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, description: 'Conditions and cleans ebony and rosewood fingerboards.', }, { name: 'Chin Rest — Violin Guarneri Style', sku: 'RPR-CHN-VLN-GU', brand: 'Wittner', model: 'Guarneri', category: 'Maintenance & Care', price: '16.00', minPrice: '12.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 3, description: 'Wittner composite Guarneri-style violin chin rest. Center mount.', }, { name: 'Chin Rest — Viola Side Mount', sku: 'RPR-CHN-VLA-SD', brand: 'Wittner', model: 'Augsburg', category: 'Maintenance & Care', price: '18.00', minPrice: '14.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Chin Rest Screws — Replacement Set', sku: 'RPR-CHN-SCR', brand: 'Generic', model: 'Brass', category: 'Maintenance & Care', price: '3.00', minPrice: '2.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 20, qtyReorderPoint: 8, description: 'Replacement brass chin rest barrel screws, set of 2.', }, { name: 'Bow Grip — Leather Violin/Viola', sku: 'RPR-GRP-VLN', brand: 'Generic', model: 'Leather', category: 'Maintenance & Care', price: '5.00', minPrice: '3.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 15, qtyReorderPoint: 6, description: 'Replacement leather bow grip for violin or viola bow.', }, { name: 'Bow Grip — Leather Cello/Bass', sku: 'RPR-GRP-CLO', brand: 'Generic', model: 'Leather', category: 'Maintenance & Care', price: '6.00', minPrice: '4.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, }, { name: 'Bow Winding — Silver Lapping', sku: 'RPR-WND-SLV', brand: 'Generic', model: 'Silver', category: 'Maintenance & Care', price: '8.00', minPrice: '6.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 8, qtyReorderPoint: 3, description: 'Silver lapping wire for bow winding replacement.', }, { name: 'Bow Tip Plate — Violin Ivory-Style', sku: 'RPR-TIP-VLN', brand: 'Generic', model: 'Mammoth Ivory', category: 'Maintenance & Care', price: '7.00', minPrice: '5.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, description: 'Replacement bow tip plate (mammoth ivory substitute).', }, { name: 'Saddle — Violin Ebony', sku: 'RPR-SDL-VLN', brand: 'Generic', model: 'Ebony', category: 'Maintenance & Care', price: '5.00', minPrice: '3.50', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 10, qtyReorderPoint: 4, }, { name: 'Saddle — Cello Ebony', sku: 'RPR-SDL-CLO', brand: 'Generic', model: 'Ebony', category: 'Maintenance & Care', price: '8.00', minPrice: '6.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 6, qtyReorderPoint: 2, }, { name: 'Retouching Varnish — Violin/Viola', sku: 'RPR-VRN-RTH', brand: 'Hammerl', model: 'Retouching', category: 'Maintenance & Care', price: '22.00', minPrice: '17.00', isSerialized: false, isRental: false, isDualUseRepair: true, qtyOnHand: 4, qtyReorderPoint: 2, description: 'Spirit-based retouching varnish for cosmetic repairs and touch-ups.', }, { name: 'Humidifier — Dampit Violin', sku: 'MNT-HUM-VLN', brand: 'Dampit', model: 'Violin', category: 'Maintenance & Care', price: '16.00', minPrice: '13.00', isSerialized: false, isRental: false, qtyOnHand: 12, qtyReorderPoint: 4, }, { name: 'Humidifier — Dampit Cello', sku: 'MNT-HUM-CLO', brand: 'Dampit', model: 'Cello', category: 'Maintenance & Care', price: '18.00', minPrice: '15.00', isSerialized: false, isRental: false, qtyOnHand: 8, qtyReorderPoint: 3, }, ] const productIds: Record = {} for (const p of productDefs) { const [existing] = await sql`SELECT id FROM product WHERE sku = ${p.sku}` if (existing) { productIds[p.sku] = existing.id; continue } const catId = catIds[p.category] ?? null const [row] = await sql` INSERT INTO product (sku, name, brand, model, category_id, price, min_price, rental_rate_monthly, is_serialized, is_rental, is_dual_use_repair, qty_on_hand, qty_reorder_point, description, is_active) VALUES (${p.sku}, ${p.name}, ${p.brand}, ${p.model}, ${catId}, ${p.price}, ${p.minPrice ?? null}, ${p.rentalRateMonthly ?? null}, ${p.isSerialized}, ${p.isRental}, ${p.isDualUseRepair ?? false}, ${p.qtyOnHand ?? 0}, ${p.qtyReorderPoint ?? null}, ${p.description ?? null}, true) RETURNING id` productIds[p.sku] = row.id console.log(` Product: ${p.name}`) } // ── Inventory Units (serialized instruments) ────────────────────────────── type UnitDef = { sku: string; serial: string; condition: string; status: string; purchaseDate: string; cost: string; notes?: string } const unitDefs: UnitDef[] = [ // Student violin 4/4 — rental fleet { sku: 'VLN-44-STU', serial: 'EA-V80-001', condition: 'good', status: 'available', purchaseDate: '2022-08-15', cost: '185.00' }, { sku: 'VLN-44-STU', serial: 'EA-V80-002', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00' }, { sku: 'VLN-44-STU', serial: 'EA-V80-003', condition: 'fair', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00', notes: 'Minor scratches on back' }, { sku: 'VLN-44-STU', serial: 'EA-V80-004', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '185.00' }, { sku: 'VLN-44-STU', serial: 'EA-V80-005', condition: 'excellent', status: 'available', purchaseDate: '2023-06-01', cost: '195.00' }, { sku: 'VLN-44-STU', serial: 'EA-V80-006', condition: 'fair', status: 'in_repair', purchaseDate: '2021-08-01', cost: '175.00', notes: 'Pegs slipping, in shop' }, // Student violin 3/4 { sku: 'VLN-34-STU', serial: 'EA-V80-3Q-001', condition: 'good', status: 'available', purchaseDate: '2022-09-01', cost: '160.00' }, { sku: 'VLN-34-STU', serial: 'EA-V80-3Q-002', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '160.00' }, { sku: 'VLN-34-STU', serial: 'EA-V80-3Q-003', condition: 'fair', status: 'rented', purchaseDate: '2021-09-01', cost: '155.00', notes: 'Repaired open seam 2023' }, // Student violin 1/2 { sku: 'VLN-12-STU', serial: 'EA-V80-HF-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '140.00' }, { sku: 'VLN-12-STU', serial: 'EA-V80-HF-002', condition: 'good', status: 'available', purchaseDate: '2023-07-01', cost: '145.00' }, // Student violin 1/4 { sku: 'VLN-14-STU', serial: 'EA-V80-QT-001', condition: 'good', status: 'rented', purchaseDate: '2023-01-15', cost: '120.00' }, { sku: 'VLN-14-STU', serial: 'EA-V80-QT-002', condition: 'fair', status: 'available', purchaseDate: '2022-01-15', cost: '115.00', notes: 'Replaced tailpiece 2023' }, // Intermediate violin { sku: 'VLN-44-INT', serial: 'EA-V305-001', condition: 'excellent', status: 'available', purchaseDate: '2023-08-01', cost: '420.00' }, { sku: 'VLN-44-INT', serial: 'EA-V305-002', condition: 'good', status: 'rented', purchaseDate: '2023-08-01', cost: '420.00' }, // Advanced violin { sku: 'VLN-44-ADV', serial: 'EA-V605-001', condition: 'new', status: 'available', purchaseDate: '2024-02-01', cost: '1200.00' }, // Violas { sku: 'VLA-15-STU', serial: 'EA-VA80-15-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '210.00' }, { sku: 'VLA-15-STU', serial: 'EA-VA80-15-002', condition: 'excellent', status: 'available', purchaseDate: '2023-08-01', cost: '220.00' }, { sku: 'VLA-16-STU', serial: 'EA-VA80-16-001', condition: 'good', status: 'rented', purchaseDate: '2022-09-01', cost: '210.00' }, { sku: 'VLA-16-INT', serial: 'EA-VA305-001', condition: 'new', status: 'available', purchaseDate: '2024-01-15', cost: '520.00' }, // Cellos { sku: 'CLO-44-STU', serial: 'EA-VC80-001', condition: 'good', status: 'rented', purchaseDate: '2021-08-15', cost: '300.00' }, { sku: 'CLO-44-STU', serial: 'EA-VC80-002', condition: 'good', status: 'rented', purchaseDate: '2021-08-15', cost: '300.00' }, { sku: 'CLO-44-STU', serial: 'EA-VC80-003', condition: 'fair', status: 'in_repair', purchaseDate: '2020-08-15', cost: '285.00', notes: 'Endpin mechanism worn, awaiting part' }, { sku: 'CLO-44-STU', serial: 'EA-VC80-004', condition: 'excellent', status: 'available', purchaseDate: '2023-09-01', cost: '315.00' }, { sku: 'CLO-34-STU', serial: 'EA-VC80-3Q-001', condition: 'good', status: 'rented', purchaseDate: '2022-08-15', cost: '275.00' }, { sku: 'CLO-12-STU', serial: 'EA-VC80-HF-001', condition: 'good', status: 'available', purchaseDate: '2023-08-01', cost: '255.00' }, { sku: 'CLO-44-INT', serial: 'EA-VC305-001', condition: 'new', status: 'available', purchaseDate: '2024-03-01', cost: '720.00' }, // Basses { sku: 'BAS-34-STU', serial: 'ENG-ES1-001', condition: 'good', status: 'rented', purchaseDate: '2020-08-01', cost: '600.00', notes: 'New strings installed 2024' }, { sku: 'BAS-34-STU', serial: 'ENG-ES1-002', condition: 'fair', status: 'available', purchaseDate: '2019-08-01', cost: '575.00', notes: 'Repaired bass bar 2022' }, { sku: 'BAS-34-STU', serial: 'ENG-ES1-003', condition: 'excellent', status: 'available', purchaseDate: '2023-09-01', cost: '620.00' }, { sku: 'BAS-12-STU', serial: 'EA-VB80-HF-001', condition: 'good', status: 'rented', purchaseDate: '2022-08-01', cost: '510.00' }, ] for (const u of unitDefs) { const productId = productIds[u.sku] if (!productId) continue const [existing] = await sql`SELECT id FROM inventory_unit WHERE serial_number = ${u.serial}` if (existing) continue await sql` INSERT INTO inventory_unit (product_id, serial_number, condition, status, purchase_date, purchase_cost, notes) VALUES (${productId}, ${u.serial}, ${u.condition}, ${u.status}, ${u.purchaseDate}, ${u.cost}, ${u.notes ?? null})` console.log(` Unit: ${u.serial} (${u.sku}) [${u.status}]`) } // ── Price History ────────────────────────────────────────────────────────── // Simulate a couple of price changes per key product over the past year type PriceHistoryDef = { sku: string; previousPrice: string; newPrice: string; previousMinPrice?: string; newMinPrice?: string; daysAgo: number } const priceHistoryDefs: PriceHistoryDef[] = [ // Student violins — small price increases over two cycles { sku: 'VLN-44-STU', previousPrice: '379.00', newPrice: '389.00', previousMinPrice: '330.00', newMinPrice: '340.00', daysAgo: 365 }, { sku: 'VLN-44-STU', previousPrice: '389.00', newPrice: '399.00', previousMinPrice: '340.00', newMinPrice: '350.00', daysAgo: 180 }, { sku: 'VLN-34-STU', previousPrice: '329.00', newPrice: '349.00', previousMinPrice: '285.00', newMinPrice: '300.00', daysAgo: 180 }, { sku: 'VLN-12-STU', previousPrice: '279.00', newPrice: '299.00', previousMinPrice: '245.00', newMinPrice: '260.00', daysAgo: 180 }, { sku: 'VLN-14-STU', previousPrice: '229.00', newPrice: '249.00', previousMinPrice: '200.00', newMinPrice: '220.00', daysAgo: 180 }, // Intermediate violin — one increase { sku: 'VLN-44-INT', previousPrice: '849.00', newPrice: '895.00', previousMinPrice: '759.00', newMinPrice: '800.00', daysAgo: 270 }, // Advanced violin — introduced at lower price, increased { sku: 'VLN-44-ADV', previousPrice: '2295.00', newPrice: '2495.00', previousMinPrice: '2050.00', newMinPrice: '2200.00', daysAgo: 120 }, // Violas { sku: 'VLA-15-STU', previousPrice: '419.00', newPrice: '449.00', previousMinPrice: '370.00', newMinPrice: '400.00', daysAgo: 200 }, { sku: 'VLA-16-STU', previousPrice: '419.00', newPrice: '449.00', previousMinPrice: '370.00', newMinPrice: '400.00', daysAgo: 200 }, { sku: 'VLA-16-INT', previousPrice: '995.00', newPrice: '1095.00', previousMinPrice: '875.00', newMinPrice: '950.00', daysAgo: 150 }, // Cellos { sku: 'CLO-44-STU', previousPrice: '599.00', newPrice: '649.00', previousMinPrice: '535.00', newMinPrice: '580.00', daysAgo: 220 }, { sku: 'CLO-34-STU', previousPrice: '549.00', newPrice: '599.00', previousMinPrice: '490.00', newMinPrice: '530.00', daysAgo: 220 }, { sku: 'CLO-44-INT', previousPrice: '1349.00', newPrice: '1495.00', previousMinPrice: '1195.00', newMinPrice: '1300.00', daysAgo: 160 }, // Basses { sku: 'BAS-34-STU', previousPrice: '1199.00', newPrice: '1299.00', previousMinPrice: '1075.00', newMinPrice: '1150.00', daysAgo: 240 }, // Strings — supplier cost increases { sku: 'STR-VLN-DOM-44', previousPrice: '34.00', newPrice: '38.00', previousMinPrice: '28.00', newMinPrice: '32.00', daysAgo: 90 }, { sku: 'STR-CLO-LAR', previousPrice: '88.00', newPrice: '95.00', previousMinPrice: '78.00', newMinPrice: '85.00', daysAgo: 90 }, { sku: 'STR-BAS-HEL', previousPrice: '99.00', newPrice: '110.00', previousMinPrice: '87.00', newMinPrice: '95.00', daysAgo: 90 }, // Bridges — material cost increase { sku: 'RPR-BRG-VLN-44', previousPrice: '15.00', newPrice: '18.00', daysAgo: 120 }, { sku: 'RPR-BRG-CLO', previousPrice: '28.00', newPrice: '32.00', daysAgo: 120 }, // Bow hair { sku: 'RPR-HAIR-WH', previousPrice: '15.00', newPrice: '18.00', daysAgo: 60 }, ] for (const h of priceHistoryDefs) { const productId = productIds[h.sku] if (!productId) continue const changedAt = new Date(Date.now() - h.daysAgo * 24 * 60 * 60 * 1000).toISOString() const [existing] = await sql` SELECT id FROM price_history WHERE product_id = ${productId} AND new_price = ${h.newPrice} AND previous_price = ${h.previousPrice}` if (existing) continue await sql` INSERT INTO price_history (product_id, previous_price, new_price, previous_min_price, new_min_price, created_at) VALUES (${productId}, ${h.previousPrice}, ${h.newPrice}, ${h.previousMinPrice ?? null}, ${h.newMinPrice ?? null}, ${changedAt})` console.log(` Price history: ${h.sku} $${h.previousPrice} → $${h.newPrice}`) } // ── Product-Supplier Links ───────────────────────────────────────────────── type SupplierLink = { sku: string; supplier: string; supplierSku?: string; isPreferred: boolean } const supplierLinks: SupplierLink[] = [ // Violins — Shar preferred, Southwest alternate { sku: 'VLN-44-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-44', isPreferred: true }, { sku: 'VLN-44-STU', supplier: 'Southwest Strings', supplierSku: 'EAST-VL80-4', isPreferred: false }, { sku: 'VLN-34-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-34', isPreferred: true }, { sku: 'VLN-34-STU', supplier: 'Southwest Strings', supplierSku: 'EAST-VL80-3', isPreferred: false }, { sku: 'VLN-12-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-12', isPreferred: true }, { sku: 'VLN-14-STU', supplier: 'Shar Music', supplierSku: 'EA-VL80-14', isPreferred: true }, { sku: 'VLN-44-INT', supplier: 'Shar Music', supplierSku: 'EA-VL305', isPreferred: true }, { sku: 'VLN-44-INT', supplier: 'Southwest Strings', supplierSku: 'EAST-VL305', isPreferred: false }, { sku: 'VLN-44-ADV', supplier: 'Shar Music', supplierSku: 'EA-VL605', isPreferred: true }, // Violas — Shar preferred, Connolly alternate { sku: 'VLA-15-STU', supplier: 'Shar Music', supplierSku: 'EA-VA80-15', isPreferred: true }, { sku: 'VLA-15-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VA80-15', isPreferred: false }, { sku: 'VLA-16-STU', supplier: 'Shar Music', supplierSku: 'EA-VA80-16', isPreferred: true }, { sku: 'VLA-16-INT', supplier: 'Shar Music', supplierSku: 'EA-VA305-16', isPreferred: true }, { sku: 'VLA-16-INT', supplier: 'Connolly Music', supplierSku: 'EAST-VA305', isPreferred: false }, // Cellos — Connolly preferred, Shar alternate { sku: 'CLO-44-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-44', isPreferred: true }, { sku: 'CLO-44-STU', supplier: 'Shar Music', supplierSku: 'EA-VC80-44', isPreferred: false }, { sku: 'CLO-34-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-34', isPreferred: true }, { sku: 'CLO-12-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VC80-12', isPreferred: true }, { sku: 'CLO-44-INT', supplier: 'Connolly Music', supplierSku: 'EAST-VC305', isPreferred: true }, { sku: 'CLO-44-INT', supplier: 'Shar Music', supplierSku: 'EA-VC305', isPreferred: false }, // Basses — Connolly preferred { sku: 'BAS-34-STU', supplier: 'Connolly Music', supplierSku: 'ENG-ES1', isPreferred: true }, { sku: 'BAS-34-STU', supplier: 'Southwest Strings', supplierSku: 'ENG-ES1-SW', isPreferred: false }, { sku: 'BAS-12-STU', supplier: 'Connolly Music', supplierSku: 'EAST-VB80-12', isPreferred: true }, // Strings — D'Addario for Prelude/Helicore, Shar for Dominant, Pirastro for Larsen { sku: 'STR-VLN-PRE-44', supplier: "D'Addario", supplierSku: 'J810-4/4M', isPreferred: true }, { sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', supplierSku: 'TH-132B', isPreferred: true }, { sku: 'STR-VLN-DOM-34', supplier: 'Shar Music', supplierSku: 'TH-132B-34', isPreferred: true }, { sku: 'STR-VLA-DOM', supplier: 'Shar Music', supplierSku: 'TH-141B', isPreferred: true }, { sku: 'STR-CLO-PRE', supplier: "D'Addario", supplierSku: 'J1010-4/4M', isPreferred: true }, { sku: 'STR-CLO-LAR', supplier: 'Pirastro', supplierSku: 'LAR-SET-4', isPreferred: true }, { sku: 'STR-BAS-HEL', supplier: "D'Addario", supplierSku: 'J6103/4M', isPreferred: true }, // Rosin { sku: 'RSN-HILL-L', supplier: 'Shar Music', supplierSku: 'HILL-RSN-L', isPreferred: true }, { sku: 'RSN-HILL-D', supplier: 'Shar Music', supplierSku: 'HILL-RSN-D', isPreferred: true }, { sku: 'RSN-PIR-GF', supplier: 'Pirastro', supplierSku: '900500', isPreferred: true }, // Bows { sku: 'BOW-VLN-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-VLN', isPreferred: true }, { sku: 'BOW-VLN-STU', supplier: 'Southwest Strings', supplierSku: 'CODA-JOULE-V', isPreferred: false }, { sku: 'BOW-VLN-CF', supplier: 'Shar Music', supplierSku: 'CB-DNX-VLN', isPreferred: true }, { sku: 'BOW-VLA-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-VLA', isPreferred: true }, { sku: 'BOW-CLO-STU', supplier: 'Shar Music', supplierSku: 'CB-JOULE-CLO', isPreferred: true }, { sku: 'BOW-BAS-STU-G', supplier: 'Southwest Strings', supplierSku: 'GL-GER-BAS', isPreferred: true }, // Maintenance { sku: 'MNT-POL-HILL', supplier: 'Shar Music', supplierSku: 'HILL-POL', isPreferred: true }, { sku: 'MNT-HUM-VLN', supplier: 'Shar Music', supplierSku: 'DAMP-VLN', isPreferred: true }, { sku: 'MNT-HUM-CLO', supplier: 'Shar Music', supplierSku: 'DAMP-CLO', isPreferred: true }, ] for (const link of supplierLinks) { const productId = productIds[link.sku] const supplierId = supplierIds[link.supplier] if (!productId || !supplierId) continue const [existing] = await sql`SELECT id FROM product_supplier WHERE product_id = ${productId} AND supplier_id = ${supplierId}` if (existing) continue await sql` INSERT INTO product_supplier (product_id, supplier_id, supplier_sku, is_preferred) VALUES (${productId}, ${supplierId}, ${link.supplierSku ?? null}, ${link.isPreferred})` console.log(` Linked ${link.sku} → ${link.supplier}${link.isPreferred ? ' (preferred)' : ''}`) } // ── Stock Receipts ───────────────────────────────────────────────────────── type ReceiptDef = { sku: string; supplier?: string; qty: number; costPerUnit: string; receivedDate: string; invoiceNumber?: string; notes?: string } const receiptDefs: ReceiptDef[] = [ // Initial instrument purchases { sku: 'VLN-44-STU', supplier: 'Shar Music', qty: 6, costPerUnit: '185.00', receivedDate: '2022-08-01', invoiceNumber: 'SHAR-2022-0801' }, { sku: 'VLN-34-STU', supplier: 'Shar Music', qty: 3, costPerUnit: '160.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' }, { sku: 'VLN-12-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '140.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' }, { sku: 'VLN-14-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '115.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' }, { sku: 'VLN-44-INT', supplier: 'Shar Music', qty: 2, costPerUnit: '420.00', receivedDate: '2023-08-01', invoiceNumber: 'SHAR-2023-0801' }, { sku: 'VLN-44-ADV', supplier: 'Shar Music', qty: 1, costPerUnit: '1200.00', receivedDate: '2024-02-01', invoiceNumber: 'SHAR-2024-0201' }, { sku: 'VLA-15-STU', supplier: 'Shar Music', qty: 2, costPerUnit: '210.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' }, { sku: 'VLA-16-STU', supplier: 'Shar Music', qty: 1, costPerUnit: '210.00', receivedDate: '2022-09-01', invoiceNumber: 'SHAR-2022-0901' }, { sku: 'VLA-16-INT', supplier: 'Shar Music', qty: 1, costPerUnit: '520.00', receivedDate: '2024-01-15', invoiceNumber: 'SHAR-2024-0115' }, { sku: 'CLO-44-STU', supplier: 'Connolly Music', qty: 4, costPerUnit: '300.00', receivedDate: '2021-08-01', invoiceNumber: 'CON-2021-0801' }, { sku: 'CLO-34-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '275.00', receivedDate: '2022-08-01', invoiceNumber: 'CON-2022-0801' }, { sku: 'CLO-12-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '255.00', receivedDate: '2023-08-01', invoiceNumber: 'CON-2023-0801' }, { sku: 'CLO-44-INT', supplier: 'Connolly Music', qty: 1, costPerUnit: '720.00', receivedDate: '2024-03-01', invoiceNumber: 'CON-2024-0301' }, { sku: 'BAS-34-STU', supplier: 'Connolly Music', qty: 3, costPerUnit: '595.00', receivedDate: '2019-08-01', invoiceNumber: 'CON-2019-0801' }, { sku: 'BAS-12-STU', supplier: 'Connolly Music', qty: 1, costPerUnit: '510.00', receivedDate: '2022-08-01', invoiceNumber: 'CON-2022-0802' }, // Bow stock { sku: 'BOW-VLN-STU', supplier: 'Shar Music', qty: 8, costPerUnit: '52.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' }, { sku: 'BOW-VLN-CF', supplier: 'Shar Music', qty: 4, costPerUnit: '175.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' }, { sku: 'BOW-VLA-STU', supplier: 'Shar Music', qty: 5, costPerUnit: '56.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' }, { sku: 'BOW-CLO-STU', supplier: 'Shar Music', qty: 5, costPerUnit: '65.00', receivedDate: '2024-01-10', invoiceNumber: 'SHAR-2024-0110' }, { sku: 'BOW-BAS-STU-G', supplier: 'Southwest Strings', qty: 4, costPerUnit: '48.00', receivedDate: '2024-02-01', invoiceNumber: 'SW-2024-0201' }, // String restocks { sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', qty: 24, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'SHAR-2025-0901' }, { sku: 'STR-VLN-DOM-44', supplier: 'Shar Music', qty: 12, costPerUnit: '22.50', receivedDate: '2026-01-15', invoiceNumber: 'SHAR-2026-0115' }, { sku: 'STR-VLN-DOM-34', supplier: 'Shar Music', qty: 12, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'SHAR-2025-0901' }, { sku: 'STR-VLN-PRE-44', supplier: "D'Addario", qty: 36, costPerUnit: '10.50', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' }, { sku: 'STR-VLN-PRE-44', supplier: "D'Addario", qty: 24, costPerUnit: '11.00', receivedDate: '2026-02-01', invoiceNumber: 'DAD-2026-0201' }, { sku: 'STR-VLA-DOM', supplier: 'Shar Music', qty: 12, costPerUnit: '32.00', receivedDate: '2025-10-01', invoiceNumber: 'SHAR-2025-1001' }, { sku: 'STR-CLO-PRE', supplier: "D'Addario", qty: 18, costPerUnit: '22.00', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' }, { sku: 'STR-CLO-LAR', supplier: 'Pirastro', qty: 6, costPerUnit: '58.00', receivedDate: '2025-11-01', invoiceNumber: 'PIR-2025-1101' }, { sku: 'STR-BAS-HEL', supplier: "D'Addario", qty: 10, costPerUnit: '65.00', receivedDate: '2025-09-01', invoiceNumber: 'DAD-2025-0901' }, // Rosin { sku: 'RSN-HILL-L', supplier: 'Shar Music', qty: 36, costPerUnit: '5.00', receivedDate: '2025-08-15', invoiceNumber: 'SHAR-2025-0815' }, { sku: 'RSN-HILL-D', supplier: 'Shar Music', qty: 24, costPerUnit: '5.00', receivedDate: '2025-08-15', invoiceNumber: 'SHAR-2025-0815' }, { sku: 'RSN-PIR-GF', supplier: 'Pirastro', qty: 18, costPerUnit: '8.50', receivedDate: '2025-08-15', invoiceNumber: 'PIR-2025-0815' }, // Repair parts { sku: 'RPR-BRG-VLN-44', supplier: 'Shar Music', qty: 12, costPerUnit: '10.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-BRG-VLN-34', supplier: 'Shar Music', qty: 8, costPerUnit: '9.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-BRG-VLA', supplier: 'Shar Music', qty: 8, costPerUnit: '13.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-BRG-CLO', supplier: 'Shar Music', qty: 6, costPerUnit: '19.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-BRG-BAS', supplier: 'Shar Music', qty: 4, costPerUnit: '29.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-PEG-VLN-BX', supplier: 'Shar Music', qty: 15, costPerUnit: '7.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-PEG-VLA-BX', supplier: 'Shar Music', qty: 10, costPerUnit: '8.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-PEG-CLO-BX', supplier: 'Shar Music', qty: 8, costPerUnit: '10.00', receivedDate: '2025-07-01', invoiceNumber: 'SHAR-2025-0701' }, { sku: 'RPR-HAIR-WH', supplier: 'Shar Music', qty: 12, costPerUnit: '11.00', receivedDate: '2025-10-01', invoiceNumber: 'SHAR-2025-1001' }, { sku: 'RPR-TLP-VLN-44', supplier: 'Shar Music', qty: 10, costPerUnit: '8.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' }, { sku: 'RPR-GLU-HIDE', supplier: 'Shar Music', qty: 8, costPerUnit: '4.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' }, { sku: 'RPR-POL-HILL', supplier: 'Shar Music', qty: 18, costPerUnit: '6.00', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915', notes: 'SKU changed from MNT-POL-HILL' }, { sku: 'MNT-POL-HILL', supplier: 'Shar Music', qty: 18, costPerUnit: '6.00', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' }, { sku: 'RPR-PEG-DOC', supplier: 'Shar Music', qty: 15, costPerUnit: '3.50', receivedDate: '2025-09-15', invoiceNumber: 'SHAR-2025-0915' }, // Shoulder rests { sku: 'SR-KUN-44', supplier: 'Shar Music', qty: 12, costPerUnit: '17.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' }, { sku: 'SR-KUN-C', supplier: 'Shar Music', qty: 8, costPerUnit: '21.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' }, { sku: 'SR-BON-44', supplier: 'Shar Music', qty: 6, costPerUnit: '26.00', receivedDate: '2025-08-01', invoiceNumber: 'SHAR-2025-0801' }, ] for (const r of receiptDefs) { const productId = productIds[r.sku] const supplierId = r.supplier ? supplierIds[r.supplier] : null if (!productId) continue const [existing] = await sql` SELECT id FROM stock_receipt WHERE product_id = ${productId} AND received_date = ${r.receivedDate} AND cost_per_unit = ${r.costPerUnit} AND qty = ${r.qty}` if (existing) continue const totalCost = (parseFloat(r.costPerUnit) * r.qty).toFixed(2) await sql` INSERT INTO stock_receipt (product_id, supplier_id, qty, cost_per_unit, total_cost, received_date, invoice_number, notes) VALUES (${productId}, ${supplierId ?? null}, ${r.qty}, ${r.costPerUnit}, ${totalCost}, ${r.receivedDate}, ${r.invoiceNumber ?? null}, ${r.notes ?? null})` console.log(` Receipt: ${r.sku} qty=${r.qty} @ $${r.costPerUnit}`) } console.log(' Inventory seed complete.') } async function seedLessons(sql: any) { console.log('\nSeeding lessons data...') // ── Clear old non-string lesson data ────────────────────────────────────── // Delete in dependency order so FK constraints don't block await sql`DELETE FROM lesson_plan_item` await sql`DELETE FROM lesson_plan_section` await sql`DELETE FROM member_lesson_plan` await sql`DELETE FROM lesson_plan_template_item` await sql`DELETE FROM lesson_plan_template_section` await sql`DELETE FROM lesson_plan_template` await sql`DELETE FROM lesson_session` await sql`DELETE FROM enrollment` await sql`DELETE FROM schedule_slot` await sql`DELETE FROM lesson_type` await sql`DELETE FROM instructor` console.log(' Cleared old lesson data') // ── Grading scale ────────────────────────────────────────────────────────── const [existingScale] = await sql`SELECT id FROM grading_scale WHERE name = 'Standard Progress'` let scaleId: string if (existingScale) { scaleId = existingScale.id } else { const [scale] = await sql` INSERT INTO grading_scale (name, description, is_default, is_active) VALUES ('Standard Progress', 'Four-level scale used across all instruments', true, true) RETURNING id` scaleId = scale.id const levels = [ { value: '1', label: 'Introduced', numericValue: 1, colorHex: null, sortOrder: 1 }, { value: '2', label: 'Developing', numericValue: 2, colorHex: '#EAB308', sortOrder: 2 }, { value: '3', label: 'Proficient', numericValue: 3, colorHex: '#3B82F6', sortOrder: 3 }, { value: '4', label: 'Mastered', numericValue: 4, colorHex: '#22C55E', sortOrder: 4 }, ] for (const lv of levels) { await sql`INSERT INTO grading_scale_level (grading_scale_id, value, label, numeric_value, color_hex, sort_order) VALUES (${scaleId}, ${lv.value}, ${lv.label}, ${lv.numericValue}, ${lv.colorHex}, ${lv.sortOrder})` } console.log(' Grading scale: Standard Progress') } // ── Instructors ──────────────────────────────────────────────────────────── const instructorDefs = [ { displayName: 'Sarah Mitchell', bio: 'Violin and viola instructor with 12 years of teaching experience. Suzuki-certified through Book 6. Member of the City Symphony Orchestra.', instruments: ['Violin', 'Viola'], }, { displayName: 'James Carter', bio: 'Cellist and chamber musician with a masters in performance from Eastman School of Music. Teaches all levels from beginner through pre-college.', instruments: ['Cello'], }, { displayName: 'Beth Romero', bio: 'Double bassist with 10 years of orchestral and jazz experience. Former section principal. Warm, patient teaching style for all ages.', instruments: ['Bass'], }, ] const instrIds: Record = {} for (const def of instructorDefs) { const [existing] = await sql`SELECT id FROM instructor WHERE display_name = ${def.displayName}` if (existing) { instrIds[def.displayName] = existing.id; continue } const [row] = await sql` INSERT INTO instructor (display_name, bio, instruments, is_active) VALUES (${def.displayName}, ${def.bio}, ${def.instruments}, true) RETURNING id` instrIds[def.displayName] = row.id console.log(` Instructor: ${def.displayName}`) } // ── Lesson types ─────────────────────────────────────────────────────────── const lessonTypeDefs = [ { name: '30-min Private Violin', instrument: 'Violin', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' }, { name: '45-min Private Violin', instrument: 'Violin', durationMinutes: 45, lessonFormat: 'private', rateWeekly: '50.00', rateMonthly: '175.00', rateQuarterly: '480.00' }, { name: '30-min Private Viola', instrument: 'Viola', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' }, { name: '30-min Private Cello', instrument: 'Cello', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' }, { name: '45-min Private Cello', instrument: 'Cello', durationMinutes: 45, lessonFormat: 'private', rateWeekly: '50.00', rateMonthly: '175.00', rateQuarterly: '480.00' }, { name: '30-min Private Bass', instrument: 'Bass', durationMinutes: 30, lessonFormat: 'private', rateWeekly: '35.00', rateMonthly: '120.00', rateQuarterly: '330.00' }, { name: '60-min String Ensemble', instrument: null, durationMinutes: 60, lessonFormat: 'group', rateWeekly: null, rateMonthly: '65.00', rateQuarterly: '180.00' }, ] const ltIds: Record = {} for (const lt of lessonTypeDefs) { const [existing] = await sql`SELECT id FROM lesson_type WHERE name = ${lt.name}` if (existing) { ltIds[lt.name] = existing.id; continue } const [row] = await sql` INSERT INTO lesson_type (name, instrument, duration_minutes, lesson_format, rate_weekly, rate_monthly, rate_quarterly, is_active) VALUES (${lt.name}, ${lt.instrument}, ${lt.durationMinutes}, ${lt.lessonFormat}, ${lt.rateWeekly}, ${lt.rateMonthly}, ${lt.rateQuarterly}, true) RETURNING id` ltIds[lt.name] = row.id console.log(` Lesson type: ${lt.name}`) } // ── Schedule slots ───────────────────────────────────────────────────────── // dayOfWeek: 0=Sun, 1=Mon, 2=Tue, 3=Wed, 4=Thu, 5=Fri, 6=Sat const slotDefs = [ // Sarah Mitchell — Violin & Viola { key: 'sarah_mon_1530_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 1, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null }, { key: 'sarah_mon_1600_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 1, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null }, { key: 'sarah_wed_1530_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 3, startTime: '15:30', room: 'Studio 1', maxStudents: 1, rateMonthly: null }, { key: 'sarah_wed_1600_vln45', instructor: 'Sarah Mitchell', lessonType: '45-min Private Violin', dayOfWeek: 3, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: '155.00' }, { key: 'sarah_fri_1600_vla30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Viola', dayOfWeek: 5, startTime: '16:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null }, { key: 'sarah_sat_1000_vln30', instructor: 'Sarah Mitchell', lessonType: '30-min Private Violin', dayOfWeek: 6, startTime: '10:00', room: 'Studio 1', maxStudents: 1, rateMonthly: null }, // James Carter — Cello { key: 'james_tue_1500_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 2, startTime: '15:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null }, { key: 'james_tue_1600_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 2, startTime: '16:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null }, { key: 'james_thu_1530_clo45', instructor: 'James Carter', lessonType: '45-min Private Cello', dayOfWeek: 4, startTime: '15:30', room: 'Studio 2', maxStudents: 1, rateMonthly: '155.00' }, { key: 'james_sat_1000_clo30', instructor: 'James Carter', lessonType: '30-min Private Cello', dayOfWeek: 6, startTime: '10:00', room: 'Studio 2', maxStudents: 1, rateMonthly: null }, // Beth Romero — Bass { key: 'beth_wed_1700_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 3, startTime: '17:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null }, { key: 'beth_fri_1500_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 5, startTime: '15:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null }, { key: 'beth_sat_1100_bas30', instructor: 'Beth Romero', lessonType: '30-min Private Bass', dayOfWeek: 6, startTime: '11:00', room: 'Studio 3', maxStudents: 1, rateMonthly: null }, // String Ensemble — Saturdays (all three instructors rotate supervision; assign to Sarah) { key: 'ensemble_sat_1300', instructor: 'Sarah Mitchell', lessonType: '60-min String Ensemble', dayOfWeek: 6, startTime: '13:00', room: 'Main Hall', maxStudents: 12, rateMonthly: null }, ] const slotIds: Record = {} for (const s of slotDefs) { const instrId = instrIds[s.instructor] const ltId = ltIds[s.lessonType] const [existing] = await sql` SELECT id FROM schedule_slot WHERE instructor_id = ${instrId} AND day_of_week = ${s.dayOfWeek} AND start_time = ${s.startTime + ':00'} AND is_active = true` if (existing) { slotIds[s.key] = existing.id; continue } const [row] = await sql` INSERT INTO schedule_slot (instructor_id, lesson_type_id, day_of_week, start_time, room, max_students, rate_monthly, is_active) VALUES (${instrId}, ${ltId}, ${s.dayOfWeek}, ${s.startTime}, ${s.room}, ${s.maxStudents}, ${s.rateMonthly}, true) RETURNING id` slotIds[s.key] = row.id console.log(` Slot: ${s.instructor} — ${s.lessonType} (${['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][s.dayOfWeek]} ${s.startTime})`) } // ── Look up member IDs ───────────────────────────────────────────────────── const memberRows = await sql`SELECT m.id, m.first_name, m.last_name, m.account_id FROM member m` const memberMap: Record = {} for (const m of memberRows) memberMap[`${m.first_name} ${m.last_name}`] = { id: m.id, accountId: m.account_id } // ── Enrollments ──────────────────────────────────────────────────────────── const enrollmentDefs = [ { member: 'Tommy Smith', slotKey: 'sarah_mon_1530_vln30', startDate: '2026-01-05', rate: '120.00', billingInterval: 1, billingUnit: 'month', notes: 'Tommy is 8 years old, complete beginner. Using a 1/2 size rental. Parents prefer Monday after school.', }, { member: 'Jake Johnson', slotKey: 'sarah_wed_1530_vln30', startDate: '2026-01-07', rate: '120.00', billingInterval: 1, billingUnit: 'month', notes: 'Jake (age 10) started Suzuki Book 1 at school. Moving to Book 2. Uses a 3/4 rental.', }, { member: 'Emily Chen', slotKey: 'sarah_fri_1600_vla30', startDate: '2026-01-09', rate: '120.00', billingInterval: 1, billingUnit: 'month', notes: 'Emily played violin through high school, now switching to viola. Strong reader, working on tone and shifting.', }, { member: 'Mike Thompson', slotKey: 'james_sat_1000_clo30', startDate: '2026-02-01', rate: '120.00', billingInterval: 1, billingUnit: 'month', notes: 'Mike is an adult beginner on cello. Very motivated. Renting a full-size cello.', }, { member: 'Carlos Garcia', slotKey: 'beth_fri_1500_bas30', startDate: '2026-01-09', rate: '120.00', billingInterval: 1, billingUnit: 'month', notes: 'Carlos played bass in high school orchestra. Returning student — solid fundamentals, working on thumb position.', }, ] const enrollmentIds: Record = {} for (const e of enrollmentDefs) { const m = memberMap[e.member] if (!m) { console.log(` ⚠ Member not found: ${e.member} — skipping enrollment`); continue } const slotId = slotIds[e.slotKey] if (!slotId) { console.log(` ⚠ Slot not found: ${e.slotKey} — skipping enrollment`); continue } const [slot] = await sql`SELECT instructor_id FROM schedule_slot WHERE id = ${slotId}` const [existing] = await sql` SELECT id FROM enrollment WHERE member_id = ${m.id} AND schedule_slot_id = ${slotId}` if (existing) { enrollmentIds[e.member] = existing.id; continue } const [row] = await sql` INSERT INTO enrollment (member_id, account_id, schedule_slot_id, instructor_id, status, start_date, rate, billing_interval, billing_unit, notes) VALUES (${m.id}, ${m.accountId}, ${slotId}, ${slot.instructor_id}, 'active', ${e.startDate}, ${e.rate}, ${e.billingInterval}, ${e.billingUnit}, ${e.notes}) RETURNING id` enrollmentIds[e.member] = row.id console.log(` Enrollment: ${e.member}`) } // ── Lesson sessions ──────────────────────────────────────────────────────── type SessionEntry = [string, string, string?] interface SessionSet { enrollmentKey: string; slotKey: string; entries: SessionEntry[] } const sessionSets: SessionSet[] = [ { enrollmentKey: 'Tommy Smith', slotKey: 'sarah_mon_1530_vln30', entries: [ ['2026-01-05', 'attended', 'First lesson. Introduced bow hold and open string bowing. Tommy is enthusiastic!'], ['2026-01-12', 'attended'], ['2026-01-19', 'attended', 'Bow arm improving. Introduced left hand finger placement — E and A strings.'], ['2026-01-26', 'missed'], ['2026-02-02', 'attended'], ['2026-02-09', 'attended', 'Playing Twinkle Variation A slowly with good intonation. Bow hold still needs reminders.'], ['2026-02-16', 'attended'], ['2026-02-23', 'attended'], ['2026-03-02', 'attended', 'Tommy played Twinkle Theme hands-down with confidence — great milestone!'], ['2026-03-09', 'attended'], ['2026-03-16', 'attended', 'Introduced Lightly Row. Working on smooth bow changes.'], ['2026-03-23', 'attended'], ['2026-03-30', 'scheduled'], ['2026-04-06', 'scheduled'], ['2026-04-13', 'scheduled'], ], }, { enrollmentKey: 'Jake Johnson', slotKey: 'sarah_wed_1530_vln30', entries: [ ['2026-01-07', 'attended', 'Assessment: solid Book 1 foundation. Starting Book 2 — Chorus from Judas Maccabeus.'], ['2026-01-14', 'attended'], ['2026-01-21', 'attended', 'Shifting to 3rd position introduced. Intonation on D string needs focus.'], ['2026-01-28', 'attended'], ['2026-02-04', 'missed'], ['2026-02-11', 'attended', 'Third position becoming more reliable. Vibrato exercises started.'], ['2026-02-18', 'attended'], ['2026-02-25', 'attended'], ['2026-03-04', 'attended', 'Chorus from Judas Maccabeus up to performance tempo. Vibrato emerging nicely.'], ['2026-03-11', 'attended'], ['2026-03-18', 'attended', 'Jake performed the Chorus at slow recital tempo — very clean. Starting Hunters\' Chorus.'], ['2026-03-25', 'attended'], ['2026-04-01', 'scheduled'], ['2026-04-08', 'scheduled'], ['2026-04-15', 'scheduled'], ], }, { enrollmentKey: 'Emily Chen', slotKey: 'sarah_fri_1600_vla30', entries: [ ['2026-01-09', 'attended', 'Assessment: violin background is strong. Adjusted chin rest and shoulder rest for viola. Alto clef introduced.'], ['2026-01-16', 'attended'], ['2026-01-23', 'attended', 'Alto clef reading improving quickly. Tone production on C string needs work — bowing angle adjustment.'], ['2026-01-30', 'attended'], ['2026-02-06', 'attended', 'Significantly richer tone on low strings. Started Telemann Viola Concerto in G major, 1st mvt.'], ['2026-02-13', 'attended'], ['2026-02-20', 'missed'], ['2026-02-27', 'attended'], ['2026-03-06', 'attended', 'Telemann 1st movement up to tempo. Emily\'s vibrato really coming through on viola — beautiful sound.'], ['2026-03-13', 'attended'], ['2026-03-20', 'attended', 'Starting 2nd movement. Discussing potential solo performance at spring recital.'], ['2026-03-27', 'attended'], ['2026-04-03', 'scheduled'], ['2026-04-10', 'scheduled'], ['2026-04-17', 'scheduled'], ], }, { enrollmentKey: 'Mike Thompson', slotKey: 'james_sat_1000_clo30', entries: [ ['2026-02-01', 'attended', 'First lesson — proper sitting posture, bow hold, open string bowing. Mike is a quick learner.'], ['2026-02-08', 'attended'], ['2026-02-15', 'attended', 'Left hand frame introduced. Thumb position on neck. Playing first simple melodies.'], ['2026-02-22', 'attended'], ['2026-03-01', 'attended', 'Bass clef reading going well. Playing "Lightly Row" in D major. Intonation on A string improving.'], ['2026-03-08', 'attended'], ['2026-03-15', 'attended', 'Mike nailed the first cello piece from Mooney\'s "Position Pieces." Really motivated — practices daily.'], ['2026-03-22', 'attended'], ['2026-03-29', 'attended', 'Starting Suzuki Cello Book 1 formally. Tone on D and A strings sounding great.'], ['2026-04-05', 'scheduled'], ['2026-04-12', 'scheduled'], ['2026-04-19', 'scheduled'], ], }, { enrollmentKey: 'Carlos Garcia', slotKey: 'beth_fri_1500_bas30', entries: [ ['2026-01-09', 'attended', 'Assessment — Carlos has solid orchestra foundation. Reviewing shifting, extensions, and bow distribution.'], ['2026-01-16', 'attended'], ['2026-01-23', 'attended', 'Thumb position work started. Carlos adapting well — good ear for intonation.'], ['2026-01-30', 'attended'], ['2026-02-06', 'attended', 'Working through Simandl Book 1 exercises in thumb position. Stamina building.'], ['2026-02-13', 'attended'], ['2026-02-20', 'attended', 'Thumb position solid up to 4th. Starting to work on Dragonetti Concerto opening.'], ['2026-02-27', 'attended'], ['2026-03-06', 'attended', 'Dragonetti opening phrase sounding musical. Bow arm and tone in upper positions much improved.'], ['2026-03-13', 'missed'], ['2026-03-20', 'attended'], ['2026-03-27', 'attended', 'Carlos brought in a jazz bassline he transcribed from Ray Brown — spent half the lesson on groove and feel. Excellent initiative.'], ['2026-04-03', 'scheduled'], ['2026-04-10', 'scheduled'], ['2026-04-17', 'scheduled'], ], }, ] for (const ss of sessionSets) { const enrollmentId = enrollmentIds[ss.enrollmentKey] if (!enrollmentId) continue const [slot] = await sql`SELECT start_time FROM schedule_slot WHERE id = ${slotIds[ss.slotKey]}` const scheduledTime = slot.start_time for (const [date, status, instructorNotes] of ss.entries) { const [existing] = await sql` SELECT id FROM lesson_session WHERE enrollment_id = ${enrollmentId} AND scheduled_date = ${date}` if (existing) continue await sql` INSERT INTO lesson_session (enrollment_id, scheduled_date, scheduled_time, status, instructor_notes, notes_completed_at) VALUES ( ${enrollmentId}, ${date}, ${scheduledTime}, ${status}, ${instructorNotes ?? null}, ${instructorNotes && status !== 'scheduled' ? new Date(date + 'T20:00:00Z').toISOString() : null} )` } console.log(` Sessions: ${ss.enrollmentKey} (${ss.entries.length})`) } // ── Lesson plan template — Beginner Violin (Suzuki Book 1) ───────────────── const [existingTemplate] = await sql`SELECT id FROM lesson_plan_template WHERE name = 'Beginner Violin — Suzuki Book 1'` if (!existingTemplate) { const [tmpl] = await sql` INSERT INTO lesson_plan_template (name, description, instrument, skill_level, is_active) VALUES ( 'Beginner Violin — Suzuki Book 1', 'Core skills for first-year violin students following the Suzuki method — from bow hold and posture through the full Book 1 repertoire.', 'Violin', 'beginner', true ) RETURNING id` const sections = [ { title: 'Posture & Setup', sortOrder: 0, items: [ { title: 'Proper standing/sitting posture', sortOrder: 0 }, { title: 'Violin hold and chin rest position', sortOrder: 1 }, { title: 'Shoulder rest adjustment', sortOrder: 2 }, { title: 'Bow hold (Suzuki style)', sortOrder: 3 }, { title: 'Relaxed arm weight into string', sortOrder: 4 }, ], }, { title: 'Bowing Technique', sortOrder: 1, items: [ { title: 'Open string bowing — all four strings', sortOrder: 0 }, { title: 'Straight bow path', sortOrder: 1 }, { title: 'Smooth bow changes at frog and tip', sortOrder: 2 }, { title: 'Detaché stroke', sortOrder: 3 }, { title: 'Staccato introduction', sortOrder: 4 }, ], }, { title: 'Left Hand & Intonation', sortOrder: 2, items: [ { title: 'Finger placement — E and A strings', sortOrder: 0 }, { title: 'Finger placement — D and G strings', sortOrder: 1 }, { title: 'First finger frame', sortOrder: 2 }, { title: 'Low 2nd finger (B♭, E♭)', sortOrder: 3 }, { title: 'High 3rd finger', sortOrder: 4 }, ], }, { title: 'Suzuki Book 1 Repertoire', sortOrder: 3, items: [ { title: 'Twinkle Twinkle — Variation A', sortOrder: 0 }, { title: 'Twinkle Twinkle — Theme', sortOrder: 1 }, { title: 'Lightly Row', sortOrder: 2 }, { title: 'Song of the Wind', sortOrder: 3 }, { title: 'Go Tell Aunt Rhody', sortOrder: 4 }, { title: 'O Come Little Children', sortOrder: 5 }, { title: 'May Song', sortOrder: 6 }, { title: 'Long, Long Ago', sortOrder: 7 }, { title: 'Allegro (Suzuki)', sortOrder: 8 }, { title: 'Perpetual Motion', sortOrder: 9 }, { title: 'Allegretto (Suzuki)', sortOrder: 10 }, ], }, ] for (const sec of sections) { const [s] = await sql` INSERT INTO lesson_plan_template_section (template_id, title, sort_order) VALUES (${tmpl.id}, ${sec.title}, ${sec.sortOrder}) RETURNING id` for (const item of sec.items) { await sql` INSERT INTO lesson_plan_template_item (section_id, title, grading_scale_id, sort_order) VALUES (${s.id}, ${item.title}, ${scaleId}, ${item.sortOrder})` } } console.log(' Template: Beginner Violin — Suzuki Book 1') } // ── Lesson plan for Tommy Smith (violin beginner) ────────────────────────── const tommyEnrollmentId = enrollmentIds['Tommy Smith'] const tommyMember = memberMap['Tommy Smith'] if (tommyEnrollmentId && tommyMember) { const [existingPlan] = await sql`SELECT id FROM member_lesson_plan WHERE enrollment_id = ${tommyEnrollmentId} AND is_active = true` if (!existingPlan) { const [plan] = await sql` INSERT INTO member_lesson_plan (member_id, enrollment_id, title, description, is_active, started_date) VALUES (${tommyMember.id}, ${tommyEnrollmentId}, 'Violin Suzuki Book 1 — Tommy Smith', 'Working through Suzuki Book 1.', true, '2026-01-05') RETURNING id` const [sec1] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Posture & Setup', 0) RETURNING id` for (const it of [ { title: 'Proper standing/sitting posture', status: 'mastered', sortOrder: 0 }, { title: 'Violin hold and chin rest position', status: 'mastered', sortOrder: 1 }, { title: 'Shoulder rest adjustment', status: 'mastered', sortOrder: 2 }, { title: 'Bow hold (Suzuki style)', status: 'in_progress', sortOrder: 3 }, { title: 'Relaxed arm weight into string', status: 'in_progress', sortOrder: 4 }, ]) { await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order) VALUES (${sec1.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})` } const [sec2] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Bowing Technique', 1) RETURNING id` for (const it of [ { title: 'Open string bowing — all four strings', status: 'mastered', sortOrder: 0 }, { title: 'Straight bow path', status: 'in_progress', sortOrder: 1 }, { title: 'Smooth bow changes at frog and tip', status: 'in_progress', sortOrder: 2 }, { title: 'Detaché stroke', status: 'not_started', sortOrder: 3 }, { title: 'Staccato introduction', status: 'not_started', sortOrder: 4 }, ]) { await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order) VALUES (${sec2.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})` } const [sec3] = await sql`INSERT INTO lesson_plan_section (lesson_plan_id, title, sort_order) VALUES (${plan.id}, 'Suzuki Book 1 Repertoire', 2) RETURNING id` for (const it of [ { title: 'Twinkle Twinkle — Variation A', status: 'mastered', sortOrder: 0 }, { title: 'Twinkle Twinkle — Theme', status: 'in_progress', sortOrder: 1 }, { title: 'Lightly Row', status: 'in_progress', sortOrder: 2 }, { title: 'Song of the Wind', status: 'not_started', sortOrder: 3 }, { title: 'Go Tell Aunt Rhody', status: 'not_started', sortOrder: 4 }, ]) { await sql`INSERT INTO lesson_plan_item (section_id, title, status, grading_scale_id, current_grade_value, sort_order) VALUES (${sec3.id}, ${it.title}, ${it.status}, ${scaleId}, ${it.status === 'mastered' ? '4' : it.status === 'in_progress' ? '2' : null}, ${it.sortOrder})` } console.log(' Lesson plan: Tommy Smith — Violin Suzuki Book 1') } } console.log('Lessons seed complete.') } seed().catch((err) => { console.error('Seed failed:', err) process.exit(1) })