Merchant Financial Ledger
Technical Design Document

ProjectMercury Settlement Platform (UAT)
ModuleLedger Core (New Umbrella App)
Prepared bySettlement Module Team
DateJune 2026
ScopeInternal — Finance and Operations only
StatusDesign Plan — Ready for Discussion

Contents

  1. Background & Motivation
  2. How This Differs From the Event Logger
  3. Core Concept — Per Merchant Per Settlement Date
  4. Database Design
  5. How Ledger Entries Get Created
  6. The Balance Rule
  7. Outstanding Balances — Finance View
  8. Edge Cases
  9. UI Design
  10. Umbrella App Structure
  11. Implementation Steps
  12. Relationship With Event Logger

1. Background & Motivation

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.

Objectives

2. How This Differs From the Event Logger

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?"
Simple way to remember the difference: The Event Logger tells you the story of the settlement. The Merchant Ledger tells you the score — who owes what and whether it has been settled.

3. Core Concept — Per Merchant Per Settlement Date

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.

Example — Same Merchant, Two Settlement Dates

These two cycles are completely independent of each other:

Merchant: MID 419926360000000  |  Settlement Date: 18-Jun-2026
Entry Type Side Amount
Gross settlement — 342 txnsGross SettlementCredit+AED 1,420.00
MDR deduction (10%)MDR DeductionDebit−AED 142.00
VAT on MDR (5%)VAT DeductionDebit−AED 7.10
Adjustment — chargeback recoveryAdjustmentDebit−AED 25.00
Net payable to merchantRunning Balance—AED 1,245.90
Bank payout confirmed (19-Jun)Payout CreditedDebit−AED 1,245.90
Closing balanceClosed—AED 0.00 ✓

Merchant: MID 419926360000000  |  Settlement Date: 19-Jun-2026
Entry Type Side Amount
Gross settlement — 218 txnsGross SettlementCredit+AED 980.00
MDR deduction (10%)MDR DeductionDebit−AED 98.00
VAT on MDR (5%)VAT DeductionDebit−AED 4.90
Net payable to merchantRunning Balance—AED 877.10
Bank payout confirmed (20-Jun)Payout CreditedDebit−AED 877.10
Closing balanceClosed—AED 0.00 ✓
The Balance Rule: Sum of all credit entries minus sum of all debit entries = 0 when the cycle is fully closed. If the result is not zero, the cycle is still open — Mercury has an outstanding obligation to the merchant for that settlement date.

Why Per Date (Not Cumulative)?

4. Database Design

Table 1 — merchant_ledger_entries

One row per financial entry. This is the detailed ledger — every credit and debit.

CREATE TABLE merchant_ledger_entries ( id BIGSERIAL PRIMARY KEY, merchant_mid VARCHAR NOT NULL, settlement_date DATE NOT NULL, entry_type VARCHAR NOT NULL, side VARCHAR NOT NULL, -- credit | debit amount DECIMAL NOT NULL, -- always positive; side gives direction running_balance DECIMAL NOT NULL, -- balance after this entry reference_type VARCHAR NULL, -- settlement_mis | payout_item | adjustment | etc. reference_id BIGINT NULL, -- ID of the source record (soft link) description TEXT NOT NULL, -- human readable: "Gross settlement — 342 txns" posted_by VARCHAR NULL, -- "System" or user name posted_at TIMESTAMP NOT NULL DEFAULT NOW() );

Entry Types

entry_type Side Meaning Triggered By
gross_settlementCreditTotal gross transaction amount for the merchant on this dateMIS L2 approval
mdr_deductionDebitMDR fee charged to merchantMIS L2 approval
vat_deductionDebitVAT on MDR (5% UAE)MIS L2 approval
adjustment_debitDebitDeduction adjustment (chargeback recovery, AR recovery, etc.)Adjustment approved
adjustment_creditCreditAddition adjustment (goodwill credit, correction, etc.)Adjustment approved
risk_holdDebitAmount withheld due to risk flag — reduces net payableRisk hold applied
risk_releaseCreditRisk hold amount released back to merchantRisk hold released
payout_creditedDebitBank has confirmed the transfer to merchant — clears the balanceBank confirmation approved

