# Dispute Management — what was built and how to use it

Written for the people who run the platform (not developers). It says what the
portal now does, where to click, and what happens at each step. Technical
detail is in the last section and in the linked files.

---

## 1. The big picture

There are **two different kinds of dispute**, handled in two different places in the portal.

| | **Merchant disputes** (new) | **Chargebacks** (existing, improved) |
|---|---|---|
| Who starts it | The **merchant**, in the MMS merchant portal — or our staff **on the merchant's behalf** | The **card scheme** (Visa/Mastercard) through the issuer, because a *cardholder* complained |
| What it is about | "This transaction / settlement is wrong" — short-paid, charged twice, refund not credited… | "The cardholder says they didn't make / didn't get this" |
| Who investigates | Our operations team | Our operations team, then the card scheme |
| Where | Disputes → **Merchant Disputes** | Disputes → **Dispute Dashboard** |

Cardholders never log in anywhere. Their complaints reach us as chargebacks.

**How MMS and this portal fit together.** The merchant uses MMS only. MMS asks this
portal for everything (it never keeps its own copy of disputes). This portal is the
single source of truth: it stores the dispute, the files, the history and the decision.
When something changes, the portal sends MMS a short notification so MMS can refresh.

---

## 2. Where to find everything (navigation)

Sign in, then choose **Disputes** in the top bar. The left menu has these groups:

| Menu group | Page | Address | What it is for |
|---|---|---|---|
| **Merchant Disputes** | **Merchant Disputes** | `/admin/merchant-disputes` | The work queue: every merchant dispute, with tabs and filters. **Raise on behalf of a merchant** button is here. |
| | **Merchant Dispute Reports** | `/admin/merchant-disputes/reports` | How many are open, overdue, how old, how long they take, who owns them, MMS notification health. |
| | **Old MMS Disputes** | `/admin/merchant-disputes/legacy` | Read-only list of disputes raised through MMS's **old** screens (`DSP…`). They have no verified transaction so they can't be worked here. A yellow banner on the queue tells you when there are any. |
| | *(opens from the queue)* Dispute page | `/admin/merchant-disputes/MD-…` | One dispute: investigate, ask for evidence, message the merchant, escalate, decide. |
| **Case Management** | **Dispute Dashboard** | `/admin/chargebacks` | Chargeback cases (scheme chargebacks). Click a row → the case drawer. |
| | Chargeback Cases, Dispute Cases, Case Search, Retrieval Requests | `/admin/chargebacks/…` | Lists and searches (unchanged). |
| **Workflow** | **Approval Queue** | `/admin/chargebacks/approval-queue` | Requests waiting for a Supervisor/Head to approve (chargebacks). |
| | **Compliance Queue** | `/admin/chargebacks/compliance` | Cases waiting for a compliance decision (chargebacks). Only compliance reviewers see it. |
| | **Scheme Simulator** | `/admin/chargebacks/simulator` | **Demo/testing tool.** Pretend a card scheme sends chargebacks, watch the merchant being debited, then have the scheme decide. Needs `dispute.config.manage`; switched off in production. See `SIMULATION_DEMO.md`. |
| **Rules & SLA** | **Dispute Parameters** | `/admin/chargebacks/parameters` | All the business numbers (deadlines, limits…). See section 9. |
| | Automation Rules | `/admin/chargebacks/automation-rules` | Automatic escalation / assignment rules for chargebacks. |

*You only see the pages your role allows.*

---

## 3. Who can do what

| Role | Can do |
|---|---|
| **Viewer** (`dispute.case.view`) | Look at everything. Change nothing. |
| **Analyst** (`dispute.case.work`) | Work disputes: take ownership, review, ask for evidence, message, note, escalate to a Supervisor, **decline** any dispute, **accept** up to the Analyst limit (default 500 AED). |
| **Supervisor** (`dispute.case.supervise`) | Everything an Analyst does, plus: assign/reassign anyone's cases, escalate to the Head, clear an escalation, **accept** up to the Supervisor limit (default 10,000 AED), approve chargeback requests up to their limit. |
| **Head** (`dispute.case.approve_high`) | Unlimited. Final say on escalations and the largest amounts. |
| **Compliance** (`dispute.case.compliance`) | Decide compliance reviews on chargebacks. |
| **Config** (`dispute.config.manage`) | Change approval limits and the business numbers. |

