npm package: @remade/nestjs-ledger
Description: Double-entry accounting ledger module for NestJS with TypeORM
Author: remade
Latest version: 0.1.0

# 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:

```typescript
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/`](./docs/):

| Document | Description |
|---|---|
| [Overview & Quick Start](./docs/README.md) | Architecture, principles, module options |
| [Entities](./docs/entities.md) | All 11 entities with fields, relationships, constraints |
| [Payment Flows](./docs/payment-flows.md) | Payment lifecycle, deposit, withdraw, transfer, FX conversion |
| [Fee Engine](./docs/fee-engine.md) | Fee resolution, calculation methods, splits, tiered volume |
| [Events](./docs/events.md) | Transactional outbox pattern, event types, consumer guidance |
| [Audit & Reconciliation](./docs/audit.md) | Trial balance, balance reconstruction, account statements |
| [API Reference](./docs/api-reference.md) | 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 controllers
```

## Entities

| 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 `BalanceUpdater` sorts account IDs before acquiring `FOR UPDATE` locks to prevent deadlocks in concurrent transactions.
- **BigInt arithmetic**: All monetary calculations use JavaScript `BigInt` to avoid floating-point precision issues. Amounts are stored as `numeric(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.