> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vultlocal.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ledger

> Transaction processing engine for atomic payments

# Ledger

The Ledger is the core transaction processing engine in Wallet-Core, responsible for atomic payments, account management, and transaction integrity.

## Core Principles

<CardGroup cols={2}>
  <Card title="Atomicity" icon="atom">
    All operations complete entirely or not at all
  </Card>

  <Card title="Idempotency" icon="repeat">
    Duplicate requests return existing results
  </Card>

  <Card title="Consistency" icon="check-double">
    Balances always reflect actual state
  </Card>

  <Card title="Auditability" icon="file-invoice">
    Every operation is logged for traceability
  </Card>
</CardGroup>

## Payment Flow

```mermaid theme={null}
sequenceDiagram
    participant C as Client
    participant L as Ledger
    participant DB as Database

    C->>L: ExecutePayment(request)
    L->>DB: Check idempotency (request_id)
    
    alt Duplicate Request
        DB-->>L: Existing transaction
        L-->>C: Return existing
    else New Request
        L->>DB: BEGIN transaction
        L->>DB: Get/create sender account
        L->>DB: Get/create recipient account
        L->>DB: Validate sender balance
        
        alt Insufficient Balance
            L->>DB: ROLLBACK
            L-->>C: Error: insufficient balance
        else Sufficient Balance
            L->>DB: Debit sender
            L->>DB: Credit recipient
            L->>DB: Insert transaction
            L->>DB: Insert audit log
            L->>DB: COMMIT
            L-->>C: Success + transaction
        end
    end
```

## Idempotency

Every payment request requires a unique `request_id`. If the same `request_id` is submitted again, the ledger returns the existing transaction without re-executing.

### Implementation

```go theme={null}
func (l *Service) ExecutePayment(ctx context.Context, req *PaymentRequest) (*Transaction, error) {
    // Check for existing transaction with same request_id
    existing, err := l.txRepo.GetByRequestID(ctx, req.RequestID)
    if err == nil && existing != nil {
        // Return cached result
        return existing, nil
    }
    
    // Proceed with new transaction
    // ...
}
```

### Client Usage

```bash theme={null}
# First request
curl -X POST /api/v1/payments \
  -H "X-Request-ID: uuid-12345" \
  -d '{"user_id": "u1", "recipient": "u2", "amount": 1000}'
# Returns: transaction_id = txn_abc

# Duplicate request (same X-Request-ID)
curl -X POST /api/v1/payments \
  -H "X-Request-ID: uuid-12345" \
  -d '{"user_id": "u1", "recipient": "u2", "amount": 1000}'
# Returns: same transaction_id = txn_abc (no new transaction created)
```

## Account Model

Accounts are created per user and currency combination.

### Schema

```sql theme={null}
CREATE TABLE accounts (
    id TEXT PRIMARY KEY,
    user_id TEXT NOT NULL,
    currency TEXT NOT NULL,
    balance BIGINT NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(user_id, currency)
);

CREATE INDEX idx_accounts_user_id ON accounts(user_id);
CREATE INDEX idx_accounts_user_currency ON accounts(user_id, currency);
```

### Lazy Creation

Accounts are created automatically when needed:

```go theme={null}
func (l *Service) getOrCreateAccount(ctx context.Context, tx *sql.Tx, userID, currency string) (*Account, error) {
    // Try to get existing
    acc, err := l.accRepo.GetByUserAndCurrency(ctx, userID, currency)
    if err == nil {
        return acc, nil
    }
    
    // Create new account
    newAcc := &Account{
        ID:        generateID(),
        UserID:    userID,
        Currency:  currency,
        Balance:   0,
        CreatedAt: time.Now(),
    }
    
    if err := l.accRepo.Create(ctx, tx, newAcc); err != nil {
        return nil, err
    }
    
    return newAcc, nil
}
```

## Transaction Model

### Schema

```sql theme={null}
CREATE TABLE transactions (
    id TEXT PRIMARY KEY,
    request_id TEXT UNIQUE NOT NULL,
    user_id TEXT NOT NULL,
    recipient_id TEXT NOT NULL,
    amount BIGINT NOT NULL,
    currency TEXT NOT NULL,
    status TEXT NOT NULL,
    memo TEXT,
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    completed_at TIMESTAMP
);

CREATE INDEX idx_transactions_user_id ON transactions(user_id);
CREATE INDEX idx_transactions_request_id ON transactions(request_id);
CREATE INDEX idx_transactions_created_at ON transactions(created_at);
```

### Transaction Statuses

| Status      | Description                              |
| ----------- | ---------------------------------------- |
| `PENDING`   | Transaction initiated, not yet processed |
| `COMPLETED` | Successfully executed                    |
| `FAILED`    | Processing failed                        |
| `REVERSED`  | Transaction was reversed                 |
| `EXPIRED`   | Pending transaction expired              |

## Balance Operations

### Get Balance

```go theme={null}
func (l *Service) GetBalance(ctx context.Context, userID, currency string) (*Balance, error) {
    acc, err := l.accRepo.GetByUserAndCurrency(ctx, userID, currency)
    if err != nil {
        if errors.Is(err, ErrAccountNotFound) {
            // Return zero balance for non-existent account
            return &Balance{
                UserID:   userID,
                Currency: currency,
                Balance:  0,
            }, nil
        }
        return nil, err
    }
    
    return &Balance{
        UserID:   acc.UserID,
        Currency: acc.Currency,
        Balance:  acc.Balance,
        AsOf:     time.Now(),
    }, nil
}
```