Nobody can approve their own request, and a compliance reviewer cannot review a request they raised.

---

## 4. A merchant dispute, start to finish

### Step 1 — The merchant raises it (in MMS)
1. The merchant enters the **RRN** (and date/terminal if asked) of the transaction.
2. MMS asks the portal "can this be disputed?". The portal answers yes — or says why not:
   - **a refund exists** (any refund blocks a dispute),
   - the transaction was **reversed**,
   - a **card-scheme chargeback** is already open on it,
   - it is **older than 90 days** (a setting),
   - an **open dispute already exists** on it.
3. If yes, the merchant picks a **reason**, the **amount** (can be part of the sale), writes what happened (at least 20 characters), gives a contact email, and submits.
4. The portal creates the dispute with a reference like **`MD-20261023-K7Q2XA`** and shows MMS the status **Submitted**. A resolution deadline is set (default 7 working days).

*Retrying a slow submit never creates two disputes.*

> **If an MMS admin raises it:** MMS's admin screens must call the same API (with the header `X-Actor-Role: mms_admin` and the **merchant's** MID). It then appears in the queue exactly like a merchant's, marked as raised by an MMS admin. Disputes created through MMS's *old* admin screens (references starting `DSP`) are not in the queue — see **Old MMS Disputes** below.

### Step 2 — It appears in the work queue
Go to **Disputes → Merchant Disputes**. The new dispute is in **All open** and **Unassigned**.
Tabs across the top: **All open · My queue · Unassigned · Waiting for merchant · Overdue · Escalated**
(Supervisors and Heads also see **My escalations**). Use the filters to search by reference, RRN or MID.

### Step 3 — Take it and start reviewing
Click the reference to open the dispute page.
1. Under **Ownership**, pick yourself and press **Assign / reassign** (or **Update assignment**). Moving *someone else's* dispute needs a reason.
2. Press **Start review**. The merchant's status in MMS becomes **In review**.

You can see the transaction, the merchant's account, and everything so far on the left.

### Step 4 — Ask the merchant for evidence (if needed)
1. In **Ask the merchant for evidence**, type exactly what you need (at least 10 characters).
2. Optionally set the number of working days (default 5). Press **Request evidence**.
3. The merchant is told in MMS and the status becomes **Waiting for merchant**.
4. The merchant uploads files (PDF, JPG or PNG, up to 10 MB each). The files appear under **Evidence** with a **Download** link.
5. When every request is answered, the status returns to **In review** by itself.

**If the merchant does not answer:** one day before the deadline the merchant gets a reminder. After the deadline the request is marked *expired* and — if it is also past the resolution deadline — the dispute is escalated. A missed deadline **never decides** the dispute by itself. If the merchant stays silent for a further 7 days, the dispute is closed as **Closed — no response**. (All these numbers are settings.)

### Step 5 — Talk to the merchant, and keep notes
- **Message the merchant** — the merchant *will* see this in MMS.
- **Internal note** — only our staff ever see it.

The **Timeline** at the bottom labels every entry **Visible to merchant** or **Internal only**, so there is no guessing.

### Step 6 — Escalate if you are stuck
Type a **reason**, then press **To Supervisor** (anyone who can work disputes) or **To Head** (Supervisors and Heads). Supervisors/Heads can **Clear** an escalation, with a reason. Escalation is also automatic: past the resolution deadline → Supervisor; 24 hours later → Head. Each escalation raises an in-app alert.

### Step 7 — Record the decision
In **Record the outcome**:
- choose **In the merchant's favour** or **Declined**,
- write the **reason the merchant will read** (at least 10 characters),
- optionally add an internal note,
- press **Record outcome**.

The merchant sees the result and your reason in MMS. **It is final — there is no reopen.** If the merchant still disagrees, they raise a new dispute.

> **Important:** "In the merchant's favour" records a *decision*. It does **not** refund or adjust anything. Any refund or settlement adjustment is a separate, authorised action.