Table 2 — merchant_ledger_summary

One 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.

CREATE TABLE merchant_ledger_summary ( id BIGSERIAL PRIMARY KEY, merchant_mid VARCHAR NOT NULL, settlement_date DATE NOT NULL, gross_amount DECIMAL NOT NULL DEFAULT 0, mdr_total DECIMAL NOT NULL DEFAULT 0, vat_total DECIMAL NOT NULL DEFAULT 0, adjustments_total DECIMAL NOT NULL DEFAULT 0, -- net of credits and debits risk_hold_total DECIMAL NOT NULL DEFAULT 0, net_payable DECIMAL NOT NULL DEFAULT 0, -- gross minus all deductions paid_amount DECIMAL NOT NULL DEFAULT 0, -- confirmed by bank balance DECIMAL NOT NULL DEFAULT 0, -- net_payable minus paid_amount status VARCHAR NOT NULL DEFAULT 'open', opened_at TIMESTAMP NULL, -- when MIS L2 was approved closed_at TIMESTAMP NULL, -- when balance reached zero UNIQUE (merchant_mid, settlement_date) );

Status Values for Summary

StatusMeaning
openMIS approved, entries posted, payout not yet confirmed by bank
on_holdA risk hold is applied — net payable is reduced, cycle cannot close
partialBank confirmed payment but amount does not match net payable (short payment)
closedBalance = 0 — merchant fully paid for this settlement date

Indexes

-- Primary query: outstanding balances per date CREATE INDEX idx_ledger_summary_balance ON merchant_ledger_summary (settlement_date, balance, status); -- Merchant history: all cycles for a merchant CREATE INDEX idx_ledger_summary_merchant ON merchant_ledger_summary (merchant_mid, settlement_date DESC); -- Entries per merchant per date CREATE INDEX idx_ledger_entries_merchant_date ON merchant_ledger_entries (merchant_mid, settlement_date, posted_at);

5. How Ledger Entries Get Created

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.

Trigger Map

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

Example — MIS L2 Approval Creates Three Entries Per Merchant

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.

-- For merchant MID 419926360000000 on 18-Jun-2026: INSERT INTO merchant_ledger_entries VALUES (merchant_mid: '419926360000000', settlement_date: '2026-06-18', entry_type: 'gross_settlement', side: 'credit', amount: 1420.00, running_balance: 1420.00, reference_type: 'settlement_mis_item', reference_id: 214, description: 'Gross settlement — 342 transactions'), (merchant_mid: '419926360000000', settlement_date: '2026-06-18', entry_type: 'mdr_deduction', side: 'debit', amount: 142.00, running_balance: 1278.00, reference_type: 'settlement_mis_item', reference_id: 214, description: 'MDR deduction — 10% on AED 1,420.00'), (merchant_mid: '419926360000000', settlement_date: '2026-06-18', entry_type: 'vat_deduction', side: 'debit', amount: 7.10, running_balance: 1270.90, reference_type: 'settlement_mis_item', reference_id: 214, description: 'VAT on MDR — 5% on AED 142.00');
Note: After MIS approval, if there are no adjustments, the running balance at this point is the net payable to the merchant. The summary table is updated in the same operation — merchant_ledger_summary.net_payable and balance are set, status is set to open.

Example — Bank Confirmation Closes the Cycle

-- Finance Ops approves bank confirmation batch -- Bank has credited AED 1,245.90 to merchant 419926360000000 INSERT INTO merchant_ledger_entries VALUES (merchant_mid: '419926360000000', settlement_date: '2026-06-18', entry_type: 'payout_credited', side: 'debit', amount: 1245.90, running_balance: 0.00, -- balance hits zero reference_type: 'payout_item', reference_id: 88, description: 'Bank payout confirmed — ref: ENBD-20260619-0441'); -- Summary updated: UPDATE merchant_ledger_summary SET paid_amount = 1245.90, balance = 0.00, status = 'closed', closed_at = NOW() WHERE merchant_mid = '419926360000000' AND settlement_date = '2026-06-18';

