Files
lunarfront-app/packages/backend/src/db/seeds/music-store-seed.ts
ryan 785071e5fd fix: add line items to repair tickets in music store seed
Tickets with work in progress or ready for pickup now have realistic
line items (labor, parts, flat rates, consumables). The ready ticket
(David Smith — Violin) has billable items for POS checkout testing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:05:20 +00:00

1525 lines
97 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, string> = {}
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<string, { id: string }> = {}
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<string, string> = {}
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<string, string> = {}
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 710.',
},
{
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 57.',
},
{
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 46.',
},
{
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 912.',
},
{
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 710.',
},
{
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/44/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<string, string> = {}
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<string, string> = {}
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<string, string> = {}
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<string, string> = {}
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<string, { id: string; accountId: string }> = {}
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<string, string> = {}
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)
})