// Phase 1: Foundation — auth, company/fiscal setup, segmented Chart of Accounts, GL posting engine, journals. // AR / AP / Tax / Stock models are added in later phases per the implementation plan. generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // --------------------------------------------------------------------------- // Company / fiscal setup // --------------------------------------------------------------------------- model CompanySettings { id Int @id @default(1) companyName String baseCurrencyCode String @default("ZAR") fiscalYearStartMonth Int @default(1) currentFiscalYear Int accountCodeSeparator String @default("-") // Login now happens at the central Account Manager, which hands this deployment a signed // token instead of a password (see src/lib/handoffVerify.ts). If false (the default), a // handoff for an email with no matching User here is rejected rather than auto-creating one. autoProvisionOnHandoff Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // Records each handoff token's jti once used, so a captured token can't be replayed. model HandoffTokenUse { id Int @id @default(autoincrement()) jti String @unique usedAt DateTime @default(now()) } enum FiscalYearStatus { OPEN LOCKED CLOSED } enum PeriodStatus { OPEN CLOSED } model FiscalYear { id Int @id @default(autoincrement()) year Int @unique startDate DateTime endDate DateTime status FiscalYearStatus @default(OPEN) periods FiscalPeriod[] } model FiscalPeriod { id Int @id @default(autoincrement()) fiscalYearId Int fiscalYear FiscalYear @relation(fields: [fiscalYearId], references: [id]) periodNumber Int startDate DateTime endDate DateTime status PeriodStatus @default(OPEN) journalEntries JournalEntry[] @@unique([fiscalYearId, periodNumber]) } // --------------------------------------------------------------------------- // Segmented Chart of Accounts (Sage 300-style account segments) // --------------------------------------------------------------------------- enum SegmentFormat { NUMERIC ALPHA ALPHANUMERIC } model AccountSegmentDefinition { id Int @id @default(autoincrement()) segmentKey String @unique label String order Int length Int format SegmentFormat @default(NUMERIC) isBranchSegment Boolean @default(false) isActive Boolean @default(true) values AccountSegmentValue[] glAccountLinks GLAccountSegmentValue[] @@index([order]) } model AccountSegmentValue { id Int @id @default(autoincrement()) segmentDefId Int segmentDef AccountSegmentDefinition @relation(fields: [segmentDefId], references: [id]) code String description String isActive Boolean @default(true) branchProfile BranchProfile? glAccountLinks GLAccountSegmentValue[] customers Customer[] suppliers Supplier[] warehouses Warehouse[] @@unique([segmentDefId, code]) } model BranchProfile { id Int @id @default(autoincrement()) segmentValueId Int @unique segmentValue AccountSegmentValue @relation(fields: [segmentValueId], references: [id]) address String? contactName String? isConsolidatable Boolean @default(true) } enum GLAccountType { ASSET LIABILITY EQUITY INCOME EXPENSE } enum DebitCredit { DEBIT CREDIT } enum ControlAccountType { AR AP TAX_OUTPUT TAX_INPUT BANK RETAINED_EARNINGS } model GLAccount { id Int @id @default(autoincrement()) accountCode String @unique accountName String accountType GLAccountType normalBalance DebitCredit isControlAccount Boolean @default(false) controlType ControlAccountType? isActive Boolean @default(true) isPostable Boolean @default(true) segmentValues GLAccountSegmentValue[] journalLines JournalEntryLine[] arInvoiceLines ARInvoiceLine[] @relation("ARInvoiceLineRevenue") arCreditNoteLines ARCreditNoteLine[] @relation("ARCreditNoteLineRevenue") arReceipts ARReceipt[] @relation("ARReceiptBank") apInvoiceLines APInvoiceLine[] @relation("APInvoiceLineExpense") apDebitNoteLines APDebitNoteLine[] @relation("APDebitNoteLineExpense") apPayments APPayment[] @relation("APPaymentBank") createdAt DateTime @default(now()) @@index([accountType]) } model GLAccountSegmentValue { id Int @id @default(autoincrement()) glAccountId Int glAccount GLAccount @relation(fields: [glAccountId], references: [id]) segmentDefId Int segmentDef AccountSegmentDefinition @relation(fields: [segmentDefId], references: [id]) segmentValueId Int segmentValue AccountSegmentValue @relation(fields: [segmentValueId], references: [id]) @@unique([glAccountId, segmentDefId]) } // --------------------------------------------------------------------------- // Journals / General Ledger posting core // --------------------------------------------------------------------------- enum JournalType { GENERAL SALES PURCHASES CASH_RECEIPT CASH_PAYMENT STOCK BANK CUSTOM } model Journal { id Int @id @default(autoincrement()) code String @unique name String journalType JournalType defaultBranchSegmentValueId Int? isActive Boolean @default(true) entries JournalEntry[] } enum SourceModule { GL AR AP STOCK SYSTEM } enum JournalEntryStatus { DRAFT POSTED REVERSED } model JournalEntry { id Int @id @default(autoincrement()) entryNumber String @unique journalId Int journal Journal @relation(fields: [journalId], references: [id]) fiscalPeriodId Int fiscalPeriod FiscalPeriod @relation(fields: [fiscalPeriodId], references: [id]) entryDate DateTime description String sourceModule SourceModule sourceType String? sourceId Int? status JournalEntryStatus @default(POSTED) reversalOfId Int? @unique reversalOf JournalEntry? @relation("Reversal", fields: [reversalOfId], references: [id]) reversedBy JournalEntry? @relation("Reversal") totalDebits Decimal @db.Decimal(18, 2) totalCredits Decimal @db.Decimal(18, 2) createdById Int createdBy User @relation(fields: [createdById], references: [id]) createdAt DateTime @default(now()) lines JournalEntryLine[] @@index([fiscalPeriodId]) @@index([sourceModule, sourceType, sourceId]) } model JournalEntryLine { id Int @id @default(autoincrement()) journalEntryId Int journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id]) lineNumber Int glAccountId Int glAccount GLAccount @relation(fields: [glAccountId], references: [id]) debit Decimal @default(0) @db.Decimal(18, 2) credit Decimal @default(0) @db.Decimal(18, 2) description String? branchSegmentValueId Int? // Sub-ledger cross-references for drill-down. arInvoiceId Int? arInvoice ARInvoice? @relation(fields: [arInvoiceId], references: [id]) apInvoiceId Int? apInvoice APInvoice? @relation(fields: [apInvoiceId], references: [id]) currencyCode String @default("BASE") exchangeRateToBase Decimal @default(1) @db.Decimal(18, 6) @@index([journalEntryId]) @@index([glAccountId]) } // --------------------------------------------------------------------------- // Document numbering // --------------------------------------------------------------------------- // One row per document-number series (e.g. "JE-2026", "ARINV-2026"). Numbers are // issued via an atomic increment inside the same transaction as the document create, // so they stay gap-free/race-free under concurrent posting. model Sequence { id Int @id @default(autoincrement()) key String @unique lastValue Int @default(0) } // --------------------------------------------------------------------------- // Accounts Receivable // --------------------------------------------------------------------------- model TaxCode { id Int @id @default(autoincrement()) code String @unique name String ratePercent Decimal @db.Decimal(6, 3) isActive Boolean @default(true) arInvoiceLines ARInvoiceLine[] arCreditNoteLines ARCreditNoteLine[] apInvoiceLines APInvoiceLine[] apDebitNoteLines APDebitNoteLine[] } model Customer { id Int @id @default(autoincrement()) code String @unique name String email String? phone String? address String? taxNumber String? branchSegmentValueId Int branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id]) creditLimit Decimal @default(0) @db.Decimal(18, 2) paymentTermsDays Int @default(30) isActive Boolean @default(true) invoices ARInvoice[] creditNotes ARCreditNote[] receipts ARReceipt[] createdAt DateTime @default(now()) @@index([branchSegmentValueId]) } enum ARDocumentStatus { DRAFT POSTED VOID } model ARInvoice { id Int @id @default(autoincrement()) invoiceNumber String @unique customerId Int customer Customer @relation(fields: [customerId], references: [id]) branchSegmentValueId Int invoiceDate DateTime dueDate DateTime reference String? status ARDocumentStatus @default(DRAFT) subtotal Decimal @db.Decimal(18, 2) taxTotal Decimal @db.Decimal(18, 2) total Decimal @db.Decimal(18, 2) balance Decimal @db.Decimal(18, 2) journalEntryId Int? lines ARInvoiceLine[] allocations ARAllocation[] journalLines JournalEntryLine[] createdById Int createdBy User @relation("ARInvoiceCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([customerId]) @@index([branchSegmentValueId]) @@index([status]) } model ARInvoiceLine { id Int @id @default(autoincrement()) invoiceId Int invoice ARInvoice @relation(fields: [invoiceId], references: [id]) lineNumber Int description String quantity Decimal @default(1) @db.Decimal(18, 4) unitPrice Decimal @db.Decimal(18, 4) revenueGLAccountId Int revenueGLAccount GLAccount @relation("ARInvoiceLineRevenue", fields: [revenueGLAccountId], references: [id]) taxCodeId Int? taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) lineSubtotal Decimal @db.Decimal(18, 2) lineTax Decimal @db.Decimal(18, 2) lineTotal Decimal @db.Decimal(18, 2) @@index([invoiceId]) } model ARCreditNote { id Int @id @default(autoincrement()) creditNoteNumber String @unique customerId Int customer Customer @relation(fields: [customerId], references: [id]) branchSegmentValueId Int creditNoteDate DateTime reference String? reason String? status ARDocumentStatus @default(DRAFT) subtotal Decimal @db.Decimal(18, 2) taxTotal Decimal @db.Decimal(18, 2) total Decimal @db.Decimal(18, 2) balance Decimal @db.Decimal(18, 2) journalEntryId Int? lines ARCreditNoteLine[] allocations ARAllocation[] createdById Int createdBy User @relation("ARCreditNoteCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([customerId]) @@index([branchSegmentValueId]) @@index([status]) } model ARCreditNoteLine { id Int @id @default(autoincrement()) creditNoteId Int creditNote ARCreditNote @relation(fields: [creditNoteId], references: [id]) lineNumber Int description String quantity Decimal @default(1) @db.Decimal(18, 4) unitPrice Decimal @db.Decimal(18, 4) revenueGLAccountId Int revenueGLAccount GLAccount @relation("ARCreditNoteLineRevenue", fields: [revenueGLAccountId], references: [id]) taxCodeId Int? taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) lineSubtotal Decimal @db.Decimal(18, 2) lineTax Decimal @db.Decimal(18, 2) lineTotal Decimal @db.Decimal(18, 2) @@index([creditNoteId]) } model ARReceipt { id Int @id @default(autoincrement()) receiptNumber String @unique customerId Int customer Customer @relation(fields: [customerId], references: [id]) branchSegmentValueId Int receiptDate DateTime amount Decimal @db.Decimal(18, 2) unallocatedAmount Decimal @db.Decimal(18, 2) bankGLAccountId Int bankGLAccount GLAccount @relation("ARReceiptBank", fields: [bankGLAccountId], references: [id]) reference String? status ARDocumentStatus @default(DRAFT) journalEntryId Int? allocations ARAllocation[] createdById Int createdBy User @relation("ARReceiptCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([customerId]) @@index([branchSegmentValueId]) @@index([status]) } model ARAllocation { id Int @id @default(autoincrement()) invoiceId Int invoice ARInvoice @relation(fields: [invoiceId], references: [id]) receiptId Int? receipt ARReceipt? @relation(fields: [receiptId], references: [id]) creditNoteId Int? creditNote ARCreditNote? @relation(fields: [creditNoteId], references: [id]) amount Decimal @db.Decimal(18, 2) allocatedAt DateTime @default(now()) @@index([invoiceId]) @@index([receiptId]) @@index([creditNoteId]) } // --------------------------------------------------------------------------- // Accounts Payable // --------------------------------------------------------------------------- model Supplier { id Int @id @default(autoincrement()) code String @unique name String email String? phone String? address String? taxNumber String? branchSegmentValueId Int branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id]) paymentTermsDays Int @default(30) isActive Boolean @default(true) invoices APInvoice[] debitNotes APDebitNote[] payments APPayment[] createdAt DateTime @default(now()) @@index([branchSegmentValueId]) } enum APDocumentStatus { DRAFT POSTED VOID } model APInvoice { id Int @id @default(autoincrement()) invoiceNumber String @unique supplierReference String? supplierId Int supplier Supplier @relation(fields: [supplierId], references: [id]) branchSegmentValueId Int invoiceDate DateTime dueDate DateTime status APDocumentStatus @default(DRAFT) subtotal Decimal @db.Decimal(18, 2) taxTotal Decimal @db.Decimal(18, 2) total Decimal @db.Decimal(18, 2) balance Decimal @db.Decimal(18, 2) journalEntryId Int? lines APInvoiceLine[] allocations APAllocation[] journalLines JournalEntryLine[] createdById Int createdBy User @relation("APInvoiceCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([supplierId]) @@index([branchSegmentValueId]) @@index([status]) } model APInvoiceLine { id Int @id @default(autoincrement()) invoiceId Int invoice APInvoice @relation(fields: [invoiceId], references: [id]) lineNumber Int description String quantity Decimal @default(1) @db.Decimal(18, 4) unitPrice Decimal @db.Decimal(18, 4) expenseGLAccountId Int expenseGLAccount GLAccount @relation("APInvoiceLineExpense", fields: [expenseGLAccountId], references: [id]) taxCodeId Int? taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) lineSubtotal Decimal @db.Decimal(18, 2) lineTax Decimal @db.Decimal(18, 2) lineTotal Decimal @db.Decimal(18, 2) @@index([invoiceId]) } model APDebitNote { id Int @id @default(autoincrement()) debitNoteNumber String @unique supplierId Int supplier Supplier @relation(fields: [supplierId], references: [id]) branchSegmentValueId Int debitNoteDate DateTime reference String? reason String? status APDocumentStatus @default(DRAFT) subtotal Decimal @db.Decimal(18, 2) taxTotal Decimal @db.Decimal(18, 2) total Decimal @db.Decimal(18, 2) balance Decimal @db.Decimal(18, 2) journalEntryId Int? lines APDebitNoteLine[] allocations APAllocation[] createdById Int createdBy User @relation("APDebitNoteCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([supplierId]) @@index([branchSegmentValueId]) @@index([status]) } model APDebitNoteLine { id Int @id @default(autoincrement()) debitNoteId Int debitNote APDebitNote @relation(fields: [debitNoteId], references: [id]) lineNumber Int description String quantity Decimal @default(1) @db.Decimal(18, 4) unitPrice Decimal @db.Decimal(18, 4) expenseGLAccountId Int expenseGLAccount GLAccount @relation("APDebitNoteLineExpense", fields: [expenseGLAccountId], references: [id]) taxCodeId Int? taxCode TaxCode? @relation(fields: [taxCodeId], references: [id]) lineSubtotal Decimal @db.Decimal(18, 2) lineTax Decimal @db.Decimal(18, 2) lineTotal Decimal @db.Decimal(18, 2) @@index([debitNoteId]) } model APPayment { id Int @id @default(autoincrement()) paymentNumber String @unique supplierId Int supplier Supplier @relation(fields: [supplierId], references: [id]) branchSegmentValueId Int paymentDate DateTime amount Decimal @db.Decimal(18, 2) unallocatedAmount Decimal @db.Decimal(18, 2) bankGLAccountId Int bankGLAccount GLAccount @relation("APPaymentBank", fields: [bankGLAccountId], references: [id]) reference String? status APDocumentStatus @default(DRAFT) journalEntryId Int? allocations APAllocation[] createdById Int createdBy User @relation("APPaymentCreatedBy", fields: [createdById], references: [id]) createdAt DateTime @default(now()) @@index([supplierId]) @@index([branchSegmentValueId]) @@index([status]) } model APAllocation { id Int @id @default(autoincrement()) invoiceId Int invoice APInvoice @relation(fields: [invoiceId], references: [id]) paymentId Int? payment APPayment? @relation(fields: [paymentId], references: [id]) debitNoteId Int? debitNote APDebitNote? @relation(fields: [debitNoteId], references: [id]) amount Decimal @db.Decimal(18, 2) allocatedAt DateTime @default(now()) @@index([invoiceId]) @@index([paymentId]) @@index([debitNoteId]) } // --------------------------------------------------------------------------- // Stock (catalog + cost centers only — see the comment on Warehouse below) // --------------------------------------------------------------------------- // Deliberately minimal: this is accounting software, not a stock/warehouse system. Physical // stock transactions (goods received, stock takes, recipe production) belong in Matrix (the // external hospitality/retail stock system this app is designed to eventually integrate with) // — this app only needs a catalog of items, a set of cost centers to view SOH by, and a place // for whatever quantities Matrix eventually reports. No costing engine, no movement ledger, no // document workflow live here. model Warehouse { id Int @id @default(autoincrement()) code String @unique name String branchSegmentValueId Int branchSegmentValue AccountSegmentValue @relation(fields: [branchSegmentValueId], references: [id]) isActive Boolean @default(true) stockLevels StockLevel[] @@index([branchSegmentValueId]) } model StockItem { id Int @id @default(autoincrement()) code String @unique name String unitOfMeasure String isActive Boolean @default(true) stockLevels StockLevel[] createdAt DateTime @default(now()) } // Quantity on hand per item/warehouse — a read target for a future Matrix feed, not written to // from any UI in this app today. model StockLevel { id Int @id @default(autoincrement()) itemId Int item StockItem @relation(fields: [itemId], references: [id]) warehouseId Int warehouse Warehouse @relation(fields: [warehouseId], references: [id]) quantityOnHand Decimal @default(0) @db.Decimal(18, 4) updatedAt DateTime @updatedAt @@unique([itemId, warehouseId]) } // --------------------------------------------------------------------------- // Auth / RBAC // --------------------------------------------------------------------------- model User { id Int @id @default(autoincrement()) email String @unique // Nullable: login happens at the central Account Manager now, not here (see // src/modules/auth/auth.service.ts verifyHandoff()). Kept, not dropped, in case a fallback // local-login path is ever needed again. passwordHash String? fullName String isActive Boolean @default(true) roles UserRole[] branchRestrictions UserBranchRestriction[] journalEntries JournalEntry[] arInvoices ARInvoice[] @relation("ARInvoiceCreatedBy") arCreditNotes ARCreditNote[] @relation("ARCreditNoteCreatedBy") arReceipts ARReceipt[] @relation("ARReceiptCreatedBy") apInvoices APInvoice[] @relation("APInvoiceCreatedBy") apDebitNotes APDebitNote[] @relation("APDebitNoteCreatedBy") apPayments APPayment[] @relation("APPaymentCreatedBy") createdAt DateTime @default(now()) } model Role { id Int @id @default(autoincrement()) name String @unique permissions RolePermission[] users UserRole[] } model Permission { id Int @id @default(autoincrement()) key String @unique roles RolePermission[] } model RolePermission { roleId Int role Role @relation(fields: [roleId], references: [id]) permissionId Int permission Permission @relation(fields: [permissionId], references: [id]) @@id([roleId, permissionId]) } model UserRole { userId Int user User @relation(fields: [userId], references: [id]) roleId Int role Role @relation(fields: [roleId], references: [id]) @@id([userId, roleId]) } model UserBranchRestriction { id Int @id @default(autoincrement()) userId Int user User @relation(fields: [userId], references: [id]) branchSegmentValueId Int @@unique([userId, branchSegmentValueId]) }