# Wallet and Ledger Operations

## Purpose

Phase 10 makes the append-only double-entry ledger the only financial authority. `wallet_balance_snapshots` is a rebuildable read projection; client input and snapshot rows never decide a balance, reward, hold, refund, or payout.

All money values use integer minor units in MySQL `BIGINT` columns and decimal strings in JavaScript and JSON. For a two-decimal currency, `1050` represents 10.50 units. Floating-point values are never accepted by the ledger.

## Posting model

Every transaction contains at least two entries, total debit must equal total credit, and each account may appear only once. The service accepts only an allowlisted transaction type and an allowlisted account selector.

| Operation         | Debit                                  | Credit                                 |
| ----------------- | -------------------------------------- | -------------------------------------- |
| Task reward       | System task reward pool                | User available                         |
| Referral reward   | System referral reward pool            | User available                         |
| Free reward       | System free-reward pool                | User available                         |
| Withdrawal hold   | User available                         | User held                              |
| Withdrawal payout | User held                              | System payout clearing                 |
| Withdrawal refund | User held                              | User available                         |
| Reversal          | Exact opposite of every original entry | Exact opposite of every original entry |

The free-reward account supports only the project's already database-constrained free-only feature. Phase 10 adds no purchase, deposit, stake, paid chance, or public claim endpoint.

## Atomic posting sequence

1. Validate transaction type, reference, metadata, amount strings, account selectors, and balanced totals before opening the transaction.
2. HMAC-digest the caller's idempotency key and calculate a canonical request fingerprint.
3. Insert or recover the pending transaction.
4. Ensure required user accounts exist.
5. Lock all involved account rows in ascending numeric ID order.
6. Rebuild current account balances from posted/reversed ledger entries under those locks.
7. Reject a posting that would make any user-owned account negative.
8. Append all entries and verify their persisted debit total, credit total, and count.
9. Atomically move the transaction from `pending` to `posted`.
10. Rebuild affected user snapshots from the authoritative view and commit.

The transaction helper retries only bounded deadlock-class failures. Every other failure rolls the complete posting back.

## Idempotency and reversals

Raw idempotency keys are never stored. The persisted key is an HMAC digest scoped to the ledger transaction type. A committed request replay returns the original transaction. Reusing the same key with different canonical data fails with `LEDGER_IDEMPOTENCY_MISMATCH`.

Corrections never update or delete ledger entries. A reversal creates a linked transaction with exact opposite entries and changes the original transaction state to `reversed`. The database blocks entry update/delete and ledger transaction delete operations with triggers.

## Authoritative balances and projections

`wallet_account_balances` rebuilds every account balance from transactions whose status is `posted` or `reversed`. `wallet_user_balances` rebuilds a user's available, held, lifetime-earned, and last-entry watermark values. Pending entries are excluded from both views.

`wallet_balance_snapshots` is updated in the same database transaction after posting. The internal reconciliation service can compare a snapshot with the authoritative view or rebuild it while user account rows are locked. A projection mismatch does not change the authoritative balance returned by the API.

## Authenticated APIs

### `GET /api/v1/wallet`

Returns the authenticated user's authoritative settlement-currency totals:

- `available_minor`
- `held_minor`
- `lifetime_earned_minor`
- currency display metadata
- snapshot consistency and rebuild time
- server time

### `GET /api/v1/wallet/history`

Returns newest-first posted/reversed transactions affecting the authenticated user's wallet. Query fields are strictly limited to:

- `limit`: integer from 1 through 100; default 20
- `cursor`: opaque, HMAC-signed keyset cursor

Each item exposes public transaction ID, type, status, description, reference, posted time, available/held/net minor-unit deltas, currency metadata, and optional public reversal link. Internal database IDs, raw idempotency keys, ledger metadata, and other users' entries are not exposed.

Both endpoints require the verified server-side Telegram session. There is intentionally no public balance mutation or ledger-posting endpoint. Later task, referral, free-reward, and withdrawal services must call the internal ledger service with server-controlled amounts and references.

## Deployment checklist

1. Back up and inspect the intended Oracle MySQL database.
2. Run `npm run migrate:dry-run` and confirm migration `015_harden_ledger_integrity.sql`.
3. Run `npm run migrate` only in the intended environment.
4. Run `npm run migrate:status` and `npm run verify:database-schema`.
5. Run `npm run check` before deploying the web process.
6. Monitor reconciliation mismatches and ledger failures without logging secrets or private metadata.

No live database, Telegram account, Redis deployment, or external balance was accessed while Phase 10 was built and tested.
