import { PrismaClient } from '@prisma/client'; import { randomUUID } from 'crypto'; import * as bcrypt from 'bcrypt'; // Default permission matrices for system roles const MODULES = [ 'dashboard', 'clients', 'subscriptions', 'invoices', 'payments', 'tickets', 'employees', 'payroll', 'expenses', 'assets', 'accounts', 'fund_transfers', 'accounting', 'reports', 'areas', 'plans', 'settings', 'users', ] as const; type Module = (typeof MODULES)[number]; interface PermRow { module: Module; canView: boolean; canCreate: boolean; canUpdate: boolean; canArchive: boolean; canApprove: boolean; canExport: boolean; } const DEFAULT_ROLES: { name: string; slug: string; description: string; perms: PermRow[] }[] = [ { name: 'Tenant Admin', slug: 'tenant_admin', description: 'Full access to all modules', perms: MODULES.map((m) => ({ module: m, canView: true, canCreate: true, canUpdate: true, canArchive: true, canApprove: true, canExport: true })), }, { name: 'Manager', slug: 'manager', description: 'Operational management with approval rights', perms: MODULES.map((m) => { const noAccess: Module[] = ['users']; const viewOnly: Module[] = ['dashboard', 'accounting', 'settings']; if (noAccess.includes(m)) return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false }; if (viewOnly.includes(m)) return { module: m, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: m === 'accounting' }; return { module: m, canView: true, canCreate: true, canUpdate: true, canArchive: true, canApprove: ['invoices', 'payments', 'expenses', 'payroll', 'fund_transfers'].includes(m), canExport: true }; }), }, { name: 'Technician', slug: 'technician', description: 'Field operations: tickets, payments, client/invoice viewing', perms: MODULES.map((m) => { const viewOnly: Module[] = ['clients', 'subscriptions', 'invoices', 'dashboard']; const fullAccess: Module[] = ['tickets', 'payments']; if (viewOnly.includes(m)) return { module: m, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false }; if (fullAccess.includes(m)) return { module: m, canView: true, canCreate: true, canUpdate: true, canArchive: false, canApprove: false, canExport: false }; return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false }; }), }, { name: 'Collector', slug: 'collector', description: 'Payment collection and client viewing', perms: MODULES.map((m) => { const canWrite: Module[] = ['payments']; const canViewMods: Module[] = ['dashboard', 'clients', 'invoices', 'payments']; if (!canViewMods.includes(m)) return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false }; return { module: m, canView: true, canCreate: canWrite.includes(m), canUpdate: false, canArchive: false, canApprove: false, canExport: false }; }), }, ]; const prisma = new PrismaClient(); async function hashPassword(password: string): Promise { return bcrypt.hash(password, 12); } async function main() { console.log('Seeding database...'); // Clean existing data (order matters for FK constraints) await prisma.journalLine.deleteMany(); await prisma.journalEntry.deleteMany(); await prisma.chartOfAccount.deleteMany(); await prisma.billingSetting.deleteMany(); await prisma.fundTransfer.deleteMany(); await prisma.companyAccount.deleteMany(); await prisma.asset.deleteMany(); await prisma.expense.deleteMany(); await prisma.payslip.deleteMany(); await prisma.payrollRun.deleteMany(); await prisma.recurringExpense.deleteMany(); await prisma.employee.deleteMany(); await prisma.notification.deleteMany(); await prisma.remittancePayment.deleteMany(); await prisma.remittance.deleteMany(); await prisma.auditLog.deleteMany(); await prisma.payment.deleteMany(); await prisma.invoice.deleteMany(); await prisma.ticket.deleteMany(); await prisma.subscription.deleteMany(); await prisma.client.deleteMany(); await prisma.plan.deleteMany(); await prisma.area.deleteMany(); await prisma.refreshToken.deleteMany(); await prisma.userTenantRole.deleteMany(); await prisma.rolePermission.deleteMany(); await prisma.tenantRole.deleteMany(); await prisma.userRole.deleteMany(); await prisma.user.deleteMany(); await prisma.tenant.deleteMany(); // ─── Tenant ──────────────────────────────────────────── const tenant = await prisma.tenant.create({ data: { id: randomUUID(), name: 'FiberNet Philippines', slug: 'fibernet-ph', settings: { companyName: 'FiberNet Philippines Inc.', currency: 'PHP', timezone: 'Asia/Manila' }, }, }); console.log(`Tenant: ${tenant.name}`); // ─── Super Admin (platform-level, no tenant) ─────────── const superAdmin = await prisma.user.create({ data: { tenantId: null, email: 'superadmin@fiberops.dev', password: await hashPassword('admin123!'), firstName: 'Super', lastName: 'Admin' }, }); await prisma.userRole.create({ data: { userId: superAdmin.id, role: 'super_admin' } }); console.log(`Super Admin: superadmin@fiberops.dev (super_admin)`); // ─── Tenant Users ───────────────────────────────────── const users: Record = {}; const usersByEmail: Record = {}; const userDefs = [ { email: 'admin@demo-isp.com', first: 'Admin', last: 'User', role: 'tenant_admin' }, { email: 'manager@demo-isp.com', first: 'Maria', last: 'Reyes', role: 'manager' }, { email: 'collector@demo-isp.com', first: 'Juan', last: 'Santos', role: 'collector' }, { email: 'tech@demo-isp.com', first: 'Pedro', last: 'Cruz', role: 'technician' }, { email: 'tech2@demo-isp.com', first: 'Jose', last: 'Garcia', role: 'technician' }, { email: 'viewer@demo-isp.com', first: 'Ana', last: 'Lopez', role: 'viewer' }, ]; for (const u of userDefs) { const user = await prisma.user.create({ data: { tenantId: tenant.id, email: u.email, password: await hashPassword('admin123!'), firstName: u.first, lastName: u.last }, }); await prisma.userRole.create({ data: { userId: user.id, role: u.role } }); users[u.role] = user; usersByEmail[u.email] = user; console.log(`User: ${u.email} (${u.role})`); } // ─── Default Tenant Roles ────────────────────────────── const tenantRoles: Record = {}; for (const def of DEFAULT_ROLES) { const role = await prisma.tenantRole.create({ data: { tenantId: tenant.id, name: def.name, slug: def.slug, description: def.description, isSystem: true, permissions: { create: def.perms.map((p) => ({ module: p.module, canView: p.canView, canCreate: p.canCreate, canUpdate: p.canUpdate, canArchive: p.canArchive, canApprove: p.canApprove, canExport: p.canExport, })), }, }, }); tenantRoles[def.slug] = role; } console.log(`Tenant Roles: ${DEFAULT_ROLES.length} (${DEFAULT_ROLES.map((r) => r.slug).join(', ')})`); // ─── Assign Tenant Roles to Users ───────────────────── // Map of old role names to new tenant role slugs const userRoleMap: Record = { tenant_admin: 'tenant_admin', manager: 'manager', technician: 'technician', viewer: 'collector', // viewer user gets collector role for demo }; // Assign roles from the map for (const [oldRole, newRoleSlug] of Object.entries(userRoleMap)) { if (users[oldRole] && tenantRoles[newRoleSlug]) { await prisma.userTenantRole.create({ data: { userId: users[oldRole].id, tenantRoleId: tenantRoles[newRoleSlug].id }, }); } } // Additional assignments: // - Assign technician users (collector, tech, tech2) to technician role // - Assign viewer user to collector role const technicianUsers = ['collector', 'tech', 'tech2']; for (const email of technicianUsers) { const userEmail = `${email}@demo-isp.com`; const user = usersByEmail[userEmail]; if (user) { const existing = await prisma.userTenantRole.findFirst({ where: { userId: user.id } }); if (!existing) { await prisma.userTenantRole.create({ data: { userId: user.id, tenantRoleId: tenantRoles['technician'].id }, }); } } } const viewerUserEmail = 'viewer@demo-isp.com'; const viewerUser = usersByEmail[viewerUserEmail]; if (viewerUser) { const existing = await prisma.userTenantRole.findFirst({ where: { userId: viewerUser.id } }); if (!existing) { await prisma.userTenantRole.create({ data: { userId: viewerUser.id, tenantRoleId: tenantRoles['collector'].id }, }); } } console.log('User-TenantRole assignments complete'); // ─── Areas ───────────────────────────────────────────── const areas = await Promise.all([ prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 1 - Centro', description: 'Town center, commercial area' } }), prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 2 - Poblacion', description: 'Residential zone near market' } }), prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 3 - San Isidro', description: 'Agricultural and residential' } }), prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 4 - Riverside', description: 'River-side residential' } }), prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 5 - Hilltop', description: 'Elevated residential subdivision' } }), ]); console.log(`Areas: ${areas.length}`); // ─── Plans ───────────────────────────────────────────── const plans = await Promise.all([ prisma.plan.create({ data: { tenantId: tenant.id, name: 'Lite 15', description: 'Entry-level 15 Mbps', speedDown: 15, speedUp: 15, price: 699, billingCycle: 30 } }), prisma.plan.create({ data: { tenantId: tenant.id, name: 'Basic 25', description: '25 Mbps residential', speedDown: 25, speedUp: 25, price: 999, billingCycle: 30 } }), prisma.plan.create({ data: { tenantId: tenant.id, name: 'Standard 50', description: '50 Mbps residential', speedDown: 50, speedUp: 50, price: 1499, billingCycle: 30 } }), prisma.plan.create({ data: { tenantId: tenant.id, name: 'Premium 100', description: '100 Mbps business', speedDown: 100, speedUp: 100, price: 2499, billingCycle: 30 } }), prisma.plan.create({ data: { tenantId: tenant.id, name: 'Enterprise 200', description: '200 Mbps dedicated', speedDown: 200, speedUp: 200, price: 4999, billingCycle: 30 } }), ]); console.log(`Plans: ${plans.length}`); // ─── Clients (20 clients across various areas/plans) ── // Area center coordinates (Lipa City, Batangas area) const areaCoords: [number, number][] = [ [14.0785, 121.1760], // Barangay 1 - Centro [14.0820, 121.1800], // Barangay 2 - Poblacion [14.0850, 121.1700], // Barangay 3 - San Isidro [14.0750, 121.1720], // Barangay 4 - Riverside [14.0900, 121.1780], // Barangay 5 - Hilltop ]; const clientDefs = [ { first: 'Juan', last: 'Dela Cruz', phone: '09171234567', email: 'juan@email.com', address: '123 Rizal St, Centro', area: 0, plan: 1, type: 'postpaid', latOff: 0.001, lngOff: 0.002 }, { first: 'Maria', last: 'Santos', phone: '09181234567', email: 'maria@email.com', address: '456 Mabini St, Centro', area: 0, plan: 2, type: 'postpaid', latOff: -0.002, lngOff: 0.001 }, { first: 'Jose', last: 'Garcia', phone: '09191234567', email: 'jose@email.com', address: '789 Bonifacio St, Poblacion', area: 1, plan: 1, type: 'prepaid', latOff: 0.003, lngOff: -0.001 }, { first: 'Ana', last: 'Reyes', phone: '09201234567', email: 'ana@email.com', address: '12 Luna St, Poblacion', area: 1, plan: 2, type: 'postpaid', latOff: -0.001, lngOff: 0.003 }, { first: 'Pedro', last: 'Aquino', phone: '09211234567', email: null, address: '34 Del Pilar St, San Isidro', area: 2, plan: 0, type: 'prepaid', latOff: 0.002, lngOff: -0.002 }, { first: 'Rosa', last: 'Mendoza', phone: '09221234567', email: 'rosa@email.com', address: '56 Quezon Ave, San Isidro', area: 2, plan: 3, type: 'postpaid', latOff: -0.003, lngOff: 0.001 }, { first: 'Carlos', last: 'Bautista', phone: '09231234567', email: null, address: '78 Magsaysay Blvd, Riverside', area: 3, plan: 1, type: 'postpaid', latOff: 0.001, lngOff: 0.002 }, { first: 'Elena', last: 'Villanueva', phone: '09241234567', email: 'elena@email.com', address: '90 Roxas St, Riverside', area: 3, plan: 2, type: 'postpaid', latOff: -0.002, lngOff: -0.003 }, { first: 'Roberto', last: 'Tan', phone: '09251234567', email: 'roberto@email.com', address: '11 Laurel St, Hilltop', area: 4, plan: 4, type: 'postpaid', latOff: 0.002, lngOff: 0.001 }, { first: 'Carmen', last: 'Lim', phone: '09261234567', email: 'carmen@email.com', address: '22 Osmena Ave, Hilltop', area: 4, plan: 3, type: 'postpaid', latOff: -0.001, lngOff: 0.002 }, { first: 'Miguel', last: 'Ramos', phone: '09271234567', email: null, address: '33 Aguinaldo St, Centro', area: 0, plan: 1, type: 'prepaid', latOff: 0.003, lngOff: -0.001 }, { first: 'Isabel', last: 'Torres', phone: '09281234567', email: 'isabel@email.com', address: '44 Andres Blvd, Poblacion', area: 1, plan: 0, type: 'postpaid', latOff: -0.002, lngOff: 0.003 }, { first: 'Ricardo', last: 'Flores', phone: '09291234567', email: null, address: '55 Katipunan Rd, San Isidro', area: 2, plan: 2, type: 'postpaid', latOff: 0.001, lngOff: -0.002 }, { first: 'Teresa', last: 'Navarro', phone: '09301234567', email: 'teresa@email.com', address: '66 Makabayan St, Riverside', area: 3, plan: 1, type: 'prepaid', latOff: -0.003, lngOff: 0.001 }, { first: 'Fernando', last: 'Castillo', phone: '09311234567', email: null, address: '77 Silang Blvd, Hilltop', area: 4, plan: 2, type: 'postpaid', latOff: 0.002, lngOff: -0.001 }, // New signups — pending installation (no lat/lng, pending status, open tickets) { first: 'Rafael', last: 'Dimaculangan', phone: '09321234567', email: null, address: '88 Burgos St, Centro', area: 0, plan: 1, type: 'postpaid', latOff: 0.0015, lngOff: -0.001 }, { first: 'Lorna', last: 'Perez', phone: '09331234567', email: 'lorna@email.com', address: '99 Villareal St, Poblacion', area: 1, plan: 2, type: 'postpaid', latOff: -0.001, lngOff: 0.002 }, { first: 'Danilo', last: 'Rivera', phone: '09341234567', email: null, address: '101 Kapitan St, San Isidro', area: 2, plan: 0, type: 'prepaid', latOff: 0.002, lngOff: 0.001 }, { first: 'Grace', last: 'Sison', phone: '09351234567', email: 'grace@email.com', address: '202 Magdalena St, Riverside', area: 3, plan: 3, type: 'postpaid', latOff: -0.001, lngOff: -0.002 }, { first: 'Allan', last: 'Vergara', phone: '09361234567', email: null, address: '303 Gomez St, Hilltop', area: 4, plan: 2, type: 'postpaid', latOff: 0.001, lngOff: 0.0015 }, // Additional clients for richer testing { first: 'Angelo', last: 'Manalo', phone: '09371234567', email: 'angelo@email.com', address: '15 Rizal Ave, Centro', area: 0, plan: 3, type: 'postpaid', latOff: 0.0025, lngOff: -0.0015 }, { first: 'Bella', last: 'Cruz', phone: '09381234567', email: 'bella@email.com', address: '26 Mabini Ext, Poblacion', area: 1, plan: 1, type: 'postpaid', latOff: -0.0015, lngOff: 0.002 }, { first: 'Claudio', last: 'Diaz', phone: '09391234567', email: null, address: '37 Bonifacio Rd, San Isidro', area: 2, plan: 4, type: 'postpaid', latOff: 0.001, lngOff: 0.003 }, { first: 'Diana', last: 'Espiritu', phone: '09401234567', email: 'diana@email.com', address: '48 Luna Ext, Riverside', area: 3, plan: 0, type: 'prepaid', latOff: -0.002, lngOff: -0.001 }, { first: 'Eduardo', last: 'Fernandez', phone: '09411234567', email: null, address: '59 Del Pilar St, Hilltop', area: 4, plan: 2, type: 'postpaid', latOff: 0.003, lngOff: 0.001 }, { first: 'Flora', last: 'Gonzales', phone: '09421234567', email: 'flora@email.com', address: '60 Quezon Blvd, Centro', area: 0, plan: 1, type: 'postpaid', latOff: -0.001, lngOff: -0.002 }, { first: 'Gilbert', last: 'Hernandez', phone: '09431234567', email: null, address: '71 Magsaysay St, Poblacion', area: 1, plan: 3, type: 'postpaid', latOff: 0.002, lngOff: 0.001 }, { first: 'Helen', last: 'Ibañez', phone: '09441234567', email: 'helen@email.com', address: '82 Roxas Blvd, San Isidro', area: 2, plan: 2, type: 'postpaid', latOff: -0.003, lngOff: -0.001 }, { first: 'Ivan', last: 'Jimenez', phone: '09451234567', email: null, address: '93 Laurel Ave, Riverside', area: 3, plan: 4, type: 'postpaid', latOff: 0.0015, lngOff: 0.002 }, { first: 'Julia', last: 'Kho', phone: '09461234567', email: 'julia@email.com', address: '104 Osmena St, Hilltop', area: 4, plan: 1, type: 'prepaid', latOff: -0.002, lngOff: 0.0015 }, { first: 'Kenneth', last: 'Lopez', phone: '09471234567', email: null, address: '115 Aguinaldo Blvd, Centro', area: 0, plan: 2, type: 'postpaid', latOff: 0.001, lngOff: -0.003 }, { first: 'Linda', last: 'Madrid', phone: '09481234567', email: 'linda@email.com', address: '126 Andres St, Poblacion', area: 1, plan: 0, type: 'postpaid', latOff: -0.0015, lngOff: 0.002 }, { first: 'Mario', last: 'Ng', phone: '09491234567', email: null, address: '137 Katipunan Rd, San Isidro', area: 2, plan: 3, type: 'postpaid', latOff: 0.002, lngOff: -0.002 }, { first: 'Nancy', last: 'Ong', phone: '09501234567', email: 'nancy@email.com', address: '148 Makabayan Blvd, Riverside', area: 3, plan: 2, type: 'postpaid', latOff: -0.001, lngOff: 0.003 }, // Additional new signups — pending installation { first: 'Oscar', last: 'Pineda', phone: '09511234567', email: 'oscar@email.com', address: '159 Rizal Ext, Centro', area: 0, plan: 2, type: 'postpaid', latOff: 0.001, lngOff: -0.002 }, { first: 'Patricia', last: 'Quintos', phone: '09521234567', email: 'patricia@email.com', address: '170 Mabini Rd, Poblacion', area: 1, plan: 1, type: 'postpaid', latOff: -0.002, lngOff: 0.001 }, { first: 'Quentin', last: 'Reyes Jr', phone: '09531234567', email: null, address: '181 Bonifacio Ext, San Isidro', area: 2, plan: 3, type: 'postpaid', latOff: 0.002, lngOff: 0.002 }, { first: 'Rita', last: 'Santillan', phone: '09541234567', email: 'rita@email.com', address: '192 Luna St, Riverside', area: 3, plan: 0, type: 'prepaid', latOff: -0.001, lngOff: -0.001 }, { first: 'Samuel', last: 'Torres', phone: '09551234567', email: null, address: '203 Del Pilar Blvd, Hilltop', area: 4, plan: 2, type: 'postpaid', latOff: 0.0015, lngOff: 0.001 }, { first: 'Tina', last: 'Uy', phone: '09561234567', email: 'tina@email.com', address: '214 Quezon Rd, Centro', area: 0, plan: 4, type: 'postpaid', latOff: -0.002, lngOff: -0.0015 }, { first: 'Ulysses', last: 'Velasco', phone: '09571234567', email: null, address: '225 Magsaysay Ext, Poblacion', area: 1, plan: 1, type: 'prepaid', latOff: 0.003, lngOff: -0.002 }, { first: 'Vivian', last: 'Walsh', phone: '09581234567', email: 'vivian@email.com', address: '236 Roxas Ave, San Isidro', area: 2, plan: 2, type: 'postpaid', latOff: -0.001, lngOff: 0.003 }, { first: 'Walter', last: 'Xavier', phone: '09591234567', email: null, address: '247 Laurel Blvd, Riverside', area: 3, plan: 3, type: 'postpaid', latOff: 0.002, lngOff: -0.001 }, { first: 'Yolanda', last: 'Yap', phone: '09601234567', email: 'yolanda@email.com', address: '258 Osmena Rd, Hilltop', area: 4, plan: 1, type: 'postpaid', latOff: -0.0015, lngOff: 0.002 }, ]; const clients: any[] = []; let invoiceCount = 0; const now = new Date(); for (let i = 0; i < clientDefs.length; i++) { const c = clientDefs[i]; const accountNumber = `C-${String(i + 1).padStart(6, '0')}`; const plan = plans[c.plan]; const isNewSignup = i >= 30; // 15 new signups pending installation const client = await prisma.client.create({ data: { tenantId: tenant.id, accountNumber, firstName: c.first, lastName: c.last, phone: c.phone, email: c.email, address: c.address, areaId: areas[c.area].id, status: isNewSignup ? 'pending' : 'active', latitude: isNewSignup ? null : (areaCoords[c.area][0] + c.latOff), longitude: isNewSignup ? null : (areaCoords[c.area][1] + c.lngOff), }, }); clients.push(client); if (isNewSignup) { // ── New signup: pending subscription + open installation ticket ── await prisma.subscription.create({ data: { tenantId: tenant.id, clientId: client.id, planId: plan.id, type: c.type, status: 'pending', }, }); // Open installation ticket (alternating assigned/unassigned) const assignedTech = i % 2 === 0 ? users.technician.id : null; await prisma.ticket.create({ data: { tenantId: tenant.id, clientId: client.id, createdById: users.tenant_admin.id, assigneeId: assignedTech, type: 'installation', title: `Installation for ${c.first} ${c.last}`, description: `New installation at ${c.address}`, status: assignedTech ? 'in_progress' : 'open', priority: 'high', }, }); // Overdue invoice for new signup (installation fee / first billing) invoiceCount++; const overdueDate = new Date(now); overdueDate.setDate(overdueDate.getDate() - 7); await prisma.invoice.create({ data: { tenantId: tenant.id, clientId: client.id, number: `INV-${String(invoiceCount).padStart(6, '0')}`, amount: plan ? Number(plan.price) : 999, balance: plan ? Number(plan.price) : 999, status: 'overdue', dueDate: overdueDate, periodStart: new Date(now.getTime() - 30 * 86400000), periodEnd: new Date(now.getTime()), }, }); } else { // ── Existing client: active subscription, resolved tickets, invoices ── // Create subscription const installedAt = new Date(now); installedAt.setDate(installedAt.getDate() - (30 + Math.floor(Math.random() * 60))); // 30-90 days ago const activatedAt = new Date(installedAt); activatedAt.setDate(activatedAt.getDate() + 2); await prisma.subscription.create({ data: { tenantId: tenant.id, clientId: client.id, planId: plan.id, type: c.type, status: 'active', installedAt, activatedAt, startDate: activatedAt, }, }); // Create resolved installation + activation tickets await prisma.ticket.create({ data: { tenantId: tenant.id, clientId: client.id, createdById: users.tenant_admin.id, assigneeId: users.technician.id, type: 'installation', title: `Installation for ${c.first} ${c.last}`, description: `Installation at ${c.address}`, status: 'resolved', priority: 'high', resolvedAt: new Date(installedAt.getTime() + 86400000), }, }); await prisma.ticket.create({ data: { tenantId: tenant.id, clientId: client.id, createdById: users.tenant_admin.id, assigneeId: users.technician.id, type: 'activation', title: `Activation for ${c.first} ${c.last}`, status: 'resolved', priority: 'high', resolvedAt: activatedAt, }, }); // Create invoices per client (3-4 per client with varied statuses) for (let m = 0; m < 4; m++) { invoiceCount++; const periodStart = new Date(activatedAt); periodStart.setMonth(periodStart.getMonth() + m); const periodEnd = new Date(periodStart); periodEnd.setDate(periodEnd.getDate() + 30); const dueDate = new Date(periodStart); dueDate.setDate(dueDate.getDate() + 15); // Determine invoice status based on month let status: string; let balance: number; let paidAt: Date | null = null; const amount = Number(plan.price); if (m === 0) { // Month 1: always paid status = 'paid'; balance = 0; paidAt = new Date(dueDate.getTime() - 86400000 * 3); } else if (m === 1) { // Month 2: overdue (unpaid, past due) status = 'overdue'; balance = amount; } else if (m === 2) { // Month 3: 50% paid → partial status = 'partial'; balance = Math.round(amount / 2); } else { // Month 4: upcoming (due in near future) const futureDue = new Date(now); futureDue.setDate(futureDue.getDate() + 3); status = 'sent'; balance = amount; dueDate.setTime(futureDue.getTime()); } const invoice = await prisma.invoice.create({ data: { tenantId: tenant.id, clientId: client.id, number: `INV-${String(invoiceCount).padStart(6, '0')}`, amount, balance, status, dueDate, paidAt, periodStart, periodEnd, }, }); // Create payment(s) for paid/partial invoices if (status === 'paid') { const methods = ['gcash', 'maya', 'cash', 'bank_transfer']; const method = methods[Math.floor(Math.random() * methods.length)]; const paidDate = new Date(dueDate.getTime() - 86400000 * Math.floor(Math.random() * 5)); const collector = i % 2 === 0 ? usersByEmail['collector@demo-isp.com'] : usersByEmail['tech@demo-isp.com']; await prisma.payment.create({ data: { tenantId: tenant.id, clientId: client.id, invoiceId: invoice.id, collectedById: collector.id, amount, method, referenceNo: method !== 'cash' ? `REF-${String(Math.floor(Math.random() * 99999)).padStart(5, '0')}` : null, createdAt: paidDate, }, }); } else if (status === 'partial') { const methods = ['gcash', 'cash']; const method = methods[Math.floor(Math.random() * methods.length)]; const paidDate = new Date(dueDate.getTime() - 86400000 * 2); const collector = i % 2 === 0 ? usersByEmail['collector@demo-isp.com'] : usersByEmail['tech@demo-isp.com']; await prisma.payment.create({ data: { tenantId: tenant.id, clientId: client.id, invoiceId: invoice.id, collectedById: collector.id, amount: Math.round(amount / 2), method, referenceNo: method !== 'cash' ? `REF-${String(Math.floor(Math.random() * 99999)).padStart(5, '0')}` : null, createdAt: paidDate, }, }); } } } } console.log(`Clients: ${clients.length} (with subscriptions, tickets, invoices, payments)`); // ─── Open support tickets ────────────────────────────── const supportTickets = [ { clientIdx: 2, title: 'Intermittent connection drops', desc: 'Internet keeps disconnecting every 30 minutes', priority: 'high', status: 'open', type: 'support', assignee: null }, { clientIdx: 5, title: 'Slow speed during peak hours', desc: 'Speed drops to 5 Mbps from 8-10 PM', priority: 'normal', status: 'open', type: 'support', assignee: null }, { clientIdx: 8, title: 'No internet connection', desc: 'Complete outage since this morning', priority: 'urgent', status: 'in_progress', type: 'support', assignee: 'tech' }, { clientIdx: 11, title: 'Request for plan upgrade', desc: 'Would like to upgrade from Basic to Standard', priority: 'low', status: 'open', type: 'support', assignee: null }, { clientIdx: 1, title: 'WiFi router not working', desc: 'Power light blinking, no WiFi signal', priority: 'high', status: 'in_progress', type: 'support', assignee: 'tech2' }, { clientIdx: 20, title: 'Fiber cable damaged by construction', desc: 'Backhoe hit the fiber line on Rizal Ave', priority: 'urgent', status: 'in_progress', type: 'maintenance', assignee: 'tech' }, { clientIdx: 22, title: 'Billing discrepancy - double charged', desc: 'Customer was charged twice for March billing', priority: 'high', status: 'open', type: 'support', assignee: null }, { clientIdx: 25, title: 'New access point installation request', desc: 'Needs additional AP for 2nd floor', priority: 'normal', status: 'open', type: 'installation', assignee: null }, { clientIdx: 18, title: 'Connection slow after rain', desc: 'Speed degrades significantly during/after rainfall', priority: 'normal', status: 'in_progress', type: 'maintenance', assignee: 'tech2' }, { clientIdx: 28, title: 'Account suspension appeal', desc: 'Customer requests reconnection, willing to pay balance', priority: 'high', status: 'open', type: 'support', assignee: null }, { clientIdx: 23, title: 'Router firmware update needed', desc: 'Current firmware causing intermittent WiFi drops', priority: 'normal', status: 'open', type: 'maintenance', assignee: null }, { clientIdx: 15, title: 'Relocation request - new address', desc: 'Moving to Barangay 4, wants service transferred', priority: 'low', status: 'open', type: 'support', assignee: null }, { clientIdx: 26, title: 'High latency for gaming', desc: 'Ping above 100ms during evenings', priority: 'normal', status: 'in_progress', type: 'support', assignee: 'tech' }, { clientIdx: 19, title: 'ONT replacement needed', desc: 'ONT showing red fault light intermittently', priority: 'high', status: 'open', type: 'maintenance', assignee: null }, { clientIdx: 21, title: 'Monthly service credit request', desc: 'Requesting credit for 2-day outage last month', priority: 'low', status: 'open', type: 'support', assignee: null }, { clientIdx: 3, title: 'Second floor extension installation', desc: 'Client wants additional fiber drop to 2nd floor office', priority: 'normal', status: 'open', type: 'installation', assignee: null }, { clientIdx: 9, title: 'Fiber relocation due to renovation', desc: 'House renovation requires moving fiber entry point', priority: 'normal', status: 'in_progress', type: 'installation', assignee: 'tech2' }, { clientIdx: 14, title: 'ONT upgrade to GPON', desc: 'Current ONT outdated, needs GPON-compatible replacement', priority: 'low', status: 'open', type: 'installation', assignee: null }, { clientIdx: 7, title: 'New branch office fiber install', desc: 'Client opened sari-sari store next door, wants 2nd connection', priority: 'high', status: 'open', type: 'installation', assignee: null }, { clientIdx: 24, title: 'Intermittent packet loss', desc: 'Ping shows 5-10% packet loss during daytime', priority: 'high', status: 'in_progress', type: 'maintenance', assignee: 'tech' }, { clientIdx: 12, title: 'Cable exposed across driveway', desc: 'Fiber cable hanging low across client driveway, safety hazard', priority: 'urgent', status: 'open', type: 'maintenance', assignee: null }, ]; for (const t of supportTickets) { await prisma.ticket.create({ data: { tenantId: tenant.id, clientId: clients[t.clientIdx].id, createdById: users.tenant_admin.id, type: t.type, title: t.title, description: t.desc, priority: t.priority, status: t.status, assigneeId: t.assignee ? users[t.assignee]?.id ?? users.technician.id : null, }, }); } console.log(`Support tickets: ${supportTickets.length}`); // ─── Employees ───────────────────────────────────────── const empDefs = [ { first: 'Pedro', last: 'Cruz', position: 'Senior Technician', dept: 'Operations', salary: 18000 }, { first: 'Jose', last: 'Garcia', position: 'Field Technician', dept: 'Operations', salary: 15000 }, { first: 'Maria', last: 'Reyes', position: 'Operations Manager', dept: 'Management', salary: 30000 }, { first: 'Juan', last: 'Santos', position: 'Collection Officer', dept: 'Finance', salary: 16000 }, { first: 'Ana', last: 'De Leon', position: 'Billing Clerk', dept: 'Finance', salary: 14000 }, { first: 'Luis', last: 'Mercado', position: 'Network Engineer', dept: 'Technical', salary: 25000 }, { first: 'Sofia', last: 'Pascual', position: 'Customer Service', dept: 'Support', salary: 14000 }, ]; const employees: any[] = []; for (let i = 0; i < empDefs.length; i++) { const e = empDefs[i]; const emp = await prisma.employee.create({ data: { tenantId: tenant.id, employeeNo: `E-${String(i + 1).padStart(4, '0')}`, firstName: e.first, lastName: e.last, position: e.position, department: e.dept, salary: e.salary }, }); employees.push(emp); } console.log(`Employees: ${employees.length}`); // ─── Expenses ────────────────────────────────────────── const expDefs = [ { cat: 'utilities', desc: 'Electricity bill - March 2026', amount: 12500, status: 'approved', days: -15 }, { cat: 'utilities', desc: 'Internet backbone ISP bill', amount: 35000, status: 'approved', days: -10 }, { cat: 'supplies', desc: 'Fiber optic cables (500m)', amount: 8500, status: 'approved', days: -8 }, { cat: 'maintenance', desc: 'OLT maintenance and cleaning', amount: 3500, status: 'approved', days: -5 }, { cat: 'transport', desc: 'Fuel for service vehicles', amount: 4200, status: 'approved', days: -3 }, { cat: 'equipment', desc: '10x Mikrotik hEX S routers', amount: 28000, status: 'approved', days: -2 }, { cat: 'supplies', desc: 'Office supplies and printer ink', amount: 2100, status: 'pending', days: -1 }, { cat: 'maintenance', desc: 'Generator repair', amount: 7800, status: 'pending', days: 0 }, { cat: 'transport', desc: 'Technician transport allowance - April', amount: 6000, status: 'pending', days: 0 }, ]; for (const e of expDefs) { const expDate = new Date(); expDate.setDate(expDate.getDate() + e.days); await prisma.expense.create({ data: { tenantId: tenant.id, createdById: users.manager.id, approvedById: e.status === 'approved' ? users.tenant_admin.id : null, category: e.cat, description: e.desc, amount: e.amount, status: e.status, expenseDate: expDate, approvedAt: e.status === 'approved' ? expDate : null, }, }); } console.log(`Expenses: ${expDefs.length}`); // ─── Company Accounts ────────────────────────────────── // Company accounts will be created after CoA so we can link them console.log('Company accounts: deferred to after CoA'); // ─── Fund Transfers ──────────────────────────────────── // Fund transfers deferred to after company accounts console.log('Fund transfers: deferred'); // ─── Assets ──────────────────────────────────────────── const assetDefs = [ { name: 'Huawei MA5608T OLT', cat: 'olt', serial: 'HW-OLT-001', price: 85000, status: 'in_use', loc: 'Main Office' }, { name: 'Mikrotik CCR1009', cat: 'router', serial: 'MK-CCR-001', price: 32000, status: 'in_use', loc: 'Main Office' }, { name: 'Mikrotik hEX S #1', cat: 'router', serial: 'MK-HEX-001', price: 2800, status: 'in_use', empIdx: 0 }, { name: 'Mikrotik hEX S #2', cat: 'router', serial: 'MK-HEX-002', price: 2800, status: 'in_use', empIdx: 1 }, { name: 'Mikrotik hEX S #3', cat: 'router', serial: 'MK-HEX-003', price: 2800, status: 'available', loc: 'Warehouse' }, { name: 'OTDR Tester', cat: 'tool', serial: 'OTDR-001', price: 45000, status: 'in_use', empIdx: 0 }, { name: 'Fiber Splicer', cat: 'tool', serial: 'FS-001', price: 65000, status: 'in_use', empIdx: 5 }, { name: 'Honda XRM 125 (Field)', cat: 'vehicle', serial: 'MV-2024-001', price: 68000, status: 'in_use', empIdx: 0 }, { name: 'Honda Wave 110 (Field)', cat: 'vehicle', serial: 'MV-2024-002', price: 55000, status: 'in_use', empIdx: 1 }, { name: 'Dell Latitude 5540', cat: 'computer', serial: 'DELL-LAP-001', price: 48000, status: 'in_use', empIdx: 2 }, { name: 'Fiber Cable Spool 1km', cat: 'cable', price: 12000, status: 'available', loc: 'Warehouse' }, { name: 'Fiber Cable Spool 500m', cat: 'cable', price: 6500, status: 'available', loc: 'Warehouse' }, { name: 'UPS 1500VA', cat: 'other', serial: 'UPS-001', price: 8500, status: 'in_use', loc: 'Main Office' }, { name: 'Old Mikrotik RB750', cat: 'router', serial: 'MK-OLD-001', price: 1500, status: 'retired' }, ]; for (const a of assetDefs) { await prisma.asset.create({ data: { tenantId: tenant.id, name: a.name, category: a.cat, serialNumber: a.serial || null, purchasePrice: a.price, status: a.status, location: a.loc || null, assignedToId: a.empIdx !== undefined ? employees[a.empIdx].id : null, }, }); } console.log(`Assets: ${assetDefs.length}`); // ─── Billing Settings ────────────────────────────────── await prisma.billingSetting.create({ data: { tenantId: tenant.id, autoGenerate: true, gracePeriodDays: 7, dueDateOffsetDays: 15, invoicePrefix: 'INV' }, }); console.log('Billing settings created'); // ─── Chart of Accounts (auto-seeded by API, but seed defaults) ── const coaDefs = [ { code: '1000', name: 'Assets', type: 'asset', sys: true }, { code: '1010', name: 'Cash on Hand', type: 'asset', sys: true }, { code: '1020', name: 'GCash Business', type: 'asset', sys: true }, { code: '1030', name: 'Maya Business', type: 'asset', sys: true }, { code: '1040', name: 'Bank Account', type: 'asset', sys: true }, { code: '1100', name: 'Accounts Receivable', type: 'asset', sys: true }, { code: '1200', name: 'Equipment', type: 'asset', sys: true }, { code: '2000', name: 'Liabilities', type: 'liability', sys: true }, { code: '2010', name: 'Accounts Payable', type: 'liability', sys: true }, { code: '3000', name: 'Equity', type: 'equity', sys: true }, { code: '3010', name: "Owner's Equity", type: 'equity', sys: true }, { code: '3020', name: 'Retained Earnings', type: 'equity', sys: true }, { code: '4000', name: 'Revenue', type: 'revenue', sys: true }, { code: '4010', name: 'Internet Service Revenue', type: 'revenue', sys: true }, { code: '4020', name: 'Installation Fees', type: 'revenue', sys: true }, { code: '5000', name: 'Expenses', type: 'expense', sys: true }, { code: '5010', name: 'Utilities Expense', type: 'expense', sys: true }, { code: '5020', name: 'Salaries Expense', type: 'expense', sys: true }, { code: '5030', name: 'Maintenance Expense', type: 'expense', sys: true }, { code: '5040', name: 'Transport Expense', type: 'expense', sys: true }, { code: '5050', name: 'Supplies Expense', type: 'expense', sys: true }, { code: '5060', name: 'Equipment Expense', type: 'expense', sys: true }, ]; for (const a of coaDefs) { await prisma.chartOfAccount.create({ data: { tenantId: tenant.id, code: a.code, name: a.name, type: a.type, isSystem: a.sys }, }); } console.log(`Chart of Accounts: ${coaDefs.length}`); // ─── Custodial CoA per user ──────────────────────────── const methods = ['Cash', 'GCash', 'Maya', 'Bank']; let custodialCode = 1500; for (const u of userDefs) { const user = users[u.role]; for (const m of methods) { await prisma.chartOfAccount.create({ data: { tenantId: tenant.id, code: String(custodialCode++), name: `${u.first} ${u.last} - ${m}`, type: 'asset', isSystem: false }, }); } } console.log(`Custodial CoA accounts: ${userDefs.length * 4}`); // ─── Company Accounts (linked to CoA) ────────────────── const coa1010 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1010' } }); const coa1020 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1020' } }); const coa1030 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1030' } }); const coa1040 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1040' } }); const accts = await Promise.all([ prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'Cash on Hand', type: 'cash', balance: 15000, isSystem: true, chartOfAccountId: coa1010?.id } }), prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'GCash Business', type: 'e_wallet', accountNo: '09171234567', balance: 42500, chartOfAccountId: coa1020?.id } }), prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'Maya Business', type: 'e_wallet', accountNo: '09181234567', balance: 18200, chartOfAccountId: coa1030?.id } }), prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'BDO Savings', type: 'bank', accountNo: '0012-3456-7890', balance: 285000, chartOfAccountId: coa1040?.id } }), ]); console.log(`Company accounts: ${accts.length} (linked to CoA)`); // ─── Fund Transfers ──────────────────────────────────── await prisma.fundTransfer.create({ data: { tenantId: tenant.id, fromAccountId: accts[1].id, toAccountId: accts[3].id, amount: 20000, description: 'GCash to BDO weekly transfer', transferredBy: users.tenant_admin.id }, }); await prisma.fundTransfer.create({ data: { tenantId: tenant.id, fromAccountId: accts[3].id, toAccountId: accts[0].id, amount: 5000, description: 'Petty cash replenishment', transferredBy: users.tenant_admin.id }, }); console.log('Fund transfers: 2'); // ─── Remittances (properly linked to payments) ────────── // Get all payments that were for paid invoices (these are candidates for remittances) const allPayments = await prisma.payment.findMany({ where: { tenantId: tenant.id }, orderBy: { createdAt: 'asc' }, }); // Split payments: first 60% → remitted (confirmed), next 20% → remitted (pending), last 20% → unremitted const confirmedEnd = Math.floor(allPayments.length * 0.6); const pendingEnd = Math.floor(allPayments.length * 0.8); const confirmedPayments = allPayments.slice(0, confirmedEnd); const pendingPayments = allPayments.slice(confirmedEnd, pendingEnd); // remaining payments (pendingEnd onward) stay unremitted // Create confirmed remittance if (confirmedPayments.length > 0) { const confirmedTotal = confirmedPayments.reduce((s, p) => s + Number(p.amount), 0); const confirmedRemittance = await prisma.remittance.create({ data: { tenantId: tenant.id, collectorId: users.technician.id, confirmedById: users.tenant_admin.id, totalAmount: confirmedTotal, status: 'confirmed', submittedAt: new Date(Date.now() - 86400000 * 7), confirmedAt: new Date(Date.now() - 86400000 * 5), payments: { create: confirmedPayments.map((p) => ({ paymentId: p.id })), }, }, }); console.log(`Remittance (confirmed): ₱${confirmedTotal} (${confirmedPayments.length} payments)`); } // Create pending remittance if (pendingPayments.length > 0) { const pendingTotal = pendingPayments.reduce((s, p) => s + Number(p.amount), 0); const pendingRemittance = await prisma.remittance.create({ data: { tenantId: tenant.id, collectorId: users.technician.id, totalAmount: pendingTotal, status: 'pending', submittedAt: new Date(Date.now() - 86400000 * 2), payments: { create: pendingPayments.map((p) => ({ paymentId: p.id })), }, }, }); console.log(`Remittance (pending): ₱${pendingTotal} (${pendingPayments.length} payments)`); } const unremittedCount = allPayments.length - pendingEnd; console.log(`Unremitted payments: ${unremittedCount} (available for new remittance)`); console.log('\n✅ Seed completed successfully!'); console.log(`\n📊 Summary:`); console.log(` Tenant: ${tenant.name}`); console.log(` Users: ${userDefs.length}`); console.log(` Areas: ${areas.length}`); console.log(` Plans: ${plans.length}`); console.log(` Clients: ${clients.length} (with active subscriptions)`); console.log(` Invoices: ${invoiceCount * 2} (paid + unpaid)`); console.log(` Employees: ${empDefs.length}`); console.log(` Expenses: ${expDefs.length} (approved + pending)`); console.log(` Assets: ${assetDefs.length}`); console.log(` Company Accounts: ${accts.length}`); console.log(`\n🔑 Login: admin@demo-isp.com / admin123!`); console.log(`🌐 Portal: C-000001 / 09171234567`); } main() .catch((e) => { console.error('Seed failed:', e); process.exit(1); }) .finally(async () => { await prisma.$disconnect(); });