fix: make music store seed self-contained, remove non-string instruments

- Seed now bootstraps company, location, RBAC, admin user, modules, and
  default register — no dev-seed dependency
- Admin: admin@harmonymusic.com / admin1234 (POS: 10011234)
- Added 10 music-focused accounts and 16 members (families, individuals,
  schools, orchestra)
- Removed all guitar, brass, and woodwind templates and repair tickets
- Added string-specific templates (fingerboard planing, varnish touch-up,
  neck reset, bass bar replacement, tailgut replacement)
- School batch changed from band instruments to string orchestra instruments
- All repair tickets now reference string instruments only

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-05 02:35:04 +00:00
parent 663f00b099
commit ac264eecc0

View File

@@ -15,16 +15,139 @@ const sql = postgres(DB_URL)
async function seed() {
console.log('Seeding music store data...')
// Verify company exists (dev-seed must run first)
const [company] = await sql`SELECT id FROM company WHERE id = ${COMPANY_ID}`
if (!company) {
console.error('Company not found — run dev-seed first: bun run db:seed-dev')
process.exit(1)
// --- 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')
}
// Update company name to music store
await sql`UPDATE company SET name = 'Harmony Music Shop' WHERE id = ${COMPANY_ID}`
console.log(' Updated company name: 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 = [
@@ -55,42 +178,17 @@ async function seed() {
{ 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' },
// Guitar
{ name: 'String Change', itemCategory: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '25.00', cost: '8.00' },
{ name: 'String Change', itemCategory: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '25.00', cost: '7.00' },
{ name: 'String Change', itemCategory: 'Guitar', size: 'Classical', itemType: 'flat_rate', price: '25.00', cost: '10.00' },
{ name: 'Full Setup', itemCategory: 'Guitar', size: 'Acoustic', itemType: 'flat_rate', price: '65.00', cost: '5.00' },
{ name: 'Full Setup', itemCategory: 'Guitar', size: 'Electric', itemType: 'flat_rate', price: '65.00', cost: '5.00' },
{ name: 'Fret Level & Crown', itemCategory: 'Guitar', size: null, itemType: 'labor', price: '150.00', cost: null },
{ name: 'Pickup Installation', itemCategory: 'Guitar', size: null, itemType: 'labor', price: '45.00', cost: null },
{ name: 'Nut Replacement', itemCategory: 'Guitar', size: null, itemType: 'flat_rate', price: '35.00', cost: '8.00' },
{ name: 'Tuning Machine Replacement', itemCategory: 'Guitar', size: null, itemType: 'flat_rate', price: '40.00', cost: '15.00' },
// Brass
{ name: 'Valve Overhaul', itemCategory: 'Trumpet', size: null, itemType: 'labor', price: '85.00', cost: null },
{ name: 'Chemical Cleaning', itemCategory: 'Trumpet', size: null, itemType: 'flat_rate', price: '55.00', cost: '10.00' },
{ name: 'Dent Removal', itemCategory: 'Trumpet', size: null, itemType: 'labor', price: '50.00', cost: null },
{ name: 'Slide Repair', itemCategory: 'Trombone', size: null, itemType: 'labor', price: '75.00', cost: null },
{ name: 'Chemical Cleaning', itemCategory: 'Trombone', size: null, itemType: 'flat_rate', price: '65.00', cost: '12.00' },
{ name: 'Dent Removal', itemCategory: 'Trombone', size: null, itemType: 'labor', price: '60.00', cost: null },
{ name: 'Valve Overhaul', itemCategory: 'French Horn', size: null, itemType: 'labor', price: '120.00', cost: null },
{ name: 'Chemical Cleaning', itemCategory: 'French Horn', size: null, itemType: 'flat_rate', price: '75.00', cost: '15.00' },
{ name: 'Valve Overhaul', itemCategory: 'Tuba', size: null, itemType: 'labor', price: '150.00', cost: null },
// Woodwinds
{ name: 'Pad Replacement', itemCategory: 'Clarinet', size: null, itemType: 'flat_rate', price: '120.00', cost: '30.00' },
{ name: 'Cork Replacement', itemCategory: 'Clarinet', size: null, itemType: 'flat_rate', price: '45.00', cost: '5.00' },
{ name: 'Key Adjustment', itemCategory: 'Clarinet', size: null, itemType: 'labor', price: '35.00', cost: null },
{ name: 'Pad Replacement', itemCategory: 'Flute', size: null, itemType: 'flat_rate', price: '110.00', cost: '25.00' },
{ name: 'Headjoint Cork', itemCategory: 'Flute', size: null, itemType: 'flat_rate', price: '30.00', cost: '5.00' },
{ name: 'Key Adjustment', itemCategory: 'Flute', size: null, itemType: 'labor', price: '35.00', cost: null },
{ name: 'Pad Replacement', itemCategory: 'Saxophone', size: 'Alto', itemType: 'flat_rate', price: '150.00', cost: '40.00' },
{ name: 'Pad Replacement', itemCategory: 'Saxophone', size: 'Tenor', itemType: 'flat_rate', price: '175.00', cost: '50.00' },
{ name: 'Cork & Felt Replacement', itemCategory: 'Saxophone', size: null, itemType: 'flat_rate', price: '65.00', cost: '10.00' },
{ name: 'Neck Cork', itemCategory: 'Saxophone', size: null, itemType: 'flat_rate', price: '20.00', cost: '3.00' },
{ name: 'Pad Replacement', itemCategory: 'Oboe', size: null, itemType: 'flat_rate', price: '200.00', cost: '60.00' },
{ name: 'Reed Adjustment', itemCategory: 'Oboe', size: null, itemType: 'labor', price: '15.00', cost: null },
{ name: 'Pad Replacement', itemCategory: 'Bassoon', size: null, itemType: 'flat_rate', price: '250.00', cost: '80.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' },
@@ -112,19 +210,15 @@ async function seed() {
// First clear any generic tickets
await sql`DELETE FROM repair_ticket WHERE id NOT IN (SELECT DISTINCT repair_ticket_id FROM repair_note)`
const acctRows = await sql`SELECT id, name FROM account`
const acctIds: Record<string, string> = {}
for (const a of acctRows) acctIds[a.name] = a.id
const tickets = [
{ customer: 'Mike Thompson', item: 'Fender Stratocaster', serial: 'US22-045891', problem: 'Fret buzz on 3rd and 5th fret, needs full setup', condition: 'good', status: 'in_progress', estimate: '65.00' },
{ customer: 'Emily Chen', item: 'Yamaha YTR-2330 Trumpet', serial: 'YTR-78432', problem: 'Stuck 2nd valve, sluggish action on all valves', condition: 'fair', status: 'pending_approval', estimate: '85.00' },
{ 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' },
{ 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' },
{ 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' },
{ customer: 'Carlos Garcia', item: 'Martin D-28 Acoustic Guitar', serial: 'M2284563', problem: 'Broken tuning peg, needs replacement', condition: 'good', status: 'new', estimate: null },
{ customer: 'Lisa Johnson', item: 'Yamaha YCL-255 Clarinet', serial: null, problem: 'Several pads worn, keys sticking', condition: 'poor', status: 'diagnosing', estimate: null },
{ customer: 'Walk-In Customer', item: 'Gemeinhardt 2SP Flute', serial: null, problem: 'Squeaks on high notes, headjoint cork may need replacing', condition: 'fair', status: 'intake', estimate: null },
{ 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 },
{ 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 },
{ 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 },
{ customer: 'Smith Family', item: 'Suzuki Student Violin 1/2', serial: null, problem: 'Pegs slipping, bridge leaning forward', condition: 'fair', status: 'new', estimate: null },
{ customer: 'Johnson Family', item: 'Selmer AS500 Alto Saxophone', serial: 'AS-99231', problem: 'Neck cork loose, low notes not speaking', condition: 'good', status: 'in_progress', estimate: '85.00' },
{ 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' },
]
for (const t of tickets) {
@@ -136,21 +230,21 @@ async function seed() {
console.log(` Ticket: ${t.customer}${t.item} [${t.status}]`)
}
// --- School Band Batch ---
const batchExists = await sql`SELECT id FROM repair_batch WHERE contact_name = 'Mr. Williams'`
// --- School Orchestra Batch ---
const batchExists = await sql`SELECT id FROM repair_batch WHERE contact_name = 'Ms. Park'`
if (batchExists.length === 0) {
const schoolId = acctIds['Lincoln High School']
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}, 'Mr. Williams', '555-0210', 'williams@lincoln.edu', 6, 'Annual band instrument checkup — 6 instruments for fall semester', 'intake') RETURNING id`
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 Flute — Gemeinhardt 2SP', problem: 'Pads worn, headjoint cork needs check', condition: 'fair' },
{ item: 'Student Clarinet — Yamaha YCL-255', problem: 'Keys sticking, barrel cork dried out', condition: 'fair' },
{ item: 'Student Clarinet — Buffet B12', problem: 'Barrel crack, needs assessment', condition: 'poor' },
{ item: 'Student Trumpet — Bach TR300', problem: 'Valve alignment off, general cleaning needed', condition: 'good' },
{ item: 'Student Trombone — Yamaha YSL-354', problem: 'Slide dent near bell, sluggish movement', condition: 'fair' },
{ item: 'Student Alto Sax — Selmer AS500', problem: 'Neck cork loose, octave key sticky', condition: 'fair' },
{ 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) {
@@ -158,7 +252,7 @@ async function seed() {
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: Lincoln High School — 6 instruments')
console.log(' Batch: Oak Elementary — 6 string instruments')
}
}