---
feature: Order chat with the customer
slug: chat
surface: provider
consumer: flutter
version: 3.0.1
status: ready
brd_version: "1.3"
brd_ids: [PROV-021]
business_rules: [BR-03, BR-15]
decisions: [TD-16, TD-19, TD-25, TD-96, TD-144, TD-145, TD-146, TD-147, TD-150, DEC-14, DEC-27]
schema_tables: [orders, conversations, messages, notifications, notification_preferences]
screens: [provider--chat--*]
related_docs: [orders.md, ../client/chat.md]
last_updated: 2026-11-02
---

# Provider order chat — Flutter integration

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 3.0.1 | 2026-11-02 | PATCH | Released as `ready`: the 3.0.0 contract is final. The provider also receives the `chat_message` inbox row and push when the customer writes (the doc said only the customer did); DEC-14's open part is only retention; the error table lists each `error_code`. No contract change. | Open the order chat when a `chat_message` notification is tapped. |
| 3.0.0 | 2026-11-01 | Major | `status`, `sender_role` and `type` are `{value,label}` objects in HTTP and socket payloads; sends are rate limited; history of an order with no messages no longer creates a conversation (`conversation.id` is `null`). | Read `.value` of the three fields (labels are localized, never branch on them); handle 429 `RATE_LIMITED` on send; accept `conversation.id: null`. |
| 2.0.0 | 2026-10-29 | Major | Conversation list paginates, unread follows provider role, recovery ordering is explicit. | Read `data.data` with pagination metadata and refetch summaries on events. |
| 1.0.0 | 2026-10-29 | Initial | Paid order text chat and explicit unread state. | Add private message and summary listeners, then recover on reconnect. |

## Changelog

### 3.0.1 — 2026-11-02

- **Changed (docs):** status `in-progress` → `ready`. The endpoints, socket events and Postman examples were re-verified against the code and tests; the 3.0.0 contract is unchanged.
- **Fixed (docs):** §3 said only the customer gets the generic `chat_message` inbox row and FCM push. The backend alerts the **recipient**, whichever side did not send, so the provider gets one for every customer message (`data` carries `order_id`, `conversation_id`, `message_id`; never the text).
- **Clarified:** DEC-14's admin-monitoring part is decided (TD-150); only the general retention period is open and nothing is deleted meanwhile. §2 lists each `error_code`.
- **Breaking:** No. **App action required:** route a tapped `chat_message` notification to the order chat.

### 3.0.0 — 2026-11-01

- **Changed (breaking):** conversation `status` (`open`, `read_only`), message `sender_role` (`customer`, `provider`) and message `type` (`text`) were strings and are now `{value,label}` objects. This covers the list, history, send and read responses and the `chat.message` and `chat.conversation.updated` events. HTTP labels follow the request language; each socket event carries labels in its **recipient's** profile language. Branch on `value`, never on `label`.
- **Changed:** `GET /orders/{order}/conversation` never writes. When the order can chat but has no messages yet it returns `messages: []`, `has_more: false` and a summary with `id: null`, `latest_message: null`, `unread_count: 0`, `status.value: open`; the row is created by the first send. An order that cannot chat and has no conversation still answers 422 `ORDER_CHAT_UNAVAILABLE`.
- **Added:** `POST /orders/{order}/conversation/messages` is throttled per user (`ORDER_CHAT_PER_MINUTE`, default 30) and answers 429 `RATE_LIMITED`; honor `Retry-After`. Postman: `provider.conversations.messages.store` › "429 Provider too many chat messages".
- **App action required:** update the models for the three enum fields, keep the empty-history shape in the chat screen, and back off on 429. Decision: TD-147.

### 2.0.0 — 2026-10-29

`GET /conversations` returns a paginated object instead of a bare array. Provider unread is based on customer-role messages, including when one account has both profiles. Telescope redacts chat content while retaining safe diagnostic metadata. No provider mobile UI changed here.

### 1.0.0 — 2026-10-29

Provider order chat is enabled under TD-144/145. Attachment, voice, edit, deletion and phone calls remain outside scope.

## 1. Business summary and data model

The provider's approved account and provider JWT may access only her order conversations. Sending requires paid confirmation and an active execution state; completed/cancelled history is read-only. `conversations` is unique by order, `messages` persists text and read state, and generic recipient alerts are in `notifications`. Retention remains open under DEC-14.

## 2. Endpoints

Base path `/api/v1/provider`; all require an approved provider JWT.

| Route | Method and path | Request | Success | Errors |
|-------|-----------------|---------|---------|--------|
| `provider.conversations.index` | `GET /conversations` | Optional `page` integer ≥ 1 (default 1), `per_page` integer 1–100 (default 15) | 200 paginated summaries in `data.data` | 401/403, 422 invalid page/size |
| `provider.conversations.show` | `GET /orders/{order}/conversation` | Optional `before` message UUID | 200 history, 50 messages/page; an order with no messages yet returns an empty history and `conversation.id: null` | 404 foreign order/cursor, 422 malformed cursor or unavailable unopened order |
| `provider.conversations.messages.store` | `POST /orders/{order}/conversation/messages` | Required `body` text, max 4000 characters | 201 persisted message | 404 foreign order, 422 invalid body or chat unavailable, 429 too many sends |
| `provider.conversations.read` | `POST /orders/{order}/conversation/read` | Required `through_message_id` UUID of displayed message | 200 updated unread summary | 404 foreign message/order, 422 malformed UUID |

