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>
283 lines
9.4 KiB
Markdown
283 lines
9.4 KiB
Markdown
# 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. |
|