# Task list — Retrieval Request intake

**Context:** report #15 (Retrieval Requests & Chargebacks) reads
`retrieval_requests`. T1–T3 and T5 are complete: the register can be populated
by file import, by the intake API and from the operations screen, and its SLA
is watched by the alert engine. **T4, the Mastercom scheme fetch, is the only
outstanding item**, blocked on identifying the endpoint.

Plan reference: `IMPLEMENTATION_PLAN_MONITORING_REPORTS.md` §3.2.

---

## What exists now

| Piece | Status |
|---|---|
| `retrieval_requests` table (migration `20260824000004`) | ✅ built, dev + test |
| `DisputeCore.RetrievalRequest` schema, changesets, `overdue?/1` | ✅ built |
| Report #15 reading it | ✅ built |
| **T1** File import — `DisputeCore.RetrievalImporter` | ✅ built |
| **T2** Intake API — `POST /api/v1/disputes/retrieval-requests` | ✅ built |
| **T3** Operations screen — `/admin/chargebacks/retrieval-requests` | ✅ built |
| **T4** Mastercom scheme fetch | ❌ blocked |
| **T5** SLA alerting and expiry sweep | ✅ built |

All three intake paths record through `DisputeCore.RetrievalImporter.record/2`,
so they validate, de-duplicate and link to the transaction identically. Only
`source` differs (`csv`, `api`, `manual`, `mastercom`), which is what
distinguishes a stalled feed from a mistyped manual entry.

---

## T1 — File import  ✅ complete

**Why first:** it is how schemes deliver retrievals today for every acquirer
that has not integrated an API, and it needs no external dependency to build or
test.

- [x] `DisputeCore.RetrievalImporter`, modelled on `DisputeCore.CsvImporter`
      (reuse its `@column_aliases` normalisation approach — **do not** extend
      that module; chargeback and retrieval rows have different mandatory
      fields and merging them would make both harder to change).
- [x] Mandatory columns: `request_number`, `merchant_mid`, `rrn`,
      `request_date`. Everything else optional.
- [x] Skip-if-exists on `request_number`, same as the chargeback importer.
- [x] Link to `core_transactions` by RRN via **raw parameterised SQL**, not an
      Ecto query — `dispute_core` must not depend on `settlement_core`
      (`docs/adr/0002-dependency-direction.md`). Copy the pattern and the
      warning comment from `CsvImporter`.
- [x] `source: "csv"`.
- [x] Tests: valid file, duplicate request number, missing mandatory column,
      unparseable date, RRN that matches no transaction.

## T2 — Intake API  ✅ complete

- [x] `PlatformWeb.RetrievalRequestController`, `POST /api/retrieval-requests`.
- [x] Auth: the `:api_key_auth` pipeline already in the router.
- [x] Accept a single request or a batch; return per-row results so a partial
      batch does not fail wholesale.
- [x] `source: "api"`.
- [x] Tests: happy path, duplicate, malformed payload, missing auth.

## T3 — Operations screen  ✅ complete

- [x] LiveView form at `/admin/chargebacks/retrieval-requests`, listing the
      register and creating a request.
- [x] Fulfil action using `RetrievalRequest.fulfil_changeset/2`, which stamps
      `fulfilled_at` — a request marked fulfilled with no timestamp cannot be
      measured against its SLA.
- [x] Menu entry in `DisputeCore.MenuProvider`, group "Case Management".
- [x] Permission — reuse the dispute permissions rather than minting a new one
      unless Ops asks for a split.
- [x] `source: "manual"`.

## T4 — Mastercom retrieval fetch  ⚠️ blocked

**Blocker: the Mastercom retrieval endpoint has not been identified.**

`DisputeCore.CardScheme.MastercomClient` currently exposes `health_check`,
`list_queues`, `get_queue_content`, `get_claim_detail`,
`create_second_presentment`, `create_case_filing` and `create_fee` — **no
retrieval endpoint**.

- [ ] **Identify the endpoint** in the Mastercom v6 API — likely a queue type
      under `get_queue_content/1` rather than a new path, which would make this
      much smaller than it looks. Check the Mastercom API reference for
      retrieval / "Retrieval Request" queue names before writing any client
      code.
- [ ] Add the client function. OAuth1 signing and key loading already work
      (`card_scheme/mastercom/oauth1_signer.ex`, `key_loader.ex`) — reuse, do
      not reimplement.
- [ ] `MastercomRetrievalSyncWorker`, modelled on
      `MastercomQueueSyncWorker`.
- [ ] `source: "mastercom"`.
- [ ] Map the scheme's reason codes onto `reason_code` /
      `reason_description`. `dispute_reason_codes` already holds Mastercard
      codes; check whether retrieval reasons share that table or need their
      own.
- [ ] **Watch the sandbox-growth trap:** `chargeback_cases` reached ~100k rows
      of shared sandbox test data from the existing Mastercom sync (project
      memory `mastercom_sandbox_growth`). A retrieval sync against the same
      sandbox will do the same to `retrieval_requests`, and #15's figures will
      follow. Decide up front whether the worker runs in non-production.

## T5 — SLA monitoring  ✅ complete

- [x] Alert on retrievals approaching `response_due_date`. `AlertsCore` already
      does escalation and dispatch — register a metric through
      `TmsCore.AlertsCore.MetricEvaluator` rather than building a second
      alerting path (the precedent is `PaymentsCore.TransactionAlertMetrics`).
- [x] An expiry job to move overdue `received`/`in_progress` rows to `expired`.
      Note `overdue?/1` computes this at read time; the status is what records
      that it actually happened.

---

## Remaining

**T4** waits on the Mastercom endpoint being identified. Everything else is
built.

## What T1–T3 delivered

- `DisputeCore.RetrievalImporter` — tolerant file parsing (headers reduced to
  letters and digits, so spacing and casing stop mattering), per-row error
  reporting so a partly-bad file imports its good rows, and skip-if-exists so
  a resent file cannot revert a request already worked.
- `PlatformWeb.RetrievalRequestController` — accepts a single request or a
  batch, returns the outcome of every row, requires ISO 8601 dates. A
  malformed row does not cost the rest of the batch.
- `PlatformWeb.ChargebackLive.RetrievalRequests` — the working screen: the
  open set by default, overdue requests flagged, manual entry, and recording a
  response. Listed under Disputes → Case Management.

## What T5 delivered

- `DisputeCore.RetrievalAlertMetrics` — three metrics registered through
  `TmsCore.AlertsCore.MetricEvaluator`, so the engine's existing cron evaluates
  them with no bespoke worker: `retrievals_due_soon` (the one that prevents the
  loss), `retrievals_overdue`, and `retrievals_unanswered_days` for requests
  recorded without a response date.
- `DisputeCore.Workers.RetrievalExpiryWorker` — daily at 05:30, records that a
  response window closed. A grace day, so evidence sent on the due date is not
  a breach.
- `priv/repo/seeds/retrieval_alert_rules_seed.exs` — four starter rules.
- `priv/repo/seeds/retrieval_requests_seed.exs` — a working set built against
  real transactions, covering every state the screen and the rules need.

**One interaction worth knowing:** the sweep moves overdue requests to
`expired`, so a metric counting only open requests would fall to zero the
morning after a breach and read as resolved. `retrievals_overdue` counts
unanswered requests including expired ones, bounded by a lookback so historic
breaches do not accumulate into a number that only ever rises.

Filed under `compliance` rather than a dispute alert type: `AlertRule.alert_type`
is a closed enum with no dispute member, and the existing
`dispute_response_days_overdue` rule already sits there.

69 tests across T1–T3 and T5.
