This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Dispatch-Bot/docs/BACKEND_SPEC.md
kirukib b4293887a8 Add dispatch bot feature brief and living backend spec.
Documents rider Telegram flows and the API/events this client needs so the shared backend can consolidate later.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-07 01:25:46 +03:00

283 lines
9.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Gishen Dispatch Bot — Backend Spec Sheet
**Living document.** Update this file whenever this repo adds or finalizes a feature, flow, scaffold, or payload change. Append a [Changelog](#changelog) entry in the same change set. If the change needs nothing new from the backend, log `no backend delta`.
**Consumers:** shared platform backend team (consolidation with other client repos’ spec sheets).
**Client:** `Gishen-Dispatch-Bot` (Telegram rider bot).
**Does not implement backend** — describes what the bot needs.
**Supersedes for v1:** Flutter rider-app assumptions in `Gishen-Ecom/docs/backend.md` §10 (JWT rider app + limited status enum). Auth and status machine below replace that sketch for the rider surface.
---
## 1. Auth & rider enrollment
### Model
| Concept | Description |
| --- | --- |
| Rider | Platform staff/driver record (`role: rider` or equivalent) |
| Telegram link | `telegram_user_id` (required), optional `telegram_username` |
| Enrollment | Ops admin links Telegram identity → rider; no password / OTP for drivers |
### Bot → backend auth
Recommended: **service credentials** for the bot process (API key / mTLS / signed service JWT), plus every rider-facing call includes `telegram_user_id` so the backend resolves `rider_id`.
Alternative acceptable: short-lived rider token issued after the backend verifies a Telegram login deep-link — still must bind to enrolled `telegram_user_id`.
Unenrolled `telegram_user_id` → `403` with stable error code `rider_not_enrolled`.
### Admin APIs needed (Ops / Admin Console will call these; bot may only read link status)
| Method | Path (proposed) | Purpose |
| --- | --- | --- |
| `POST` | `/admin/riders` | Create rider profile (name, phone, branch/zone, active) |
| `POST` | `/admin/riders/:riderId/telegram` | Link `{ telegram_user_id, telegram_username? }` |
| `DELETE` | `/admin/riders/:riderId/telegram` | Unlink Telegram identity |
| `GET` | `/admin/riders/:riderId` | Include telegram link status |
| `GET` | `/dispatch/riders/by-telegram/:telegramUserId` | Resolve rider for bot bootstrap (or bot-only equivalent under `/bot/...`) |
---
## 2. Domain events — backend → bot
Bot must be notified when Ops mutates assignment. Prefer a **push** to the bot service (HTTP webhook the bot exposes, or a queue the bot consumes). Polling alone is insufficient for “prompt” assign UX.
| Event | When | Payload (minimum) |
| --- | --- | --- |
| `trip.assigned` | Rider assigned to trip | `trip` manifest (see §4), `rider_id`, `telegram_user_id`, `assigned_at` |
| `trip.reassigned` | Rider changed | Previous + new `telegram_user_id`, full `trip` for new rider, `reason?` |
| `trip.cancelled` | Trip cancelled | `trip_id`, `telegram_user_id`, `cancelled_at`, `reason?` |
| `trip.updated` | Manifest material change (address, COD, stops) while in progress | `trip` (full or patch), `telegram_user_id` |
### Webhook contract (proposed)
`POST {DISPATCH_BOT_WEBHOOK_URL}/hooks/dispatch`
```json
{
"event": "trip.assigned",
"occurred_at": "2026-08-07T00:00:00Z",
"idempotency_key": "trip.assigned:trip_123:rider_9",
"data": { }
}
```
Bot responds `2xx` after durable accept. Backend retries on non-2xx with backoff. Bot treats duplicate `idempotency_key` as no-op success.
---
## 3. Trip status machine
### Statuses (v1)
| Status | Meaning |
| --- | --- |
| `assigned` | On rider; not yet picked up |
| `picked_up` | Parcel(s) collected from branch / pickup |
| `en_route` | Heading to customer |
| `arrived` | At delivery location |
| `delivered` | Completed successfully (POD required) |
| `failed` | Could not complete (exception reason required) |
**Delta vs Ecom sketch:** add `en_route` and `arrived`; keep `picked_up` / `delivered` / `failed`.
### Allowed transitions
```
assigned → picked_up → en_route → arrived → delivered
arrived → failed
en_route → failed
picked_up → failed
```
Backend is source of truth. Bot only offers buttons for transitions the API allows.
### Bot → backend
| Method | Path (proposed) | Body |
| --- | --- | --- |
| `POST` | `/rider/trips/:tripId/status` | See below |
```json
{
"telegram_user_id": 123456789,
"status": "en_route",
"occurred_at": "2026-08-07T00:05:00Z",
"idempotency_key": "trip_123:en_route:2026-08-07T00:05:00Z",
"exception": null,
"stop_id": null
}
```
For `failed`:
```json
{
"telegram_user_id": 123456789,
"status": "failed",
"occurred_at": "2026-08-07T00:10:00Z",
"idempotency_key": "trip_123:failed:…",
"exception": {
"code": "unreachable_customer",
"note": "Phone off after 3 attempts"
},
"stop_id": "stop_1"
}
```
**Exception codes (v1):** `unreachable_customer` | `wrong_address` | `refused` | `partial_delivery` | `other`
**Responses**
| Code | Meaning |
| --- | --- |
| `200` | Applied; return current trip snapshot |
| `409` | Illegal transition or trip no longer assigned to this rider |
| `403` | Not enrolled / not assignee |
| `404` | Unknown trip |
Backend fan-out: update Dispatch Board + customer order tracking after successful transition.
---
## 4. Trip manifest shape
Returned on assign events and on `GET /rider/trips/me` (or `GET /rider/trips/:tripId`).
```json
{
"trip_id": "trip_123",
"status": "assigned",
"branch": {
"id": "br_1",
"name": "Gishen Bole",
"address": "…"
},
"zone": "bole-east",
"cod_total_etb": 450.0,
"currency": "ETB",
"notes": "Call on arrival",
"assigned_at": "2026-08-07T00:00:00Z",
"stops": [
{
"stop_id": "stop_1",
"sequence": 1,
"customer": {
"display_name": "A. Bekele",
"phone": "+2519…"
},
"address": {
"line1": "…",
"line2": null,
"lat": 9.01,
"lng": 38.79,
"maps_url": "https://maps.google.com/…"
},
"cod_etb": 450.0,
"items": [
{
"sku": "MED-001",
"name": "Paracetamol 500mg 20s",
"qty": 1
}
],
"order_id": "ord_99",
"fulfilment_id": "ful_55"
}
]
}
```
### List active trips
| Method | Path | Purpose |
| --- | --- | --- |
| `GET` | `/rider/trips/me?telegram_user_id=` | Active + recently completed (window TBD) |
---
## 5. Proof of delivery (POD)
Required before or with transition to `delivered` (backend enforces).
| Method | Path | Purpose |
| --- | --- | --- |
| `POST` | `/rider/trips/:tripId/pod` | Upload POD for a stop (or whole trip if single-stop) |
**Multipart or two-step:**
1. `POST /rider/trips/:tripId/pod/media` — binary photo (`image/jpeg` \| `image/png`), returns `media_id`
2. `POST /rider/trips/:tripId/pod` — JSON metadata
```json
{
"telegram_user_id": 123456789,
"stop_id": "stop_1",
"recipient_name": "Almaz Bekele",
"media_id": "media_abc",
"captured_at": "2026-08-07T00:12:00Z",
"idempotency_key": "trip_123:stop_1:pod"
}
```
Bot obtains the photo from Telegram (`getFile`) and uploads bytes to the backend (backend should not depend on Telegram file URLs long-term).
**Delivered transition** may be combined:
`POST /rider/trips/:tripId/status` with `status: "delivered"` and embedded `pod: { … }` — acceptable if backend prefers one round-trip; document one approach as canonical. **Canonical for this sheet:** separate POD create, then status `delivered` (clearer retries).
---
## 6. What this bot does **not** need from backend (v1)
| Capability | Note |
| --- | --- |
| `POST /rider/location` continuous heartbeat | Deferred — Telegram chat is a poor fit; revisit if Dispatch Board needs live map pins from riders |
| Customer notification send | Owned by notification worker / Ecom channels |
| Catalog, cart, loyalty, B2B entitlements | Other repos |
| Pharmacist / stock / finance admin APIs | Admin Console |
---
## 7. Idempotency & errors
- All mutating bot → backend calls carry `idempotency_key` (stable per user action).
- Backend → bot webhooks carry `idempotency_key` per event emission.
- Error body shape (proposed): `{ "error": { "code": "rider_not_enrolled", "message": "…" } }`
- Bot maps known codes to Amharic/English driver-facing copy (copy can live in bot; codes must be stable).
---
## 8. Real-time fan-out (backend responsibility)
After successful status or POD write, backend must update:
1. Admin Dispatch Board subscribers
2. Customer order tracking (timeline; map when rider location exists — N/A until location is in scope)
Exact mechanism (SSE, websocket, poll) is backend-owned; this bot only requires durable API success.
---
## 9. Open items
| # | Item | Proposal |
| --- | --- | --- |
| 1 | Driver channel | Telegram (confirmed default) |
| 2 | Spec ownership | This repo’s sheet stays independent; Admin board has its own |
| 3 | Customer vs driver bots | Separate Telegram bots |
| 4 | Live GPS | Out of v1 |
| 5 | Multi-stop POD | POD per `stop_id`; trip `delivered` only when all stops terminal |
| 6 | Partial delivery | Use `failed` + `partial_delivery` or a future `partially_delivered` status — **prefer exception on `failed` for v1** until Ops defines partial completion rules |
| 7 | Path prefix | `/rider/...` vs `/bot/dispatch/...` — backend may rename; semantics above matter more than literal paths |
---
## Changelog
| Date | Change |
| --- | --- |
| 2026-08-07 | Initial backend spec: Telegram rider auth/enrollment, assign/reassign/cancel events, status machine (`en_route`, `arrived`), manifest shape, POD upload, idempotency; defer live GPS; supersede Ecom Flutter-rider §10 for v1 rider surface. |