import { PrismaClient, GLAccountType, DebitCredit, ControlAccountType, JournalType } from '@prisma/client'; const prisma = new PrismaClient(); const PERMISSIONS = [ 'admin.segments.manage', 'admin.fiscalPeriods.manage', 'admin.users.manage', 'admin.tax.manage', 'gl.account.manage', 'gl.journal.manage', 'gl.journal.post', 'ar.customer.manage', 'ar.invoice.manage', 'ar.creditnote.manage', 'ar.receipt.manage', 'ap.supplier.manage', 'ap.invoice.manage', 'ap.debitnote.manage', 'ap.payment.manage', 'stock.warehouse.manage', 'stock.item.manage', ] as const; const ROLE_PERMISSIONS: Record = { ADMIN: PERMISSIONS, ACCOUNTANT: [ 'gl.account.manage', 'gl.journal.manage', 'gl.journal.post', 'admin.tax.manage', 'ar.customer.manage', 'ar.invoice.manage', 'ar.creditnote.manage', 'ar.receipt.manage', 'ap.supplier.manage', 'ap.invoice.manage', 'ap.debitnote.manage', 'ap.payment.manage', 'stock.warehouse.manage', 'stock.item.manage', ], AR_AP_CLERK: [ 'ar.customer.manage', 'ar.invoice.manage', 'ar.creditnote.manage', 'ar.receipt.manage', 'ap.supplier.manage', 'ap.invoice.manage', 'ap.debitnote.manage', 'ap.payment.manage', ], STOCK_CLERK: ['stock.warehouse.manage', 'stock.item.manage'], BRANCH_USER: [], READ_ONLY: [], }; async function main() { console.log('Seeding company settings...'); await prisma.companySettings.upsert({ where: { id: 1 }, create: { id: 1, companyName: 'Demo Company', baseCurrencyCode: 'ZAR', fiscalYearStartMonth: 1, currentFiscalYear: 2026 }, update: {}, }); console.log('Seeding permissions and roles...'); for (const key of PERMISSIONS) { await prisma.permission.upsert({ where: { key }, create: { key }, update: {} }); } for (const roleName of Object.keys(ROLE_PERMISSIONS)) { await prisma.role.upsert({ where: { name: roleName }, create: { name: roleName }, update: {} }); } for (const [roleName, permissionKeys] of Object.entries(ROLE_PERMISSIONS)) { const role = await prisma.role.findUniqueOrThrow({ where: { name: roleName } }); for (const key of permissionKeys) { const permission = await prisma.permission.findUniqueOrThrow({ where: { key } }); await prisma.rolePermission.upsert({ where: { roleId_permissionId: { roleId: role.id, permissionId: permission.id } }, create: { roleId: role.id, permissionId: permission.id }, update: {}, }); } } console.log('Seeding default account segment structure...'); const segmentDefs = [ { segmentKey: 'MASTER', label: 'Master Account', order: 0, length: 4, format: 'NUMERIC' as const, isBranchSegment: false }, { segmentKey: 'SUB', label: 'Sub Account', order: 1, length: 3, format: 'NUMERIC' as const, isBranchSegment: false }, { segmentKey: 'SUBSUB', label: 'Sub-Sub Account', order: 2, length: 3, format: 'NUMERIC' as const, isBranchSegment: false }, { segmentKey: 'BRANCH', label: 'Branch', order: 3, length: 3, format: 'ALPHA' as const, isBranchSegment: true }, ]; const defByKey: Record = {}; for (const def of segmentDefs) { defByKey[def.segmentKey] = await prisma.accountSegmentDefinition.upsert({ where: { segmentKey: def.segmentKey }, create: def, update: def, }); } const masterValues = [ { code: '1000', description: 'Bank / Cash' }, { code: '1100', description: 'Accounts Receivable Control' }, { code: '1200', description: 'Inventory / Stock Control' }, { code: '2000', description: 'Accounts Payable Control' }, { code: '2050', description: 'Goods Received Not Invoiced' }, { code: '2100', description: 'Output Tax Payable' }, { code: '2200', description: 'Input Tax Receivable' }, { code: '2300', description: 'Accrued Expenses' }, { code: '3000', description: 'Retained Earnings' }, { code: '3100', description: 'Share Capital' }, { code: '4000', description: 'Sales Income' }, { code: '5000', description: 'Cost of Goods Sold' }, { code: '5100', description: 'Operating Expenses' }, { code: '5200', description: 'Stock Variance' }, { code: '5300', description: 'Purchase Price Variance' }, ]; const masterByCode: Record = {}; for (const value of masterValues) { masterByCode[value.code] = await prisma.accountSegmentValue.upsert({ where: { segmentDefId_code: { segmentDefId: defByKey.MASTER.id, code: value.code } }, create: { segmentDefId: defByKey.MASTER.id, ...value }, update: value, }); } const subDefault = await prisma.accountSegmentValue.upsert({ where: { segmentDefId_code: { segmentDefId: defByKey.SUB.id, code: '000' } }, create: { segmentDefId: defByKey.SUB.id, code: '000', description: 'Default' }, update: {}, }); const subsubDefault = await prisma.accountSegmentValue.upsert({ where: { segmentDefId_code: { segmentDefId: defByKey.SUBSUB.id, code: '000' } }, create: { segmentDefId: defByKey.SUBSUB.id, code: '000', description: 'Default' }, update: {}, }); const branchDefs = [ { code: 'HQ', description: 'Head Office' }, { code: 'AMA', description: 'Branch AMA (example branch)' }, ]; const branchByCode: Record = {}; for (const branch of branchDefs) { const value = await prisma.accountSegmentValue.upsert({ where: { segmentDefId_code: { segmentDefId: defByKey.BRANCH.id, code: branch.code } }, create: { segmentDefId: defByKey.BRANCH.id, code: branch.code, description: branch.description }, update: {}, }); branchByCode[branch.code] = value; await prisma.branchProfile.upsert({ where: { segmentValueId: value.id }, create: { segmentValueId: value.id }, update: {}, }); } console.log('Seeding default Chart of Accounts...'); const operationalAccounts: Array<{ masterCode: string; accountName: string; accountType: GLAccountType; normalBalance: DebitCredit; isControlAccount?: boolean; controlType?: ControlAccountType; }> = [ { masterCode: '1000', accountName: 'Bank / Cash', accountType: 'ASSET', normalBalance: 'DEBIT', controlType: 'BANK' }, { masterCode: '1100', accountName: 'Accounts Receivable Control', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'AR' }, // Not a control account: no subledger in this app owns stock value — see the Stock section // of prisma/schema.prisma. Postable directly via manual journal like any other GL account. { masterCode: '1200', accountName: 'Inventory / Stock Control', accountType: 'ASSET', normalBalance: 'DEBIT' }, { masterCode: '2000', accountName: 'Accounts Payable Control', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'AP' }, { masterCode: '2050', accountName: 'Goods Received Not Invoiced', accountType: 'LIABILITY', normalBalance: 'CREDIT' }, { masterCode: '2100', accountName: 'Output Tax Payable', accountType: 'LIABILITY', normalBalance: 'CREDIT', isControlAccount: true, controlType: 'TAX_OUTPUT' }, { masterCode: '2200', accountName: 'Input Tax Receivable', accountType: 'ASSET', normalBalance: 'DEBIT', isControlAccount: true, controlType: 'TAX_INPUT' }, { masterCode: '2300', accountName: 'Accrued Expenses', accountType: 'LIABILITY', normalBalance: 'CREDIT' }, { masterCode: '4000', accountName: 'Sales Income', accountType: 'INCOME', normalBalance: 'CREDIT' }, { masterCode: '5000', accountName: 'Cost of Goods Sold', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, { masterCode: '5100', accountName: 'Operating Expenses', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, { masterCode: '5200', accountName: 'Stock Variance', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, { masterCode: '5300', accountName: 'Purchase Price Variance', accountType: 'EXPENSE', normalBalance: 'DEBIT' }, ]; for (const branchCode of Object.keys(branchByCode)) { for (const account of operationalAccounts) { const accountCode = [masterByCode[account.masterCode].id, subDefault.id, subsubDefault.id, branchByCode[branchCode].id]; const fullCode = `${account.masterCode}-000-000-${branchCode}`; await prisma.gLAccount.upsert({ where: { accountCode: fullCode }, create: { accountCode: fullCode, accountName: `${account.accountName} — ${branchCode}`, accountType: account.accountType, normalBalance: account.normalBalance, isControlAccount: account.isControlAccount ?? false, controlType: account.controlType, segmentValues: { create: [ { segmentDefId: defByKey.MASTER.id, segmentValueId: accountCode[0] }, { segmentDefId: defByKey.SUB.id, segmentValueId: accountCode[1] }, { segmentDefId: defByKey.SUBSUB.id, segmentValueId: accountCode[2] }, { segmentDefId: defByKey.BRANCH.id, segmentValueId: accountCode[3] }, ], }, }, update: { accountName: `${account.accountName} — ${branchCode}`, accountType: account.accountType, normalBalance: account.normalBalance, isControlAccount: account.isControlAccount ?? false, controlType: account.controlType ?? null, }, }); } } // Equity accounts are booked at Head Office only. const equityAccounts: Array<{ masterCode: string; accountName: string; controlType?: ControlAccountType }> = [ { masterCode: '3000', accountName: 'Retained Earnings', controlType: 'RETAINED_EARNINGS' }, { masterCode: '3100', accountName: 'Share Capital' }, ]; for (const account of equityAccounts) { const fullCode = `${account.masterCode}-000-000-HQ`; await prisma.gLAccount.upsert({ where: { accountCode: fullCode }, create: { accountCode: fullCode, accountName: account.accountName, accountType: 'EQUITY', normalBalance: 'CREDIT', controlType: account.controlType, segmentValues: { create: [ { segmentDefId: defByKey.MASTER.id, segmentValueId: masterByCode[account.masterCode].id }, { segmentDefId: defByKey.SUB.id, segmentValueId: subDefault.id }, { segmentDefId: defByKey.SUBSUB.id, segmentValueId: subsubDefault.id }, { segmentDefId: defByKey.BRANCH.id, segmentValueId: branchByCode.HQ.id }, ], }, }, update: { accountName: account.accountName, controlType: account.controlType ?? null }, }); } console.log('Seeding journals...'); await prisma.journal.upsert({ where: { code: 'GJ' }, create: { code: 'GJ', name: 'General Journal', journalType: JournalType.GENERAL }, update: {}, }); await prisma.journal.upsert({ where: { code: 'PC-HQ' }, create: { code: 'PC-HQ', name: 'Petty Cash — Head Office', journalType: JournalType.CUSTOM, defaultBranchSegmentValueId: branchByCode.HQ.id, }, update: {}, }); await prisma.journal.upsert({ where: { code: 'SALES' }, create: { code: 'SALES', name: 'Sales Journal', journalType: JournalType.SALES }, update: {}, }); await prisma.journal.upsert({ where: { code: 'CASH_RECEIPT' }, create: { code: 'CASH_RECEIPT', name: 'Cash Receipts Journal', journalType: JournalType.CASH_RECEIPT }, update: {}, }); await prisma.journal.upsert({ where: { code: 'PURCHASES' }, create: { code: 'PURCHASES', name: 'Purchases Journal', journalType: JournalType.PURCHASES }, update: {}, }); await prisma.journal.upsert({ where: { code: 'CASH_PAYMENT' }, create: { code: 'CASH_PAYMENT', name: 'Cash Payments Journal', journalType: JournalType.CASH_PAYMENT }, update: {}, }); await prisma.journal.upsert({ where: { code: 'STOCK' }, create: { code: 'STOCK', name: 'Stock Journal', journalType: JournalType.STOCK }, update: {}, }); console.log('Seeding warehouses...'); for (const branchCode of Object.keys(branchByCode)) { await prisma.warehouse.upsert({ where: { code: `WH-${branchCode}` }, create: { code: `WH-${branchCode}`, name: `Main Warehouse — ${branchCode}`, branchSegmentValueId: branchByCode[branchCode].id }, update: {}, }); } console.log('Seeding tax codes...'); const taxCodes = [ { code: 'STD', name: 'Standard Rate (15%)', ratePercent: 15 }, { code: 'ZERO', name: 'Zero-Rated (0%)', ratePercent: 0 }, ]; for (const taxCode of taxCodes) { await prisma.taxCode.upsert({ where: { code: taxCode.code }, create: taxCode, update: taxCode }); } console.log('Seeding current fiscal year...'); const fiscalYear = await prisma.fiscalYear.upsert({ where: { year: 2026 }, create: { year: 2026, startDate: new Date(Date.UTC(2026, 0, 1)), endDate: new Date(Date.UTC(2026, 11, 31)) }, update: {}, }); for (let month = 0; month < 12; month += 1) { const startDate = new Date(Date.UTC(2026, month, 1)); const endDate = new Date(Date.UTC(2026, month + 1, 0)); await prisma.fiscalPeriod.upsert({ where: { fiscalYearId_periodNumber: { fiscalYearId: fiscalYear.id, periodNumber: month + 1 } }, create: { fiscalYearId: fiscalYear.id, periodNumber: month + 1, startDate, endDate }, update: {}, }); } console.log('Seeding admin user...'); // No password here — identity is established by the Account Manager's handoff token and // matched to this User by email (see src/modules/auth/auth.service.ts verifyHandoff()). // This email must match the Person who logs into the Manager for the mapping to resolve. const adminEmail = 'joosth2002@icloud.com'; const adminRole = await prisma.role.findUniqueOrThrow({ where: { name: 'ADMIN' } }); const existingAdmin = await prisma.user.findUnique({ where: { email: adminEmail } }); if (!existingAdmin) { await prisma.user.create({ data: { email: adminEmail, fullName: 'System Administrator', roles: { create: { roleId: adminRole.id } }, }, }); console.log(`Created admin user ${adminEmail} — signs in via the Account Manager, no local password.`); } console.log('Seed complete.'); } main() .catch((error) => { console.error(error); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });