Bundul
Internal
Browse docs
Waiting for review

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/ and docs/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. TransactionTimelineService already normalizes 4 sources per-user (reuse its mapping + the vc_charge/VC-charge dedup).

Locked decisions

  1. Materialized MoneyMovement collection (not query-time aggregation).
  2. Include in-flight + failed (from PassportPaymentRecord), with a status filter.
  3. Bundul platform-account sync is in scope (Phase 2) — surfaces Bundul's own inflows.
  4. Direction (Bundul perspective):
    • IN: one_sub_collection, one_time_collection (recurring collections), bundul_fee, trueup_debit, merchant/inbound refund.
    • OUT: vc_charge, ach_utility_pull, trueup_deposit, outbound refund.
    • TRANSFER (internal, excluded from net): card funding/top_up (customer wallet → card).
    • Caveat: one_sub_collection includes the fee that bundul_fee later sweeps — so a naive sum(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).

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

  1. Model + ingestionMoneyMovement schema/module; an ingestion service that maps each source → MoneyMovement (upsert by eventKey, reuse timeline dedup); a backfill + an ingestion cron (runs after ledger-sync). Sources: CustomerCashLedger (backbone), VirtualCardLedgerEntry (funding/top-ups), PassportPaymentRecord (pending/failed), TrueUpRecord.
  2. Bundul-side — extend ledger-sync to also sync the platform accounts (CHARGES 4784031
    • true-up) → entityType: 'bundul' IN rows (fee revenue, true-up collections landing).
  3. EndpointGET /admin/ledger/movements: paginated + filterable (direction, entityType, source/category, customer, date range, status), sorted by occurredAt, returns aggregate in / out / net for the filtered set. LEDGER_READ.
  4. Admin tabMoneyMovementsPage in 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/api client, mirroring FinancialsPage conventions.

This is a record of a decision at the time. It is not edited — write a new record that supersedes it.