6. The Balance Rule

The fundamental rule of the Merchant Financial Ledger is:

For every merchant on every settlement date:

Gross Settlement
− MDR Deduction
− VAT Deduction
± Adjustments
− Risk Holds
+ Risk Releases
− Payout Credited
───────────────
= 0.00  (when fully closed)

If the result is not zero, the cycle is still open and Mercury has an outstanding financial obligation to that merchant for that settlement date.

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.

7. Outstanding Balances — Finance View

The most important query for Finance is: "Which merchants still have an outstanding balance and why?"

Outstanding Balances Dashboard

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.

Closed Cycles — Historical View

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

8. Edge Cases

Case 1 — Partial Bank Confirmation

The bank confirms payment for 45 out of 47 merchants in a batch. The 2 failed merchants are not credited.

The two failed merchants' cycles are fully visible in the outstanding dashboard. Nothing is hidden or merged into the batch total. Finance knows exactly who is unpaid and for how much.

Case 2 — Adjustment After MIS Approval

A 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 settlementCredit+AED 1,420.00AED 1,420.00
MDR deductionDebit−AED 142.00AED 1,278.00
VAT on MDRDebit−AED 7.10AED 1,270.90
Adjustment — chargeback recovery (posted later)Debit−AED 25.00AED 1,245.90
Bank payout confirmedDebit−AED 1,245.90AED 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.

Case 3 — Risk Hold Applied

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 settlementCredit+AED 800.00AED 800.00
MDR deductionDebit−AED 80.00AED 720.00
VAT on MDRDebit−AED 4.00AED 716.00
Risk hold appliedDebit−AED 340.00AED 376.00
Bank payout confirmed (AED 376.00 only)Debit−AED 376.00AED 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.

9. UI Design

Two views are needed — both internal, for Finance and Operations only.

View 1 — Merchant Ledger Detail

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.

View 2 — Outstanding Balances Dashboard

Shows all open settlement cycles across all merchants — the primary Finance monitoring view.

Menu Placement

Add a new section in the left navigation called Ledger with two items:

Menu ItemPathPermission
Outstanding Balances/admin/ledger/outstandingledger.balances.view
Merchant Ledger/admin/ledger/merchantledger.merchant.view

10. Umbrella App Structure

The Merchant Ledger is implemented as a new app — ledger_core — within the existing umbrella structure.

tmsuat_apps/ ├── apps/ │ ├── platform_core/ ← shared repo, auth, users │ ├── platform_web/ ← web layer (LiveView, controllers) │ ├── settlement_core/ ← settlement pipeline logic │ ├── tms_core/ │ ├── risk_core/ │ └── ledger_core/ ← NEW — merchant financial ledger │ ├── lib/ │ │ └── ledger_core/ │ │ ├── merchant_ledger_entry.ex (Ecto schema) │ │ ├── merchant_ledger_summary.ex (Ecto schema) │ │ ├── ledger_writer.ex (writes entries) │ │ ├── ledger_query.ex (reads + balance queries) │ │ └── application.ex │ └── mix.exs

Why a Separate App and Not Inside settlement_core?

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.
Dependency rule: 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.

11. Implementation Steps

# 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
All changes to existing apps are additive only. Steps 5, 6, 7, and 8 add one new function call each to existing context functions — no existing logic is modified. The settlement and risk pipelines continue to work exactly as they do today.

12. Relationship With the Event Logger

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.

Together, these two systems give Mercury complete settlement intelligence:

The Event Logger answers: What happened in the settlement pipeline today and did it succeed?

The Merchant Ledger answers: What does Mercury currently owe each merchant and has it been paid?

One tells the story. The other keeps the score.