The Mercury Settlement platform processes payouts to hundreds of merchants across daily settlement cycles. The current system tracks the process of settlement well — recon runs, MIS is generated, payouts are transmitted. However it does not maintain a clear record of the financial position of each merchant at any point in time.
Concretely: if Finance needs to answer the question "How much does Mercury currently owe merchant X, and for which settlement dates?" — there is no single place to look. The answer is spread across core_transactions, settlement_mis_items, payout_items, and merchant_adjustments.
The Merchant Financial Ledger solves this by maintaining a running financial record per merchant per settlement date — every credit and debit that affects what Mercury owes a merchant, structured so that each settlement cycle must add up to zero when the merchant is fully paid.
These are two separate systems serving two different purposes. Both are needed and they complement each other.
| Dimension | Settlement Event Logger | Merchant Financial Ledger |
|---|---|---|
| What it records | What happened and when in the pipeline | The financial impact of what happened |
| A typical row | "MIS approved at 09:14 by Finance L2" | "+AED 1,420.00 Gross settlement — 342 txns" |
| Unit | Pipeline event (process milestone) | Money entry (financial transaction) |
| Per | Per settlement date (pipeline view) | Per merchant per settlement date (financial view) |
| Must balance? | No — events are just a timeline | Yes — every cycle must sum to zero when closed |
| Primary audience | Operations, Engineering | Finance, Accounting, Audit |
| Question it answers | "Did the SFTP fetch succeed today?" | "Does Mercury still owe merchant X any money?" |
Each merchant has an independent ledger cycle for each settlement date. The cycle opens when the MIS is approved and closes when the bank confirms the payout and the balance reaches zero.
These two cycles are completely independent of each other:
| Merchant: MID 419926360000000 | Settlement Date: 18-Jun-2026 | |||
|---|---|---|---|
| Entry | Type | Side | Amount |
| Gross settlement — 342 txns | Gross Settlement | Credit | +AED 1,420.00 |
| MDR deduction (10%) | MDR Deduction | Debit | −AED 142.00 |
| VAT on MDR (5%) | VAT Deduction | Debit | −AED 7.10 |
| Adjustment — chargeback recovery | Adjustment | Debit | −AED 25.00 |
| Net payable to merchant | Running Balance | — | AED 1,245.90 |
| Bank payout confirmed (19-Jun) | Payout Credited | Debit | −AED 1,245.90 |
| Closing balance | Closed | — | AED 0.00 ✓ |
| Merchant: MID 419926360000000 | Settlement Date: 19-Jun-2026 | |||
|---|---|---|---|
| Entry | Type | Side | Amount |
| Gross settlement — 218 txns | Gross Settlement | Credit | +AED 980.00 |
| MDR deduction (10%) | MDR Deduction | Debit | −AED 98.00 |
| VAT on MDR (5%) | VAT Deduction | Debit | −AED 4.90 |
| Net payable to merchant | Running Balance | — | AED 877.10 |
| Bank payout confirmed (20-Jun) | Payout Credited | Debit | −AED 877.10 |
| Closing balance | Closed | — | AED 0.00 ✓ |
WHERE balance != 0 — no complex date arithmetic needed.merchant_ledger_entriesOne row per financial entry. This is the detailed ledger — every credit and debit.
| entry_type | Side | Meaning | Triggered By |
|---|---|---|---|
gross_settlement | Credit | Total gross transaction amount for the merchant on this date | MIS L2 approval |
mdr_deduction | Debit | MDR fee charged to merchant | MIS L2 approval |
vat_deduction | Debit | VAT on MDR (5% UAE) | MIS L2 approval |
adjustment_debit | Debit | Deduction adjustment (chargeback recovery, AR recovery, etc.) | Adjustment approved |
adjustment_credit | Credit | Addition adjustment (goodwill credit, correction, etc.) | Adjustment approved |
risk_hold | Debit | Amount withheld due to risk flag — reduces net payable | Risk hold applied |
risk_release | Credit | Risk hold amount released back to merchant | Risk hold released |
payout_credited | Debit | Bank has confirmed the transfer to merchant — clears the balance | Bank confirmation approved |
merchant_ledger_summaryOne row per merchant per settlement date. This is the summary view — Finance queries this table for the outstanding balances dashboard. It is updated each time an entry is added.
| Status | Meaning |
|---|---|
open | MIS approved, entries posted, payout not yet confirmed by bank |
on_hold | A risk hold is applied — net payable is reduced, cycle cannot close |
partial | Bank confirmed payment but amount does not match net payable (short payment) |
closed | Balance = 0 — merchant fully paid for this settlement date |
Entries are written to the ledger at specific points in the settlement pipeline — triggered by the same events that the Event Logger records, but writing financial data instead of audit data.
| Settlement Event | Ledger Entries Created | Per |
|---|---|---|
| MIS L2 Approved |
gross_settlement (credit)mdr_deduction (debit)vat_deduction (debit)
|
One set per merchant in the MIS |
| Adjustment Approved (L2) | adjustment_debit or adjustment_credit |
One entry per adjustment |
| Risk Hold Applied | risk_hold (debit) |
One entry per merchant affected |
| Risk Hold Released | risk_release (credit) |
One entry per merchant |
| Bank Confirmation Approved | payout_credited (debit) — closes the cycle |
One entry per merchant in the batch |
When Finance L2 approves the MIS for 18-Jun-2026 covering 47 merchants, the ledger receives 3 entries × 47 merchants = 141 rows inserted at once.
merchant_ledger_summary.net_payable and balance are set, status is set to open.
The fundamental rule of the Merchant Financial Ledger is:
Gross Settlement− MDR Deduction− VAT Deduction± Adjustments− Risk Holds+ Risk Releases− Payout Credited───────────────= 0.00 (when fully closed)This rule is enforced at the application level. The merchant_ledger_summary.balance field always reflects the current outstanding amount. When it reaches zero, the cycle is marked closed.
The most important query for Finance is: "Which merchants still have an outstanding balance and why?"
| Merchant MID | Settlement Date | Net Payable | Paid Amount | Outstanding Balance | Status | Days Open |
|---|---|---|---|---|---|---|
| 419926360000000 | 19-Jun-2026 | AED 877.10 | AED 0.00 | AED 877.10 | Open | 1 |
| Mercury_CB6F6A5 | 19-Jun-2026 | AED 620.00 | AED 0.00 | AED 620.00 | Open | 1 |
| Mercury0BCFEFDC | 18-Jun-2026 | AED 340.00 | AED 0.00 | AED 340.00 | On Hold | 2 |
The third merchant's 18-Jun cycle is still open two days later because a risk hold was applied. The first two are open because their bank confirmation has not yet been processed. Finance can see all three at a glance and take action accordingly.
| Merchant MID | Settlement Date | Net Payable | Paid Amount | Status | Opened | Closed |
|---|---|---|---|---|---|---|
| 419926360000000 | 18-Jun-2026 | AED 1,245.90 | AED 1,245.90 | Closed ✓ | 19-Jun 11:32 | 19-Jun 14:18 |
| Mercury_CB6F6A5 | 18-Jun-2026 | AED 890.00 | AED 890.00 | Closed ✓ | 19-Jun 11:32 | 19-Jun 14:18 |
The bank confirms payment for 45 out of 47 merchants in a batch. The 2 failed merchants are not credited.
payout_credited entry posted, cycle closes to zero, status → closedopenpayout_credited entry is posted and the cycle closesA chargeback recovery adjustment of AED 25.00 is raised against a merchant after the MIS is already approved but before the payout is transmitted.
| Entry | Side | Amount | Running Balance |
|---|---|---|---|
| Gross settlement | Credit | +AED 1,420.00 | AED 1,420.00 |
| MDR deduction | Debit | −AED 142.00 | AED 1,278.00 |
| VAT on MDR | Debit | −AED 7.10 | AED 1,270.90 |
| Adjustment — chargeback recovery (posted later) | Debit | −AED 25.00 | AED 1,245.90 |
| Bank payout confirmed | Debit | −AED 1,245.90 | AED 0.00 |
| Closing balance | — | — | AED 0.00 ✓ |
The adjustment entry is simply added to the existing cycle. The summary table's net_payable and balance are updated. The payout transmission picks up the revised net payable automatically.
A risk hold of AED 340.00 is placed on a merchant. The hold reduces the net payable, and the cycle stays open until the hold is either released or the held amount is confirmed as forfeited.
| Entry | Side | Amount | Running Balance |
|---|---|---|---|
| Gross settlement | Credit | +AED 800.00 | AED 800.00 |
| MDR deduction | Debit | −AED 80.00 | AED 720.00 |
| VAT on MDR | Debit | −AED 4.00 | AED 716.00 |
| Risk hold applied | Debit | −AED 340.00 | AED 376.00 |
| Bank payout confirmed (AED 376.00 only) | Debit | −AED 376.00 | AED 0.00 |
| Closing balance (hold still active separately) | — | — | AED 0.00 ✓ |
The risk hold is managed via the existing Risk Holds module. Once the held amount is released, a risk_release credit entry is posted and a separate payout is triggered for the released amount, opening and closing a new ledger cycle for that release.
Two views are needed — both internal, for Finance and Operations only.
Shows the full entry-by-entry ledger for a specific merchant on a specific settlement date. Accessed by clicking a merchant from the outstanding balances list or by searching.
Shows all open settlement cycles across all merchants — the primary Finance monitoring view.
Add a new section in the left navigation called Ledger with two items:
| Menu Item | Path | Permission |
|---|---|---|
| Outstanding Balances | /admin/ledger/outstanding | ledger.balances.view |
| Merchant Ledger | /admin/ledger/merchant | ledger.merchant.view |
The Merchant Ledger is implemented as a new app — ledger_core — within the existing umbrella structure.
| Reason | Explanation |
|---|---|
| Different domain | Settlement is about process (recon, MIS, payout flow). Ledger is about financial position (what is owed). These are separate concerns. |
| Future extensibility | In future, chargebacks, refunds, or risk recoveries from other modules may also affect merchant balances. A standalone ledger_core can accept entries from any module — not just settlement. |
| Independent deployability | The ledger can be built, tested, and deployed without touching the settlement pipeline. |
| Clean dependency direction | settlement_core calls into ledger_core to post entries. ledger_core does not depend on settlement_core at all. |
settlement_core → ledger_core (settlement posts entries to ledger). ledger_core never imports or calls settlement_core. This keeps the dependency graph clean and prevents circular references.
| # | Step | Work | App | Impact on Existing Code |
|---|---|---|---|---|
| 1 | Create umbrella app | Scaffold ledger_core app inside the umbrella |
ledger_core | None — new app |
| 2 | Migrations | Create merchant_ledger_entries and merchant_ledger_summary tables with indexes |
ledger_core | None — new tables |
| 3 | Ecto schemas | Create MerchantLedgerEntry and MerchantLedgerSummary schemas with changesets |
ledger_core | None — new files |
| 4 | LedgerWriter module | Create LedgerCore.LedgerWriter.post_mis_entries/1 and post_payout_credited/1 functions |
ledger_core | None — new file |
| 5 | Wire MIS L2 approval | Call LedgerWriter.post_mis_entries(mis) inside Context.l2_approve_settlement_mis/3 after successful approval |
settlement_core | Additive only — one new call after existing logic |
| 6 | Wire bank confirmation | Call LedgerWriter.post_payout_credited(payout_item) inside BankConfirmationService.process_confirmed_payout/1 |
settlement_core | Additive only |
| 7 | Wire adjustments | Call LedgerWriter.post_adjustment(adjustment) inside Context.l2_approve_adjustment/3 |
settlement_core | Additive only |
| 8 | Wire risk holds | Call LedgerWriter.post_risk_hold/post_risk_release from risk hold module |
risk_core / settlement_core | Additive only |
| 9 | LedgerQuery module | Create query functions: outstanding balances, merchant ledger detail, balance for a merchant+date | ledger_core | None — new file |
| 10 | LiveView pages | Create OutstandingBalancesLive and MerchantLedgerLive in platform_web |
platform_web | None — new files |
| 11 | Menu + permissions | Add Ledger section to menu provider, add ledger.balances.view and ledger.merchant.view permissions |
ledger_core / platform_core | Additive only |
The Settlement Event Logger and the Merchant Financial Ledger are built at the same time but serve completely different purposes. They are written from the same trigger points but record different information.
| Trigger Point | Event Logger records | Ledger records |
|---|---|---|
| MIS L2 Approved | "MIS for 18-Jun-2026 approved by Finance L2 at 11:32" | Gross settlement, MDR, VAT entries per merchant (141 rows for 47 merchants) |
| Bank Confirmation Approved | "Batch #11: 47 merchants confirmed, AED 58,420" | Payout credited entry per merchant — closes 47 cycles to zero |
| Adjustment Approved | "Adjustment of AED 25 applied to MID 419926..." | Adjustment debit entry, running balance updated |
| Risk Hold Applied | "Risk hold of AED 340 applied to MID Mercury0BC..." | Risk hold debit entry, summary status → on_hold |
From the same trigger, two things are written: the Event Logger gets one process audit row, and the Ledger gets one or more financial entries. They are independent writes — a failure in ledger writing does not affect event logging, and vice versa.