| HTTP | `error_code` | When | App action |
|------|--------------|------|------------|
| 401 | `UNAUTHENTICATED` | Missing or expired token | Refresh the token or log in again. |
| 403 | `PERMISSION_DENIED` | Token issued by another app | Use the provider app token. |
| 403 | `ACCOUNT_INACTIVE` | Provider account is not `approved` (BR-03) | Show the review-status screen. |
| 404 | `ENTITY_NOT_FOUND` | Another provider's order, unknown order, or a `before` / `through_message_id` UUID outside this conversation | Close the chat screen. |
| 422 | — (`data.errors`) | Bad `page` / `per_page`, malformed UUID, empty or too-long `body` | Show the field error from `data.errors`. |
| 422 | `ORDER_CHAT_UNAVAILABLE` | Send on an order that is not paid or no longer in `confirmed` … `in_progress`; history of such an order that never had a message | Switch to read-only (or hide chat) and refetch the order's `can_chat`. |
| 429 | `RATE_LIMITED` | More than `ORDER_CHAT_PER_MINUTE` (default 30) sends a minute | Keep the draft, wait `Retry-After`. |

The list orders newest persisted message ID first, then conversation ID descending. Empty conversations sort last and by newest conversation ID. Items contain `{id,order_id,status,latest_message,unread_count}`; `status` is `{value,label}` with `value` `open` or `read_only`, and a message is `{id,sender_role,type,body,created_at}` with `sender_role` `{value: customer|provider, label}` and `type` `{value: text, label}`. The Laravel pagination object supplies `current_page`, `per_page`, `total`, `last_page`, `from`, `to`, page URLs and `links`; follow `next_page_url` until null. Public record IDs remain UUIDs. Postman `provider.conversations.index` › "200 List provider conversations" captures the same page shape as the customer example, with `unread_count: 1` for the provider. Postman `provider.conversations.index` › "422 Provider conversation page size exceeds maximum" captures the validation response.

Postman `provider.conversations.messages.store` › "201 Provider sends text message" and `provider.conversations.read` › "200 Mark displayed messages read" contain the captured response JSON. The customer guide shows the complete captured message response; the provider response has the same shape with `sender_role.value: provider`.

```json
{
    "status": "success",
    "message": "",
    "data": {
        "id": "01a19619-16b4-7263-b7c0-c47a1ccb5b3e",
        "sender_role": {
            "value": "provider",
            "label": "Provider"
        },
        "type": {
            "value": "text",
            "label": "Text"
        },
        "body": "On my way.",
        "created_at": "2026-10-25T07:00:00+00:00",
        "order_id": "01a19619-0ff7-73d9-90e5-52260adae3b0",
        "conversation_id": "01a19619-1639-720f-90ef-038fd272c2c7"
    }
}
```

## 3. Events, notifications and consumer lifecycle

`chat.message` sends the persisted message and order/conversation UUID to both participant app rooms; enum fields are `{value,label}` with labels in each recipient's profile language. `chat.conversation.updated` sends a role-specific summary (`id`, `order_id`, `status`, `latest_message`, `unread_count`) to each participant; read changes go only to the reader's devices, without a peer receipt. The recipient (the provider for a customer message, the customer for a provider message) receives a generic `chat_message` inbox row and preference-aware FCM push carrying `order_id`, `conversation_id` and `message_id`, with no message body in the alert or push data. Telescope retains safe metadata but redacts chat request/response content and message-bearing model, query and Redis entries; authorized app payloads still contain text.

1. Register `chat.message` and `chat.conversation.updated` listeners before connecting the provider socket. Key messages by UUID and merge events/history pages without duplicates.
2. On reconnect, fetch visible `/conversations` pages and the active order's newest history page. To recover more than 50 missed messages, request older pages with `before` set to the **first** UUID of the previous page. Continue until the last locally known message appears or `has_more` is false; use the server page order for display.
3. Give each list/history request a local generation. If an event arrives while it is in flight, retain the event's message, discard that response and refetch. Apply only a response from the current generation so older HTTP data cannot overwrite newer text or unread state.
4. Treat a summary event as an invalidation/refetch prompt. It may update the row immediately, but a delayed summary event must not replace a newer authoritative HTTP result. Refetch again when a later event arrives; no server revision field is required.
5. Send `/conversation/read` only through the latest message UUID actually displayed. The server marks incoming customer-role messages at or before it, and only this provider app's sessions receive the unread update. A history GET never marks read.
6. When the screen closes, invalidate outstanding screen-specific responses. Re-run the recovery procedure when it reopens.

## 4. Acceptance and open decisions

AC-PROV-021: no chat on rejected or unpaid/unconfirmed orders; paid confirmed participants can send (`OrderChatTest`).

- **DEC-14 (partly resolved):** admin monitoring is decided (TD-150, read-only and audited). The general retention period is open; no deletion runs until it is decided.
- **DEC-27 (open):** customer ↔ provider phone calls. Do not show the call buttons from the screens.
- **Out of scope (TD-144):** attachments, voice notes, editing and deleting messages.
- The mobile UI is built in the Flutter repository, not here. The backend contract above is final for this version.
