feat(02-02): JournalEntry models + JournalEntryService

- Add JournalEntryStatus and JournalEntrySource enums to Prisma schema
- Add JournalEntry model with maker-checker fields, self-referential reversal relation, and audit timestamps
- Add JournalEntryLine model with debit/credit Decimal(15,2) fields
- Update Account model with journalEntryLines back-relation
- Update User model with createdJournalEntries and approvedJournalEntries back-relations
- Run migration: 20260304150817_add_journal_entry_models
- Add journalEntry and journalEntryLine to TENANT_SCOPED_MODELS in prisma-tenant.ts
- Create JournalEntryService with createEntry, approveEntry, reverseEntry, getAccountBalance, getTrialBalance
- Enforce debit=credit balance using integer cents comparison (avoids float issues)
- SYSTEM source entries auto-posted; MANUAL entries start as DRAFT for maker-checker
This commit is contained in:
kevin-asprec
2026-03-04 23:12:48 +08:00
parent 5c6969343b
commit 837b7f1979
4 changed files with 887 additions and 1 deletions

View File

@@ -68,6 +68,19 @@ enum BillingType {
POSTPAID
}
enum JournalEntryStatus {
DRAFT
PENDING_APPROVAL
APPROVED
POSTED
REVERSED
}
enum JournalEntrySource {
SYSTEM
MANUAL
}
// =============================================================================
// MODELS
// =============================================================================
@@ -119,6 +132,8 @@ model Account {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
journalEntryLines JournalEntryLine[]
/// Account codes must be unique within a tenant
@@unique([tenantId, code])
/// RLS-ready index — always present on tenant-scoped models
@@ -252,6 +267,11 @@ model User {
/// Accounting periods this user has closed (as administrator)
closedPeriods AccountingPeriod[]
/// Journal entries this user created (maker)
createdJournalEntries JournalEntry[] @relation("JournalEntryCreatedBy")
/// Journal entries this user approved (checker)
approvedJournalEntries JournalEntry[] @relation("JournalEntryApprovedBy")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -260,3 +280,68 @@ model User {
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
}
/// A JournalEntry records a double-entry accounting transaction.
/// All financial events in the system flow through this model.
/// Entries are IMMUTABLE — no updates or deletes. Corrections use reversing entries.
model JournalEntry {
id String @id @default(uuid())
tenantId String
/// Auto-generated sequential identifier per tenant per year (e.g., "JE-2026-0001")
entryNumber String
/// The accounting date (when the economic event occurred, not necessarily createdAt)
date DateTime
description String
source JournalEntrySource
status JournalEntryStatus @default(DRAFT)
/// Optional reference to source record (e.g., "Invoice", "Payment")
referenceType String?
/// ID of the source record (e.g., the invoice UUID)
referenceId String?
/// If this entry reverses another entry, this points to the original
reversesEntryId String? @unique
reversesEntry JournalEntry? @relation("JournalEntryReversal", fields: [reversesEntryId], references: [id])
/// If this entry has been reversed, this points to the reversing entry (back-reference)
reversedByEntry JournalEntry? @relation("JournalEntryReversal")
/// The user who created the entry (maker)
createdById String
createdBy User @relation("JournalEntryCreatedBy", fields: [createdById], references: [id])
/// The user who approved the entry (checker — for MANUAL entries)
approvedById String?
approvedBy User? @relation("JournalEntryApprovedBy", fields: [approvedById], references: [id])
approvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
lines JournalEntryLine[]
/// Entry numbers must be unique within a tenant
@@unique([tenantId, entryNumber])
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
@@index([tenantId, date])
@@index([referenceType, referenceId])
}
/// A single line (debit or credit) within a JournalEntry.
/// Every entry must have at least 2 lines, and sum(debit) = sum(credit).
model JournalEntryLine {
id String @id @default(uuid())
tenantId String
journalEntryId String
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id])
accountId String
account Account @relation(fields: [accountId], references: [id])
/// Debit amount for this line (0 if this is a credit line)
debit Decimal @default(0) @db.Decimal(15, 2)
/// Credit amount for this line (0 if this is a debit line)
credit Decimal @default(0) @db.Decimal(15, 2)
/// Optional line-level memo
description String?
createdAt DateTime @default(now())
/// RLS-ready index — always present on tenant-scoped models
@@index([tenantId])
@@index([accountId])
@@index([journalEntryId])
}