This document describes the custody accounting behavior that exists in the application. Code, migrations, and tests are authoritative when this summary differs from the implementation.
The repository contains an append-only PostgreSQL journal with balanced postings and rebuildable
account projections. The journal is not a general ledger for every payment flow: the only connected
business path is the optional shadow entry for a new internal_balance_only merchant credit after
the one-way Phase 2A cutover marker has been activated.
Custody invoice routing and payout request/execution flows are not implemented. Their gates must remain disabled.
custody_accountsidentifies asset/network accounts and their normal side.custody_journal_entriesandcustody_journal_postingsform immutable balanced events.custody_account_balancesis a rebuildable projection.custody_journal_source_linksbinds a shadow journal entry to its exactmerchant_settlement_entriessource.custody_phase2a_cutoverscontains the immutable one-way cutover marker.
PostgreSQL constraints and deferred triggers enforce balanced positive postings, matching asset/network/scale, source coverage, exact shadow posting shape, restrictive deletion, and append-only history. Corrections are new journal entries; posted financial history is not edited.
All custody and payout gates default to false.
Before a cutover marker exists, positive internal credits follow the existing legacy balance and settlement-ledger path when the shadow gate is off. Enabling the shadow gate before the marker fails closed. Once the marker exists, positive internal credits require exactly this configuration:
CUSTODY_ACCOUNTING_ENABLED=true
CUSTODY_JOURNAL_WRITES_ENABLED=true
CUSTODY_PHASE2A_SHADOW_INTERNAL_CREDITS_ENABLED=true
CUSTODY_INVOICE_ROUTING_ENABLED=false
PAYOUT_REQUESTS_ENABLED=false
PAYOUT_AUTOMATIC_REQUESTS_ENABLED=false
PAYOUT_EXECUTION_ENABLED=falseAny later mismatch fails closed. A successful post-cutover internal credit writes the settlement source, two-posting shadow journal entry, source link, projections, merchant balance, invoice state, and webhook outbox in one database transaction.
./vendor/bin/sail artisan custody:verify-projections
./vendor/bin/sail artisan custody:verify-phase2a
./vendor/bin/sail artisan custody:verify-phase2a --json
./vendor/bin/sail artisan custody:reconcile-legacy-balancesProjection rebuild is a dry run unless --write is explicitly supplied:
./vendor/bin/sail artisan custody:rebuild-projectionsThe backfill command is a pre-cutover compatibility tool. Preview it with
./vendor/bin/sail artisan settlements:backfill-ledger --dry-run; write mode is prohibited after
cutover.
./vendor/bin/sail artisan custody:activate-phase2a --reference=<stable-reference> is one-way and
succeeds only for the exact gate configuration and a completely empty, clean baseline. It does not
reset a database.
Database resets, gate changes, and activation require explicit operator intent and verified
local/testing database identity. Never run them against production, staging, an external database,
or data with financial obligations.
The implementation lives in app/Services/Custody, app/Services/Settlement, the custody
migrations, and tests/Feature/Custody.