Files
fiberops-api-admin/packages/db/prisma/seed.ts
2026-04-13 09:43:35 +08:00

604 lines
31 KiB
TypeScript

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<string> {
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<string, any> = {};
const usersByEmail: Record<string, any> = {};
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: 'technician' },
{ 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<string, any> = {};
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<string, string> = {
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 },
];
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 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,
latitude: areaCoords[c.area][0] + c.latOff,
longitude: areaCoords[c.area][1] + c.lngOff,
},
});
clients.push(client);
// 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 2 invoices per client
for (let m = 0; m < 2; 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);
const isPaid = m === 0 || Math.random() > 0.4; // First invoice always paid, second 60% chance
const balance = isPaid ? 0 : Number(plan.price);
const invoice = await prisma.invoice.create({
data: {
tenantId: tenant.id,
clientId: client.id,
number: `INV-${String(invoiceCount).padStart(6, '0')}`,
amount: plan.price,
balance,
status: isPaid ? 'paid' : (dueDate < now ? 'overdue' : 'sent'),
dueDate,
paidAt: isPaid ? new Date(dueDate.getTime() - 86400000 * 3) : null,
periodStart,
periodEnd,
},
});
// Create payment for paid invoices
if (isPaid) {
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));
await prisma.payment.create({
data: {
tenantId: tenant.id,
clientId: client.id,
invoiceId: invoice.id,
collectedById: users.technician.id,
amount: plan.price,
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' },
{ clientIdx: 5, title: 'Slow speed during peak hours', desc: 'Speed drops to 5 Mbps from 8-10 PM', priority: 'normal' },
{ clientIdx: 8, title: 'No internet connection', desc: 'Complete outage since this morning', priority: 'urgent' },
{ clientIdx: 11, title: 'Request for plan upgrade', desc: 'Would like to upgrade from Basic to Standard', priority: 'low' },
{ clientIdx: 1, title: 'WiFi router not working', desc: 'Power light blinking, no WiFi signal', priority: 'high' },
];
for (const t of supportTickets) {
await prisma.ticket.create({
data: {
tenantId: tenant.id,
clientId: clients[t.clientIdx].id,
createdById: users.tenant_admin.id,
type: 'support',
title: t.title,
description: t.desc,
priority: t.priority,
status: 'open',
},
});
}
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 ───────────────────────────────────────
await prisma.remittance.create({
data: { tenantId: tenant.id, collectorId: users.technician.id, confirmedById: users.tenant_admin.id, totalAmount: 8995, status: 'confirmed', confirmedAt: new Date() },
});
await prisma.remittance.create({
data: { tenantId: tenant.id, collectorId: users.technician.id, totalAmount: 5497, status: 'pending' },
});
console.log('Remittances: 2');
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(); });