Skip to content

Data Models Reference

Complete reference for all accounting system data models.

Overview

The accounting system uses 6 core models to implement double-entry bookkeeping:

  1. ChartOfAccounts - Account structure and definitions
  2. JournalEntry - Groups related ledger entries
  3. LedgerEntry - Individual debit/credit records
  4. AccountBalance - Performance cache
  5. Reconciliation - Reconciliation tracking
  6. 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 + entryDate
  • walletTransactionId

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)
  • journalId
  • accountCode + status
  • entityId + entityType
  • entryDate

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)
  • startedAt
  • status

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 + resourceId
  • timestamp
  • actorId

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.ts

Next Steps

Internal use only - Keshless Payment Platform