# Borrower app: mandate refresh and support chat

All endpoints below require the normal borrower bearer token unless the path starts with `/affiliate`.

## Paystack mandate authorization

### Read current state

`GET /api/v1/profile/paystack-mandate`

The `mandate` object contains `status`, `authorization_url`, `retry_available`,
`retry_available_at`, `expires_at`, and `attempt`. Do not show the retry button when
`retry_available` is false. An active mandate never returns an authorization URL.

### Retry an expired link

`POST /api/v1/profile/paystack-mandate/retry`

Only the **latest** Paystack mandate counts: an older active mandate no longer blocks a
relink when the newest one failed.

- Latest mandate **cancelled, expired, or confirmed failed by the live status check** →
  a replacement link is created immediately.
- Latest link still **pending** → idempotent inside the three-hour window (repeated taps
  return the current link); a replacement is created once it is three hours old.
- A mandate that has already received customer consent and is awaiting bank activation is
  never reset.

To check both Mono and Paystack live in one call, use `GET /api/v1/profile/mandates/status`
— see [mobile-mandate-status-and-relink.md](mobile-mandate-status-and-relink.md).

The scheduler checks stale mandates every 15 minutes and makes up to three automatic
attempts. A successful automatic refresh sends email, in-app, and push notifications.

## Borrower support chat

### Open and list conversations

- `POST /api/v1/support/conversations`
- `GET /api/v1/support/conversations`
- `GET /api/v1/support/conversations/{uuid}`

Opening payload:

```json
{
  "category": "application",
  "subject": "Optional subject",
  "body": "I need help with my application",
  "priority": "normal",
  "client_message_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

Categories are `application`, `mandate`, `repayment`, `complaint`, or `other`.
Generate a UUID `client_message_id` for every message and reuse it when retrying a
failed HTTP request; this prevents duplicate messages.

### Messages and state

- `POST /api/v1/support/conversations/{uuid}/messages`
- `POST /api/v1/support/conversations/{uuid}/read`
- `POST /api/v1/support/conversations/{uuid}/close`

Message payload:

```json
{
  "body": "Message text",
  "client_message_id": "550e8400-e29b-41d4-a716-446655440001"
}
```

Subscribe to the private Reverb channel for instant messages, read receipts, and
conversation status changes. Keep a slower HTTP refresh after reconnect as a safety
net. Closed or resolved conversations reject new messages; the customer should start
a new conversation if more help is required.

The complete mobile WebSocket contract and integration sequence are documented in
[mobile-support-chat-realtime.md](mobile-support-chat-realtime.md).

## Affiliate support inbox

- `GET /api/v1/affiliate/support/conversations`
- `GET /api/v1/affiliate/support/conversations/{uuid}`
- `POST /api/v1/affiliate/support/conversations/{uuid}/messages`
- `POST /api/v1/affiliate/support/conversations/{uuid}/read`
- `POST /api/v1/affiliate/support/conversations/{uuid}/accept`
- `POST /api/v1/affiliate/support/conversations/{uuid}/resolve`

The affiliate web application exposes these endpoints at `/dashboard/support`.


## Support team, routing, and ticket tracking

Admin → Support Chats → **Support Team** is the configuration point. Select active,
verified affiliates and save the team once. Support uses its own pool, independent of
referral ownership and the WhatsApp AI lead pool.

- New tickets rotate across eligible team members under database locks.
- Every eligible pool member receives the new-ticket email, in-app notification, and
  push. No affiliate outside this pool receives support-ticket notifications.
- All eligible team members share the inbox and can read, reply, and resolve tickets.
  The automatic owner indicates responsibility; it does not restrict teammates.
- Removing a member revokes their API access and future live events/notifications.
  Saving the team reroutes their open tickets and any waiting tickets.
- With no eligible members, tickets stay waiting and no support email is sent.
- Reply notifications use in-app and push only; new-ticket emails include a direct
  link filtered by the ticket reference.

Every conversation response includes a permanent, searchable `ticket_id`, for example
`SUP-000123`. It derives from the conversation's primary key, so historical tickets
receive references without a backfill. Existing UUID URLs remain unchanged.
Tracking includes customer details, category, source, owner, priority, created/assigned/
last-activity timestamps, first-response timestamp, and resolution/closure timestamps.
Admin and team views include actor-attributed activity and internal notes; borrower
responses and broadcasts do not expose internal notes.

Admin endpoints (admin account plus the existing borrowers permission middleware):

- `GET /api/v1/admin/support/team` — selected members and current eligibility.
- `PUT /api/v1/admin/support/team` — `{ "affiliate_ids": [1, 2] }`; an empty array clears
  the pool. Inactive/unverified IDs and duplicates are rejected.
- `GET /api/v1/admin/support/affiliates?search=...` — eligible selection candidates;
  `team_only=true` restricts results to the configured team for owner overrides.
- `GET /api/v1/admin/support/conversations?search=SUP-000123&status=open` — searchable
  tickets, paginated results, and status totals. `open` means assigned or active.
- `PATCH /api/v1/admin/support/conversations/{uuid}` — optional `priority` (`normal`
  or `high`), `status` (`active`, `resolved`, `closed`), or internal `note`.
  Reopening clears resolution/closure timestamps and routes to an eligible owner, or
  leaves the ticket waiting if the pool is empty.
- `PUT /api/v1/admin/support/conversations/{uuid}/assignment` — optional manual owner
  override with `affiliate_id` (eligible team member or null).

### Deployment

Run `php artisan migrate --force` before serving the new API. The new support team
starts empty deliberately: admin must configure it in **Support Chats → Support Team**.
Existing unresolved tickets are routed when the team is first saved. The production
API deployment workflow already runs migrations. Do not seed every affiliate or
reuse the WhatsApp pool, since that would bypass the admin's explicit selection.

### WhatsApp support routing

New WhatsApp support requests now create a linked shared support conversation, including guest contacts without a borrower account. They use the Support Team pool, round-robin owner assignment, SUP tracking reference, and pool-only email/in-app/push notifications. An empty pool leaves the ticket waiting; it never falls back to the loan-lead pool.

The bot stays in the ticket thread for follow-up messages until the customer chooses `menu`. Messages sent during loan onboarding are not copied into support. Affiliate replies are delivered through the existing WhatsApp gateway using the standard queue worker, with three delivery attempts and a ticket activity entry after permanent failure. A stored delivery timestamp prevents a completed job being sent again; an ambiguous gateway timeout can still result in a duplicate, since the gateway does not provide idempotent sends.

Deployment requires the new migration, the updated WhatsApp bot, a running API queue worker, and the existing `WHATSAPP_GATEWAY_URL` / `WHATSAPP_GATEWAY_ADMIN_TOKEN` configuration. Existing historical WhatsApp tickets are preserved in the WhatsApp ticket list; this change routes newly created tickets. WhatsApp loan applications retain their separate AI loan-lead assignment flow.
