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>
9.4 KiB
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 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
{
"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 |
{
"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:
{
"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).
{
"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:
POST /rider/trips/:tripId/pod/media— binary photo (image/jpeg|image/png), returnsmedia_idPOST /rider/trips/:tripId/pod— JSON metadata
{
"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_keyper 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:
- Admin Dispatch Board subscribers
- 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. |