# Mono: initial setup, retry, and broken-link recovery

Frontend handoff, 10 October 2026. These routes must be deployed before shipping the updated affiliate or mobile clients. Mobile source is outside this workspace.

## Three endpoints for salary draft booking

All require `Authorization: Bearer <borrower-token>`. Affiliate uses its authorized borrower session token, not the affiliate account token. Create or resume the borrower draft first; discover an existing draft with `GET /api/v1/loans/drafts/current` (`application.reference`, or `application: null`).

| Button/action | Method and path | JSON body |
| --- | --- | --- |
| Set up Mono | POST `/api/v1/loans/drafts/{reference}/collection` | `{}` |
| Continue / Retry Mono | POST `/api/v1/loans/drafts/{reference}/collection/retry` | `{}` |
| Link not working? Get a new link | POST `/api/v1/loans/drafts/{reference}/collection/recover-link` | `{"failed_mandate_id":"the-id-whose-link-failed"}` |

Initial setup and retry both check the latest Mono mandate and reuse it when possible. Initial setup is not a guarantee of creation. Retry may create when there is no usable mandate or safely replace a terminal/stale mandate. The dedicated retry route ignores `force_new`; replacement of a broken fresh link belongs to recover-link. The base collection route continues accepting the old force_new contract for existing clients.

Recover-link requires failed_mandate_id (422 if absent). Capture the ID alongside the URL when opening Mono; send that captured ID, not an ID fetched after the link failed. Returning/back navigation alone must not invoke recovery.

All three return HTTP 200 with the same envelope:

```json
{
  "message": "Collection setup checked.",
  "application": {
    "reference": "LND-EXAMPLE",
    "collection": {
      "status": "pending",
      "mandate_id": "mono-example",
      "authorization_url": "https://example.com/mono-authorization",
      "action": "authorize",
      "reused": true,
      "retry_available_at": null
    }
  }
}
```

Example is abbreviated; preserve other application fields. Recovery uses message `Collection link recovery checked.`. Treat action and readiness fields as authoritative, not the message text.

| action | Frontend behaviour |
| --- | --- |
| authorize | Open authorization_url; retain mandate_id and offer Copy link on affiliate. Pending authorization is not an error. |
| active | No URL needed; refresh draft and continue when backend readiness permits. |
| awaiting_activation | Show “Your bank is activating this mandate” and Check status. Do not create automatically. |
| retry_later | No URL; show wait until retry_available_at. Five minutes is measured from original creation, not the click. |

Recover-link checks Mono first. An active or already-approved mandate on the same bank is reused without replacement. A matching pending broken link can be replaced after five minutes, only after cancellation is confirmed. An older failed ID cannot replace a newer fresh pending mandate. Normal pending links are reused within one hour; stale/missing links also need confirmed cancellation. Terminal failed/rejected/expired/cancelled mandates can be replaced. Bank changes may need a new mandate while keeping an existing active mandate intact.

## On return from Mono

1. Call `GET /api/v1/profile/mandates/status` for a live provider check.
2. Refresh `GET /api/v1/loans/drafts/{reference}` to get draft collection and readiness.
3. If active, continue only as allowed by application.can_apply and the fee/other requirements.
4. If still pending authorization, offer Continue / Retry Mono.
5. If approved/awaiting activation, show Check status. Never infer link failure from Back/Close.

Poll no faster than about ten seconds; avoid overlapping calls. Disable all Mono controls during a request. In browsers open a placeholder popup synchronously on click, then navigate it to the returned URL or close it on no-URL/error; retain a visible copy-link fallback.

## Errors

- 401: authentication required; 404: draft absent/not owned; 422: invalid body, missing linked bank, or draft no longer eligible.
- 409 with mono.code `mono_cancellation_unconfirmed`: show server message and check again later; no replacement was created.
- 409 with mono.code `mono_setup_unconfirmed`: previous creation remains unresolved; show status/support path, not repeated create attempts.
- 503 with mono.code `mono_lookup_failed`: provider unavailable; show server message and allow a later status check.
- Creation timeout/provider errors can return 503/502. A timeout may have created a remote mandate; do not automatically submit again.

## Affiliate without an open draft / legacy mobile

Do not create a loan draft just to repair profile setup. Use `POST /api/v1/bank-statements/retry-mandate` with `{}` for setup/resume, or `{"force_new":true,"failed_mandate_id":"captured-id"}` for broken-link recovery. Its response has top-level `mandate_url`, `mandate_id`, `mandate_status`, `action`, `reused`, and `retry_available_at`. Always prefer the draft routes when a draft exists so replacement remains attached to it.

The older `POST /api/v1/loans/mandate/activate` returns `mono_url` and requires bank/BVN/customer inputs. New salary booking clients should use the three draft routes above. There are three explicit draft actions; no new standalone profile recovery endpoint is needed.

## Changes delivered in this workspace

- API: explicit retry and recover-link draft routes sharing the existing safe setup service.
- Affiliate booking: uses explicit routes and a clearly named broken-link control.
- Affiliate dashboard: resume opens the returned link; adds Copy link, Check status and broken-link recovery; selects draft routes when an open draft exists.
- Mobile FE: implement this contract in its separate codebase. No mobile app UI code was changed here.

## Acceptance scenarios for the FE agent

Verify initial setup opens one URL; repeated retry reuses the ID; pending does not display an error; Back triggers status refresh; active closes authorization UI; approved shows waiting; recovery before five minutes shows the deadline; eligible recovery opens a replacement; cancellation/lookup failure creates no duplicate; old link recovery cannot replace a newer fresh mandate; and draft refresh points to the replacement ID.

## Recovering an unresolved setup

Retry and `GET /api/v1/profile/mandates/status` now attempt to recover an
unresolved creation request by its exact reference. A `creating` request is left
alone for ten minutes so an overlapping request cannot replace an in-flight setup.
A known provider mandate ID is retained even if saving local mandate records fails.
Recovered mandates are checked live; cancelled/expired mandates can then be replaced
through the normal retry flow. Active or pending mandates follow normal reuse rules.

If no exact match is returned, retry replaces the unconfirmed setup with a new
mandate (owner decision, 2026-10-26) instead of staying blocked. The old reference is
written to the borrower Activity log (and the admin audit log when an admin retried) so
any leftover mandate can be cancelled in Mono. Only a `creating` request younger than ten
minutes still blocks, returning 409 `mono_setup_unconfirmed`; status reports
`setup_unconfirmed` for that window only.

The lookup follows the pages Mono reports in its paging metadata (`meta.paging.next`
or `pages`, up to 10 pages) and stops at an empty or repeated page.

Affiliate screens show "A Mono setup is still in progress" with the reference during
that window and label the main button Retry Mono Setup.

`php artisan mandates:reconcile-mono-setup BORROWER_ID MONO_MANDATE_ID` still records a
known mandate for an unresolved request.

Borrower email editing is already available in affiliate **Edit Profile** (the
existing borrower Profile Details stage). It saves through `PUT /api/v1/profile`,
validates email uniqueness, and clears email verification when the address changes.
