decisions
Admin "All Money Movements" tab
A frozen record of a decision at the time. Superseded by a new record rather than edited.
Written by the build · 2 September 2026
Frozen. A record of a decision at a point in time, not living documentation. Do not update it — supersede it with a new record instead. For how this works today, see
docs/explanation/anddocs/generated/.
Admin "All Money Movements" tab
Status: Implemented (2026-07-21, build-green + admin typecheck-green). Needs: run the backfill once, then live screenshot review. Owner: Farhan
A global, cross-customer, filterable feed of every Bundul money movement — in/out, the customer/entity attached, amount, what it's for, when, and status — spanning One Sub collections, the Bundul fee, Passport BOOK transfers, VC charges, ACH utility pulls, and true-up. Includes in-flight and failed movements, and Bundul's own platform accounts (revenue inflows), not just the customer side.
Data reality (why we materialize)
No single source of truth. CustomerCashLedger is the broadest (hourly mirror of each
customer's Passport ledger; covers one_sub_collection, one_time_collection, bundul_fee,
vc_charge, ach_utility_pull, refund, trueup_deposit, trueup_debit) but misses:
- Card funding / top-ups → only in
VirtualCardLedgerEntry. - Pending / failed / attempted charges → only in
PassportPaymentRecord. - Bundul's platform accounts (CHARGES
4784031+ true-up account) → not captured anywhere; the receiving side of every fee sweep + true-up collection (Bundul's revenue) is invisible.TransactionTimelineServicealready normalizes 4 sources per-user (reuse its mapping + the vc_charge/VC-charge dedup).
Locked decisions
- Materialized
MoneyMovementcollection (not query-time aggregation). - Include in-flight + failed (from
PassportPaymentRecord), with a status filter. - Bundul platform-account sync is in scope (Phase 2) — surfaces Bundul's own inflows.
- Direction (Bundul perspective):
- IN:
one_sub_collection,one_time_collection(recurring collections),bundul_fee,trueup_debit, merchant/inboundrefund. - OUT:
vc_charge,ach_utility_pull,trueup_deposit, outboundrefund. - TRANSFER (internal, excluded from net): card
funding/top_up(customer wallet → card). - Caveat:
one_sub_collectionincludes the fee thatbundul_feelater sweeps — so a naivesum(IN)−sum(OUT)double-counts the fee. The tab shows Total in / Total out per the rules above (useful for ops); a true P&L net is out of scope for v1 (flag in UI).
- IN:
Canonical MoneyMovement shape
{ eventKey (unique), source (cash_ledger|vc|passport_record|trueup|platform), occurredAt, direction (in|out|transfer), amount (+), status (normalized: pending|processing|scheduled| completed|failed|declined), rawStatus, method (ACH|BOOK|CARD), entityType (customer|merchant| utility|bundul), userId?, customerEmail?, customerName?, entityLabel?, category ("what for"), subscriptionId?, subscriptionName?, trueUpRecordId?, passportTransactionId?, billingPeriod?, sourceId, syncedAt }. eventKey = <source>:<sourceId>[:<hop>] for idempotent upserts.
Phases
- Model + ingestion —
MoneyMovementschema/module; an ingestion service that maps each source → MoneyMovement (upsert byeventKey, reuse timeline dedup); a backfill + an ingestion cron (runs afterledger-sync). Sources: CustomerCashLedger (backbone), VirtualCardLedgerEntry (funding/top-ups), PassportPaymentRecord (pending/failed), TrueUpRecord. - Bundul-side — extend
ledger-syncto also sync the platform accounts (CHARGES4784031- true-up) →
entityType: 'bundul'IN rows (fee revenue, true-up collections landing).
- true-up) →
- Endpoint —
GET /admin/ledger/movements: paginated + filterable (direction, entityType, source/category, customer, date range, status), sorted byoccurredAt, returns aggregate in / out / net for the filtered set.LEDGER_READ. - Admin tab —
MoneyMovementsPagein bundul-admin: DataTable (When · Direction · Entity · Amount · For what · Source · Status) + filters + summary cards (Total in / Total out / Net). New Finance sidebar item + route +src/apiclient, mirroringFinancialsPageconventions.
This is a record of a decision at the time. It is not edited — write a new record that supersedes it.