# Ledger Correctness Boundaries - Implementation Summary

This document summarizes the implementation of ledger correctness boundaries that enforce the separation between authoritative ledger operations and external synchronization.

## Overview

DBIS Core maintains an **authoritative ledger** (issuance, settlement, balances) while also orchestrating **dual-ledger synchronization** with external SCB ledgers. This requires two different correctness regimes:

1. **Authoritative ledger correctness** (must be atomic, invariant-safe)
2. **External synchronization correctness** (must be idempotent, replayable, eventually consistent)

## Architecture Changes

### 1. Atomic Ledger Posting (Postgres as Ledger Engine)

**Problem**: Balance updates were happening in separate Prisma calls, risking race conditions and inconsistent state.

**Solution**: Created `post_ledger_entry()` SQL function that:
- Enforces idempotency via unique constraint on `(ledger_id, reference_id)`
- Updates balances atomically within the same transaction as entry creation
- Uses deadlock-safe lock ordering
- Computes block hash with hash chaining
- Validates sufficient funds at DB level

**Location**: `db/migrations/005_post_ledger_entry.sql`

### 2. Dual-Ledger Outbox Pattern

**Problem**: Original implementation posted to SCB ledger first, then DBIS. If SCB was unavailable, DBIS couldn't commit. This violated "DBIS is authoritative" principle.

**Solution**: Implemented transactional outbox pattern:
- DBIS commits first (authoritative)
- Outbox event created in same transaction
- Async worker processes outbox jobs
- Idempotent retries with exponential backoff
- State machine enforces valid transitions

**Files**:
- `db/migrations/002_dual_ledger_outbox.sql` - Outbox table
- `db/migrations/003_outbox_state_machine.sql` - State machine constraints
- `src/workers/dual-ledger-outbox.worker.ts` - Worker service
- `src/workers/run-dual-ledger-outbox.ts` - Worker runner

### 3. Guarded Access Module

**Problem**: Any code could directly mutate `ledger_entries` or `bank_accounts`, bypassing correctness guarantees.

**Solution**: Created `LedgerPostingModule` that is the **only** allowed path to mutate ledger:
- All mutations go through atomic SQL function
- Direct balance updates are banned
- Singleton pattern enforces single access point

**Location**: `src/core/ledger/ledger-posting.module.ts`

### 4. Refactored GSS Master Ledger Service

**Changes**:
- **DBIS-first**: Posts to DBIS ledger first (authoritative)
- **Transactional**: DBIS post + outbox creation + master record in single transaction
- **Non-blocking**: Returns immediately; SCB sync happens async
- **Explicit states**: `DBIS_COMMITTED` → `SETTLED` (when SCB sync completes)

**Location**: `src/core/settlement/gss/gss-master-ledger.service.ts`

## Migration Files

All migrations are in `db/migrations/`:

1. **001_ledger_idempotency.sql** - Unique constraint on `(ledger_id, reference_id)`
2. **002_dual_ledger_outbox.sql** - Outbox table with indexes
3. **003_outbox_state_machine.sql** - Status transition enforcement
4. **004_balance_constraints.sql** - Balance integrity constraints
5. **005_post_ledger_entry.sql** - Atomic posting function

## State Machine

### Outbox States

```
QUEUED → SENT → ACKED → FINALIZED
  ↓       ↓       ↓
FAILED ← FAILED ← FAILED
  ↑
(retry)
```

### Master Ledger States

- `PENDING` - Initial state
- `DBIS_COMMITTED` - DBIS ledger posted, SCB sync queued
- `SETTLED` - Both ledgers synchronized
- `FAILED` - Posting failed

## Key Constraints

### Database Level

1. **Idempotency**: `UNIQUE (ledger_id, reference_id)` on `ledger_entries`
2. **Balance integrity**: 
   - `available_balance >= 0`
   - `reserved_balance >= 0`
   - `available_balance <= balance`
   - `(available_balance + reserved_balance) <= balance`
3. **State transitions**: Trigger enforces valid outbox status transitions

### Application Level