Accepting is limited by role. If the amount is above your limit you will see *"This is above your limit. Escalate it to the supervisor to decide."* — escalate, and the Supervisor (or Head) records it. Declining has no limit.

### Step 8 — Afterwards
Everything stays: who did what and when, every message, every file. **Merchant Dispute Reports** shows the totals.

---

## 5. Raising a dispute on a merchant's behalf

When a merchant phones or emails instead of using MMS:
1. **Disputes → Merchant Disputes → Raise on behalf of a merchant**.
2. **Step 1:** enter the merchant's MID and the RRN (add date/terminal if it says more than one matches). Press **Check transaction**. You see the transaction and whether it can be disputed, with the reason if not.
3. **Step 2:** choose the reason, amount, contact email, and describe what happened. Press **Raise dispute**.
4. You land on the new dispute's page. It is marked *raised via the portal on the merchant's behalf*, and the merchant sees it in MMS like any other.

The same rules apply as in MMS (no refunded transaction, no duplicate, within the window).

The older **Log New Dispute** button on the chargeback dashboard is for *scheme* disputes; it now also checks that the transaction exists, the amount isn't more than the sale, and no open case already exists.

---

## 6. Chargebacks — what changed

Open **Disputes → Dispute Dashboard** and click a case to open its drawer.

- **Moves are controlled.** A case can only go forward in the allowed order (received → representment → pre-arbitration → arbitration; any open case can be resolved or written off). A closed case can't be changed again.
- **Approvals.** If you try to write off an amount above your limit, or file pre-arbitration/arbitration without the right, the system **sends a request** instead. The **Approval Queue** (Workflow menu) shows it. A Supervisor/Head approves or rejects (rejecting needs a reason). Approving runs the action. You cannot approve your own request.
- **Compliance.** By default every case needs compliance clearance before a financial outcome or arbitration. The first time you try, a review is requested automatically. A compliance reviewer decides in the **Compliance Queue**: *Cleared*, or *Raise a concern* (needs written findings and escalates to the Head). This is a setting (every case / flagged only / off).
- **Escalation** in the drawer has a reason, levels (Supervisor / Head) and a **My Escalations** tab.
- **Evidence** download now checks the file belongs to the case in the address.
- **Money rule — debit on arrival, credit back on a win** (a setting, **off by default**). When on, a chargeback that arrives is debited from the merchant straight away (an approved settlement adjustment). When the case is decided, the difference is trued up: a full win credits everything back, a partial loss credits the difference, a larger loss debits the difference. Each step happens once, is shown in the case drawer under **Merchant money**, and is on the timeline. It applies only to chargebacks that arrive after it is switched on (Dispute Parameters → *Chargeback money*). Agree it with Finance first.
- **Scheme Simulator** (demo/testing): see `SIMULATION_DEMO.md` and the sample files in `samples/`.

---

## 7. What the merchant sees (MMS)

| Portal status | Shown as | Merchant can |
|---|---|---|
| Submitted | Submitted | withdraw, message |
| In review | In review | withdraw, message |
| Waiting for merchant | **Action required** (with what we need and the deadline) | upload, withdraw, message |
| Resolved — in merchant's favour | Resolved | read the reason |
| Resolved — declined | Declined | read the reason |
| Withdrawn / Closed — no response | Withdrawn / Closed | — |

The merchant **never** sees internal notes, who it is assigned to, escalations, or staff names (staff appear as "Operations").

---

## 8. Reports

**Disputes → Merchant Dispute Reports:** total, open, overdue, waiting for merchant, escalated, average resolution time (hours); by status; by category; age of open disputes (0–2, 3–5, 6–10, over 10 days); open disputes per owner; and **Notifications to MMS** (pending / sent / dead — "dead" means MMS never accepted that notification and needs attention).

---

## 9. Settings (all in one place)

**Disputes → Rules & SLA → Dispute Parameters.** Changing these needs `dispute.config.manage`; others see them read-only. The numbers are **placeholders until Finance/Risk confirm them.**

