Stock is scoped to a catalog + cost centers only, per direction that physical GRV/stock-take/recipe transactions belong in an external system (Matrix), not this accounting app. Stock Control/GRN/Variance/ PPV GL accounts are now plain postable accounts rather than locked control accounts. Also fixes session loss on page refresh: auth stores now persist to localStorage, and ProtectedRoute waits for rehydration before deciding to redirect to login. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWqiUSojMLUoN7mrJqdEbN
726 lines
26 KiB
Plaintext
726 lines
26 KiB
Plaintext
// 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])
|
|
}
|