# 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. |