1. **Guarded access**: Only `LedgerPostingModule` can mutate ledger
2. **Atomic operations**: All posting via SQL function
3. **Transactional outbox**: Outbox creation in same transaction as posting

## Usage

### Posting to Master Ledger

```typescript
import { gssMasterLedgerService } from '@/core/settlement/gss/gss-master-ledger.service';

const result = await gssMasterLedgerService.postToMasterLedger({
  nodeId: 'SSN-1',
  sourceBankId: 'SCB-1',
  destinationBankId: 'SCB-2',
  amount: '1000.00',
  currencyCode: 'USD',
  assetType: 'fiat',
  sovereignSignature: '...',
}, 'my-reference-id');

// Returns immediately with DBIS hash
// SCB sync happens async via outbox worker
```

### Running Outbox Worker

```bash
# Run worker process
npm run worker:dual-ledger-outbox

# Or use process manager
pm2 start src/workers/run-dual-ledger-outbox.ts
```

## Testing

### Verify Migrations

```sql
-- Check idempotency constraint
SELECT constraint_name 
FROM information_schema.table_constraints 
WHERE table_name = 'ledger_entries' 
  AND constraint_name LIKE '%reference%';

-- Check outbox table
SELECT COUNT(*) FROM dual_ledger_outbox;

-- Test posting function
SELECT * FROM post_ledger_entry(
  'Test'::TEXT,
  'account1'::TEXT,
  'account2'::TEXT,
  100::NUMERIC,
  'USD'::TEXT,
  'fiat'::TEXT,
  'Type_A'::TEXT,
  'test-ref-123'::TEXT,
  NULL::NUMERIC,
  NULL::JSONB
);
```

### Verify State Machine

```sql
-- Try invalid transition (should fail)
UPDATE dual_ledger_outbox 
SET status = 'QUEUED' 
WHERE status = 'FINALIZED';
-- ERROR: Invalid outbox transition: FINALIZED -> QUEUED
```

## Next Steps

1. **Apply migrations** in order (see `db/migrations/README.md`)
2. **Update Prisma schema** (already done - `dual_ledger_outbox` model added)
3. **Deploy worker** to process outbox jobs
4. **Implement SCB API client** in `DualLedgerOutboxWorker.callScbLedgerApi()`
5. **Add monitoring** for outbox queue depth and processing latency
6. **Add reconciliation** job to detect and fix sync failures

## Breaking Changes

### API Changes

- `postToMasterLedger()` now returns immediately with `dualCommit: false`
- `sovereignLedgerHash` is `null` initially (populated by worker)
- Status is `DBIS_COMMITTED` instead of `settled` initially

### Database Changes

- New constraint on `ledger_entries` (idempotency)
- New balance constraints (may fail if data is inconsistent)
- New `dual_ledger_outbox` table

### Code Changes

- Direct use of `ledgerService.postDoubleEntry()` for GSS should be replaced with `ledgerPostingModule.postEntry()`
- Direct balance updates via Prisma are now banned (use `ledgerPostingModule`)

## Rollback Plan

If needed, migrations can be rolled back:

```sql
-- Drop function
DROP FUNCTION IF EXISTS post_ledger_entry(...);

-- Drop outbox table
DROP TABLE IF EXISTS dual_ledger_outbox CASCADE;

-- Remove constraints
ALTER TABLE ledger_entries 
  DROP CONSTRAINT IF EXISTS ledger_entries_unique_ledger_reference;

ALTER TABLE bank_accounts 
  DROP CONSTRAINT IF EXISTS bank_accounts_reserved_nonnegative,
  DROP CONSTRAINT IF EXISTS bank_accounts_available_nonnegative,
  DROP CONSTRAINT IF EXISTS bank_accounts_balance_consistency;
```

## References

- Architecture discussion: See user query about "hard mode" answer
- Transactional Outbox Pattern: https://microservices.io/patterns/data/transactional-outbox.html
- Prisma transaction docs: https://www.prisma.io/docs/concepts/components/prisma-client/transactions