### Debit/Credit

```go theme={null}
func (l *Service) debit(ctx context.Context, tx *sql.Tx, accountID string, amount int64) error {
    query := `
        UPDATE accounts 
        SET balance = balance - $1, updated_at = NOW()
        WHERE id = $2 AND balance >= $1
    `
    
    result, err := tx.ExecContext(ctx, query, amount, accountID)
    if err != nil {
        return err
    }
    
    rows, _ := result.RowsAffected()
    if rows == 0 {
        return ErrInsufficientBalance
    }
    
    return nil
}

func (l *Service) credit(ctx context.Context, tx *sql.Tx, accountID string, amount int64) error {
    query := `
        UPDATE accounts 
        SET balance = balance + $1, updated_at = NOW()
        WHERE id = $2
    `
    
    _, err := tx.ExecContext(ctx, query, amount, accountID)
    return err
}
```

## Audit Logging

Every transaction creates an audit log entry:

```go theme={null}
func (l *Service) auditLog(ctx context.Context, tx *sql.Tx, txn *Transaction) error {
    entry := &AuditLog{
        ID:            generateID(),
        TransactionID: txn.ID,
        EventType:     "PAYMENT_EXECUTED",
        Actor:         txn.UserID,
        Details:       map[string]interface{}{
            "amount":     txn.Amount,
            "currency":   txn.Currency,
            "recipient":  txn.RecipientID,
        },
        Timestamp: time.Now(),
    }
    
    return l.auditRepo.Create(ctx, tx, entry)
}
```

### Audit Log Schema

```sql theme={null}
CREATE TABLE audit_log (
    id TEXT PRIMARY KEY,
    transaction_id TEXT,
    event_type TEXT NOT NULL,
    actor TEXT,
    details JSONB,
    timestamp TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_audit_log_transaction_id ON audit_log(transaction_id);
CREATE INDEX idx_audit_log_event_type ON audit_log(event_type);
CREATE INDEX idx_audit_log_timestamp ON audit_log(timestamp);
```

## Transaction Queries

### List Transactions

```go theme={null}
func (l *Service) ListTransactions(ctx context.Context, req *ListRequest) (*TransactionList, error) {
    query := `
        SELECT id, request_id, user_id, recipient_id, amount, currency, status, memo, metadata, created_at, completed_at
        FROM transactions
        WHERE user_id = $1 OR recipient_id = $1
        ORDER BY created_at DESC
        LIMIT $2 OFFSET $3
    `
    
    rows, err := l.db.QueryContext(ctx, query, req.UserID, req.Limit, req.Offset)
    // ... process rows
}
```

### Account Statement

```go theme={null}
func (l *Service) GetAccountStatement(ctx context.Context, req *StatementRequest) (*Statement, error) {
    query := `
        SELECT 
            id, 
            CASE WHEN user_id = $1 THEN -amount ELSE amount END as delta,
            created_at,
            memo
        FROM transactions
        WHERE (user_id = $1 OR recipient_id = $1)
          AND currency = $2
          AND created_at BETWEEN $3 AND $4
        ORDER BY created_at
    `
    
    // Execute and build statement with running balance
    // ...
}
```

## Reconciliation

Wallet-Core includes VULT reconciliation for external processor integration:

```go theme={null}
// services/vult_reconciliation.go
func (s *VULTReconciliationService) Reconcile(ctx context.Context) (*ReconciliationResult, error) {
    // Fetch external transactions from VULT
    external, err := s.vult.GetTransactions(ctx, lastReconcileTime)
    if err != nil {
        return nil, err
    }
    
    // Compare with internal records
    internal, err := s.txRepo.GetByTimeRange(ctx, lastReconcileTime, time.Now())
    if err != nil {
        return nil, err
    }
    
    // Find discrepancies
    mismatches := findMismatches(external, internal)
    
    // Create alerts for mismatches
    for _, m := range mismatches {
        s.compliance.CreateAlert(ctx, "RECONCILIATION_MISMATCH", m)
    }
    
    return &ReconciliationResult{
        Processed:  len(external),
        Matched:    len(external) - len(mismatches),
        Mismatches: len(mismatches),
    }, nil
}
```

## Error Handling

Common ledger errors:

| Error                    | Code                   | Description                        |
| ------------------------ | ---------------------- | ---------------------------------- |
| `ErrInsufficientBalance` | `INSUFFICIENT_BALANCE` | Sender balance too low             |
| `ErrAccountNotFound`     | `ACCOUNT_NOT_FOUND`    | Account doesn't exist              |
| `ErrDuplicateRequest`    | `DUPLICATE_REQUEST`    | Request ID already used            |
| `ErrInvalidAmount`       | `INVALID_AMOUNT`       | Amount must be positive            |
| `ErrCurrencyMismatch`    | `CURRENCY_MISMATCH`    | Sender/recipient currency mismatch |

## Next Steps

<CardGroup cols={2}>
  <Card title="Configuration" icon="gear" href="/wallet-core/configuration">
    Database and server configuration
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference/wallet/payments">
    Payment API documentation
  </Card>
</CardGroup>