| Setting | Default | Meaning |
|---|---|---|
| Dispute window | 90 days | How long after a transaction a merchant may dispute it |
| Resolution target | 7 working days | When a merchant dispute becomes overdue |
| Evidence deadline | 5 working days | How long a merchant has to answer a request |
| Close if no response | 7 days | Quiet days after an expired request before closing |
| Hours at Supervisor before Head | 24 | Escalation timing (chargebacks and merchant disputes) |
| Compliance review | every case | Chargebacks: every case / flagged only / off |
| **Approval limits** (table below it) | Analyst 500 AED · Supervisor 10,000 AED | The most each role can finalise alone |

Reasons a merchant can pick are in the table `merchant_dispute_categories` (ask a developer to change; no release needed).

---

## 10. Connecting MMS (one-time setup per environment)

1. **Run the migrations and seeds** (a developer does this):
   ```
   mix ecto.migrate
   mix run priv/repo/seeds/merchant_dispute_categories_seed.exs
   MMS_WEBHOOK_URL=https://<mms-host>/api/webhooks/disputes \
     mix run priv/repo/seeds/merchant_dispute_consumer_seed.exs
   ```
2. The last command prints an **API key** and a **webhook secret — once only**. Give them to the MMS team (`DISPUTE_PORTAL_API_KEY`, `DISPUTE_PORTAL_WEBHOOK_SECRET`). Use a different pair per environment.
3. Give MMS the base address `https://<portal-host>/api/v2/merchant` and the API description: [`merchant-dispute-api.openapi.yaml`](merchant-dispute-api.openapi.yaml).
4. MMS must send the header names MMS agreed: webhooks are signed with **`X-Signature`** (`sha256=…`) and **`X-Timestamp`** (MMS's first build used `X-Dispute-Signature` / `X-Dispute-Timestamp` — they need to change to match).
5. Raise PHP/web-server upload limits on the MMS side to at least **12 MB**.
6. Check it worked: in **Merchant Dispute Reports → Notifications to MMS**, events should go from *pending* to *sent*.

**Safety notes:** the API only works over **HTTPS** in production; keys are stored hashed; a key can be switched off without a release; there is a limit of 60 calls a minute per key.

---

## 11. What is **not** done (be aware)

- **No virus scanning.** Files are checked (type, size, contents match the type) but not scanned for malware — no antivirus is installed. The dispute page labels files "not virus-scanned". To add one, install an antivirus (e.g. ClamAV) and set `config :dispute_core, :antivirus_command, [...]`.
- **No money movement from merchant disputes.** Deciding a *merchant* dispute in the merchant's favour does not refund or adjust anything. (Scheme *chargebacks* are different: with the "debit on arrival" setting on, they post settlement adjustments — see section 6.)
- **Scheme intake is simulated.** Real scheme file formats and channels aren't connected yet (Mastercard's API is wired to Mastercard's test system only). Real formats need each scheme's real sample files.
- **Older settlement recovery step looks broken.** It writes adjustment rows with column names the table doesn't have (not run here). The new money rule bypasses it; it should be reviewed.
- **No reopen.** Final decisions are final.
- **No emails/WhatsApp from the portal.** MMS tells the merchant (it already has mail and WhatsApp). The portal only raises in-app alerts for *our* staff (escalations, approvals). The 80% "SLA warning" is not built.
- **Chargeback events are not sent to MMS** yet (merchant disputes only).
- **Webhook secrets** are stored readable in the database (they must be, to sign). Protect database access.
- **MMS's own admin dispute screens** still write through the old API, so what an admin creates there lands in **Old MMS Disputes** (read-only) instead of the work queue. MMS must switch those screens to the new API (role `mms_admin`) — then admin-raised disputes appear in the queue — or make them read-only.
- **MMS has not been tried against the real portal** until the API is deployed — expect a round of integration testing.
- The numbers in section 9 are placeholders.

---

## 12. Quick troubleshooting

| You see | Likely reason |
|---|---|
| "No matching transaction was found" | Wrong MID/RRN, or the transaction isn't in the portal's data. |
| "More than one transaction matches" | Add the date and/or terminal ID. |
| "An open dispute already exists" | Close/resolve the existing one first. |
| "This is above your limit" | Escalate to a Supervisor/Head to record the decision. |
| "Compliance clearance is required" | A review was requested; wait for the Compliance Queue decision. |
| MMS shows nothing new | Check *Notifications to MMS*: pending = not yet sent; dead = MMS rejecting. |
| A page redirects you away | Your role lacks that permission. |

---

## 13. Status checklist (as of 2026-10-22)

**Done and verified on our side**

| Item | Where / proof |
|---|---|
| Chargeback workflow: allowed status moves, approvals, compliance, escalation levels | Section 6; `apps/dispute_core/docs/dispute_workflow.md` |
| Merchant dispute module (raise, review, evidence loop, messages, escalate, decide, sweep, reports) | Sections 4–8 |
| Merchant dispute API v2 (11 endpoints) — matches the API description exactly | `merchant-dispute-api.openapi.yaml`; routes checked against it |
| MMS API key created on dev (hashed); categories, approval limits, alert rule, roles seeded | Dev database |
| `mms_admin` actor role; read-only **Old MMS Disputes** page and banner | Sections 2 and 4 |
| Money rule: debit on arrival / credit back on a win (setting, **off**) | Section 6; `SIMULATION_DEMO.md` |
| Scheme Simulator and five sample scheme files | Section 2; `samples/` |
| RRN trimming and `784` → `AED` fixes; refunds/reversals block a dispute | Section 4 |
| Tests: dispute core 206, simulator/dispute/workflow screens and API all pass | Full run on 2026-10-22 |

**Waiting on someone else**

| Item | Owner |
|---|---|
| Update the MMS module to the final v2 contract; repoint the transaction-grid "Raise dispute" button and the Admin "Create dispute" screen to v2 (`mms_admin`) | MMS team |
| Give us MMS's webhook URL (we then set it on their key); rename their webhook headers to `X-Signature` / `X-Timestamp` | MMS team → us |
| Raise MMS upload limits to at least 12 MB; make old MMS admin dispute screens read-only | MMS team |
| Confirm the business numbers in section 9 (window, SLA, evidence deadline, approval limits) | Finance / Risk |
| Agree and switch on the debit-on-arrival money rule; review the older settlement recovery step | Finance / engineering |
| Scheme contacts, real sample files and channels (Mastercard production, Visa, JCB, Amex, UnionPay) | Scheme onboarding |
| Deploy: run migrations and seeds per environment, create a key per environment, restart the portal | Developer |

**Decisions still open (ours)**

- Close the old `POST /api/v1/merchant/dispute` endpoint to new disputes once MMS has switched. Until then it still accepts unauthenticated requests.
- Add an antivirus scanner for evidence files.
- Send chargeback events to MMS and build merchant evidence for chargebacks in MMS.
- The 80% SLA warning.

**Known and not ours to fix here**

- Unrelated test failures exist elsewhere in the project (8 in the reporting app's scheme-activity report, from a column-name mismatch in the MMS test database; 2 sidebar-URL tests in `NavigationTest`).

---

## 14. For developers — where things live

- Merchant disputes core: `apps/dispute_core/lib/dispute_core/merchant_disputes.ex` (+ `merchant_dispute/*`, workers `merchant_dispute_webhook_worker.ex`, `merchant_dispute_sweep_worker.ex`).
- API: `apps/platform_web/lib/platform_web/controllers/merchant_dispute_api_controller.ex`, plug `plugs/merchant_api_v2.ex`, routes in `router.ex` under `/api/v2/merchant`.
- Screens: `apps/platform_web/lib/platform_web/live/merchant_dispute_live/{index,show,reports}.ex`.
- Chargeback workflow (Phase B): `apps/dispute_core/docs/dispute_workflow.md`.
- Migrations: `20261022100001–4` (chargeback workflow), `20261023100001–4` (merchant disputes).
- Tests: `apps/dispute_core/test/dispute_core/{merchant_disputes,workflow,transitions,normalize}_test.exs`, `apps/platform_web/test/platform_web/{controllers/merchant_dispute_api_test,live/merchant_dispute_live_test}.exs`.
