Data Models Reference
Complete reference for all accounting system data models.
Overview
The accounting system uses 6 core models to implement double-entry bookkeeping:
- ChartOfAccounts - Account structure and definitions
- JournalEntry - Groups related ledger entries
- LedgerEntry - Individual debit/credit records
- AccountBalance - Performance cache
- Reconciliation - Reconciliation tracking
- AuditLog - Tamper-proof audit trail
1. ChartOfAccounts
Defines the hierarchical account structure.
Schema:
typescript
{
code: string, // Unique account code (e.g., "1110")
name: string, // Account name (e.g., "User Wallets")
type: AccountType, // ASSET, LIABILITY, EQUITY, REVENUE, EXPENSE
subtype: string, // Subcategory (e.g., "Current Assets")
normalBalance: 'DEBIT' | 'CREDIT',
currency: 'E', // Locked to Emalageni
description?: string,
parentAccount?: string, // Parent account code for hierarchy
isActive: boolean,
createdAt: Date,
updatedAt: Date
}Indexes:
code(unique)type + isActive
Example:
json
{
"code": "1110",
"name": "User Wallets",
"type": "ASSET",
"subtype": "Current Assets",
"normalBalance": "DEBIT",
"currency": "E",
"isActive": true
}2. JournalEntry
Groups related ledger entries representing a single business transaction.
Schema:
typescript
{
journalId: string, // Unique identifier (JE-XXXXXX)
entryDate: Date,
description: string,
status: 'PENDING' | 'POSTED' | 'REVERSED' | 'CANCELLED',
totalAmount: number, // Total transaction amount
currency: 'E',
walletTransactionId?: string, // Link to WalletTransaction
reversalOf?: string, // Journal ID this reverses
reversedBy?: string, // Journal ID that reversed this
metadata: {
actorId: string,
ipAddress?: string,
requestId?: string,
amlFlagged?: boolean,
autoPost?: boolean,
postedBy?: string,
postedAt?: Date,
cancelledBy?: string,
cancelledAt?: Date,
cancelReason?: string,
},
createdAt: Date,
updatedAt: Date
}Indexes:
journalId(unique)status + entryDatewalletTransactionId
Example:
json
{
"journalId": "JE-000123",
"entryDate": "2026-01-22T10:30:00Z",
"description": "P2P Transfer - User A to User B",
"status": "POSTED",
"totalAmount": 100.00,
"currency": "E",
"walletTransactionId": "67f2ca38...",
"metadata": {
"actorId": "68e3ba49...",
"ipAddress": "192.168.1.100",
"autoPost": true
}
}3. LedgerEntry
Individual debit or credit records that make up a journal entry.
Schema:
typescript
{
entryId: string, // Unique identifier (LE-XXXXXX)
journalId: string, // Parent journal entry
accountCode: string, // Chart of Accounts reference
type: 'DEBIT' | 'CREDIT',
amount: number,
currency: 'E',
entityId?: string, // User/Vendor reference
entityType?: 'User' | 'Vendor' | 'System',
description?: string,
status: 'PENDING' | 'POSTED' | 'REVERSED',
entryDate: Date,
runningBalance?: number, // For account statements
metadata: {
counterpartyId?: string,
counterpartyType?: string,
reference?: string,
},
createdAt: Date,
updatedAt: Date
}Immutability:
- Once
status = 'POSTED', cannot be modified or deleted - Enforced via Prisma middleware
Indexes:
entryId(unique)journalIdaccountCode + statusentityId + entityTypeentryDate
Example:
json
{
"entryId": "LE-000456",
"journalId": "JE-000123",
"accountCode": "1110",
"type": "DEBIT",
"amount": 100.00,
"currency": "E",
"entityId": "68e3ba49...",
"entityType": "User",
"status": "POSTED",
"entryDate": "2026-01-22T10:30:00Z"
}4. AccountBalance
Denormalized cache for performance optimization.
Schema:
typescript
{
accountCode: string,
currency: 'E',
debitTotal: number, // Sum of all debits
creditTotal: number, // Sum of all credits
balance: number, // debitTotal - creditTotal
lastReconciledAt?: Date,
lastReconciledBalance?: number,
lastEntryId?: string, // Last ledger entry processed
version: number, // Optimistic locking
updatedAt: Date
}Optimistic Locking:
typescript
await AccountBalance.updateOne(
{ accountCode: '1110', version: currentVersion },
{ $inc: { balance: 100, version: 1 } }
);Indexes:
accountCode + currency(unique compound)
Example:
json
{
"accountCode": "1110",
"currency": "E",
"debitTotal": 50000.00,
"creditTotal": 45000.00,
"balance": 5000.00,
"lastReconciledAt": "2026-01-22T02:00:00Z",
"lastReconciledBalance": 5000.00,
"version": 142
}5. Reconciliation
Tracks reconciliation runs and discrepancies.
Schema:
typescript
{
reconciliationId: string, // REC-XXXXXX
startedAt: Date,
completedAt?: Date,
status: 'IN_PROGRESS' | 'COMPLETED' | 'FAILED',
scope: 'USER' | 'VENDOR' | 'TRIAL_BALANCE' | 'FULL',
results: {
totalChecked: number,
totalReconciled: number,
totalDiscrepancies: number,
discrepancies: [{
entityId: string,
entityType: 'User' | 'Vendor',
walletBalance: number,
ledgerBalance: number,
difference: number,
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'
}],
trialBalance?: {
isBalanced: boolean,
totalDebits: number,
totalCredits: number,
difference: number
}
},
performedBy: string | 'system',
createdAt: Date
}Indexes:
reconciliationId(unique)startedAtstatus
Example:
json
{
"reconciliationId": "REC-000042",
"startedAt": "2026-01-22T02:00:00Z",
"completedAt": "2026-01-22T02:05:32Z",
"status": "COMPLETED",
"scope": "FULL",
"results": {
"totalChecked": 1523,
"totalReconciled": 1520,
"totalDiscrepancies": 3
},
"performedBy": "system"
}6. AuditLog
Tamper-proof audit trail with SHA-256 hash chaining.
Schema:
typescript
{
entryId: string, // AL-XXXXXX
timestamp: Date,
eventType: 'JOURNAL_CREATED' | 'ENTRY_POSTED' | 'ENTRY_REVERSED' | ...,
actorId: string,
actorType: 'User' | 'Vendor' | 'Admin' | 'System',
resourceType: 'JournalEntry' | 'LedgerEntry' | 'AccountBalance',
resourceId: string,
action: 'CREATE' | 'UPDATE' | 'DELETE',
changes: {
before?: any,
after?: any
},
metadata: {
ipAddress?: string,
requestId?: string,
userAgent?: string,
},
previousHash?: string, // Hash of previous audit log entry
currentHash: string, // SHA-256 hash of this entry
createdAt: Date
}Hash Calculation:
typescript
const dataString = JSON.stringify({
entryId,
timestamp,
eventType,
actorId,
resourceType,
resourceId,
changes,
previousHash
});
currentHash = crypto
.createHash('sha256')
.update(dataString)
.digest('hex');Immutability:
- Cannot be updated or deleted (enforced via middleware)
- Any tampering breaks hash chain
Indexes:
entryId(unique)resourceType + resourceIdtimestampactorId
Example:
json
{
"entryId": "AL-001234",
"timestamp": "2026-01-22T10:30:00.123Z",
"eventType": "ENTRY_POSTED",
"actorId": "68e3ba49...",
"actorType": "User",
"resourceType": "JournalEntry",
"resourceId": "JE-000123",
"action": "UPDATE",
"changes": {
"before": { "status": "PENDING" },
"after": { "status": "POSTED" }
},
"previousHash": "a3f5d2...",
"currentHash": "b7e9c1..."
}Relationships
ChartOfAccounts
↓
LedgerEntry → JournalEntry → WalletTransaction
↓
AccountBalance
↓
Reconciliation
All changes → AuditLog (immutable)Model Locations
All models are located in:
src/models/accounting/
├── chartOfAccounts.model.ts
├── journalEntry.model.ts
├── ledgerEntry.model.ts
├── accountBalance.model.ts
├── reconciliation.model.ts
└── auditLog.model.tsNext Steps
- API Reference - Service methods using these models
- Architecture - How models interact
- Accounting System - Understanding the data flow