@remade/nestjs-ledger
v0.1.0
Published
Double-entry accounting ledger module for NestJS with TypeORM
Maintainers
Readme
Ledger Module
Double-entry accounting (DEA) library module for NestJS. Provides a complete financial ledger with support for payment lifecycles (authorize, capture, void, settle, refund), deposits, withdrawals, wallet-to-wallet transfers, cross-currency FX conversion, hierarchical chart of accounts, a configurable fee engine, transactional event publishing, and audit/reconciliation tools.
Installation
Import LedgerModule into your host application:
import { LedgerModule } from '@app/ledger';
@Module({
imports: [
LedgerModule.forRoot({
controllers: { enabled: true },
connectionName: 'default',
holdTtlDefaultSeconds: 604800, // 7 days
}),
],
})
export class AppModule {}Documentation
Full documentation is available in docs/:
| Document | Description | |---|---| | Overview & Quick Start | Architecture, principles, module options | | Entities | All 11 entities with fields, relationships, constraints | | Payment Flows | Payment lifecycle, deposit, withdraw, transfer, FX conversion | | Fee Engine | Fee resolution, calculation methods, splits, tiered volume | | Events | Transactional outbox pattern, event types, consumer guidance | | Audit & Reconciliation | Trial balance, balance reconstruction, account statements | | API Reference | All REST endpoints with request/response examples |
Directory Structure
libs/ledger/src/
index.ts # Barrel exports
ledger.module.ts # Dynamic module (forRoot / forRootAsync)
ledger.constants.ts # Injection tokens
enums/ # 10 enum files (account type, transaction status, etc.)
entities/ # 11 TypeORM entities
dto/ # 20 validation DTOs grouped by resource
services/ # 13 business logic services
engine/ # 3 pure calculation/logic classes (no DB)
controllers/ # 6 REST API controllersEntities
| Entity | Table | Description |
|---|---|---|
| Currency | ledger_currencies | Currency definitions (fiat + crypto). PK is code (VARCHAR). |
| Account | ledger_accounts | Chart of accounts with hierarchy, 7 account types, materialized balances (available/held/pending). |
| JournalEntry | ledger_journal_entries | Immutable double-entry records. Each entry debits one account and credits another. |
| Transaction | ledger_transactions | Payment lifecycle tracking with state machine and FX conversion fields. |
| ActiveHold | ledger_active_holds | Authorization holds with TTL and partial capture support. |
| FeeSchedule | ledger_fee_schedules | Fee configuration with 4 calculation methods, priority resolution, and fee splitting. |
| FeeTier | ledger_fee_tiers | Volume-based tier brackets for tiered fee schedules. |
| ExchangeRate | ledger_exchange_rates | Currency pair rates with validity periods and auto-computed inverse. |
| HoldTtlPolicy | ledger_hold_ttl_policies | Priority-based hold TTL configuration (global/merchant/type/currency). |
| AccountStatement | ledger_account_statements | Period-end balance snapshots for audit. |
| LedgerEvent | ledger_events | Transactional outbox for reliable event publishing. |
All monetary amounts use numeric(78,0) (BigInt-compatible), stored as string in TypeScript.
API Endpoints
Accounts (/accounts)
| Method | Route | Description |
|---|---|---|
| POST | /accounts | Create a new account |
| GET | /accounts | List accounts (paginated, filterable) |
| GET | /accounts/:id | Get account by ID |
| GET | /accounts/:id/balance | Get account balances |
| GET | /accounts/:id/subtree | Get subtree balance (recursive roll-up) |
| GET | /accounts/owner/:ownerType/:ownerId | Get accounts by owner |
| POST | /accounts/:id/deactivate | Deactivate an account |
Payments (/payments)
| Method | Route | Description |
|---|---|---|
| POST | /payments/authorize | Create authorization (places hold) |
| POST | /payments/:id/capture | Capture authorized amount (full or partial) |
| POST | /payments/:id/void | Void authorization (releases hold) |
| POST | /payments/:id/settle | Settle captured amount to merchant |
| POST | /payments/:id/refund | Refund settled amount (full or partial) |
| POST | /payments/deposit | Fund a wallet from house cash |
| POST | /payments/withdraw | Withdraw from wallet to house cash |
| POST | /payments/transfer | Wallet-to-wallet same-currency transfer |
| POST | /payments/convert | Cross-currency FX conversion |
Fee Schedules (/fee-schedules)
| Method | Route | Description |
|---|---|---|
| POST | /fee-schedules | Create a fee schedule |
| GET | /fee-schedules | List fee schedules (paginated) |
| GET | /fee-schedules/:id | Get fee schedule with tiers |
| PATCH | /fee-schedules/:id | Update fee schedule |
| POST | /fee-schedules/:id/deactivate | Deactivate a fee schedule |
| POST | /fee-schedules/:id/tiers | Add a volume tier |
| DELETE | /fee-schedules/tiers/:tierId | Remove a volume tier |
| POST | /fee-schedules/resolve | Preview fee resolution and calculation |
Exchange Rates (/exchange-rates)
| Method | Route | Description |
|---|---|---|
| GET | /exchange-rates/:from/:to | Get current exchange rate |
| GET | /exchange-rates/:from/:to/history | Get rate history (paginated) |
Hold TTL Policies (/hold-policies)
| Method | Route | Description |
|---|---|---|
| POST | /hold-policies | Create a TTL policy |
| GET | /hold-policies | List policies (paginated) |
| GET | /hold-policies/:id | Get policy by ID |
| PUT | /hold-policies/:id | Update a policy |
| POST | /hold-policies/:id/deactivate | Deactivate a policy |
Admin / Audit
| Method | Route | Description |
|---|---|---|
| GET | /ledger/trial-balance | SUM(debits) vs SUM(credits) per currency |
| GET | /ledger/accounts | Full chart of accounts tree |
| GET | /ledger/accounts/:id/subtree | Subtree roll-up balance |
| POST | /ledger/reconcile | Trigger balance reconstruction |
| GET | /ledger/fx-suspense-check | Check FX suspense zero-balance |
| POST | /admin/statements/generate | Generate period-end statement |
| GET | /admin/events | List ledger events (filtered) |
| POST | /admin/events/:id/replay | Republish an event |
Exported Services
| Service | Key Methods |
|---|---|
| CurrencyService | create, findByCode, findAll, deactivate |
| AccountService | create, findById, findAll, findByOwner, getBalance, getSubtreeBalance, deactivate |
| JournalEntryService | createEntry (within a transaction manager) |
| TransactionService | create, updateStatus, findById, findByIdempotencyKey |
| PaymentService | authorize, capture, void, settle, refund, deposit, withdraw, transfer |
| HoldService | createHold, captureHold, releaseHold, findActiveByTransaction |
| FeeService | create, update, findById, findAll, deactivate, addTier, removeTier, resolve, calculateFee, applyFee |
| ExchangeRateService | create, findCurrentRate, findAll, getHistory |
| HoldTtlPolicyService | create, update, findById, findAll, deactivate, resolveTtl |
| LedgerEventService | emit, findPending, markPublished, markFailed, findByAggregate, findAll |
| AccountStatementService | generate, findByAccount, findByPeriod |
| ConversionService | convert |
| AuditService | trialBalance, reconstructBalance, fxSuspenseCheck, chartOfAccounts |
Key Design Decisions
- Immutable journal entries: Once created, journal entries are never modified or deleted. Corrections are made via reversal entries.
- Materialized balances: Account balances (available, held, pending) are stored directly on the account and updated atomically with each journal entry. This avoids expensive aggregation queries at read time.
- Deterministic lock ordering: The
BalanceUpdatersorts account IDs before acquiringFOR UPDATElocks to prevent deadlocks in concurrent transactions. - BigInt arithmetic: All monetary calculations use JavaScript
BigIntto avoid floating-point precision issues. Amounts are stored asnumeric(78,0)in PostgreSQL, supporting values up to 10^78. - Ceiling division for fees: Fee percentage calculations round UP (ceiling), ensuring the platform never undercharges.
- Idempotency keys: All payment operations support idempotency keys. Terminal transactions are replayed; in-progress duplicates return 409 Conflict.
- Fee resolution priority: Fee schedules are resolved by priority (highest wins), with merchant-specific schedules preferred over global ones. Schedules can be scoped by currency and transaction type.
- Fee splitting: Fee schedules support splitting between platform and partner accounts via basis-point shares that must sum to 10,000.
- Transactional outbox: Events are written in the same DB transaction as journal entries, ensuring no events are lost. A separate poller publishes to the external broker.
- FX via suspense accounts: Cross-currency operations route through FX suspense accounts, preserving the same-currency invariant on every journal entry.
- Hold TTL policies: Hierarchical hold TTL resolution (per-transaction > per-merchant+type+currency > global) with priority-based lookup.
