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>
This commit is contained in:
parent
bb81b99244
commit
b4293887a8
46
README.md
46
README.md
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Gishen Dispatch Bot
|
||||||
|
|
||||||
|
Telegram bot for **drivers / riders**. Replaces a dedicated rider mobile app for v1.
|
||||||
|
|
||||||
|
When Ops assigns a trip on the Admin Dispatch Board, this bot pushes the trip manifest to the driver’s Telegram chat, collects status updates via inline buttons, and captures proof of delivery (photo + recipient name). Status writes back to the shared backend so the Dispatch Board and customer order tracking stay in sync.
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
| Doc | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| [docs/FEATURE_BRIEF.md](docs/FEATURE_BRIEF.md) | What this bot does (acceptance inventory) |
|
||||||
|
| [docs/BACKEND_SPEC.md](docs/BACKEND_SPEC.md) | **Living** backend needs: events, endpoints, payloads, auth |
|
||||||
|
|
||||||
|
## Standing rule — backend spec
|
||||||
|
|
||||||
|
Whenever we **add or finalize** anything in this repo (feature, flow change, scaffold, payload tweak), update [`docs/BACKEND_SPEC.md`](docs/BACKEND_SPEC.md) in the same change set and append a changelog entry. If there is no new backend need, record `no backend delta` for that change so the sheet stays the single source of truth.
|
||||||
|
|
||||||
|
## Stack (intended)
|
||||||
|
|
||||||
|
| Layer | Choice |
|
||||||
|
| --- | --- |
|
||||||
|
| Runtime | Node.js |
|
||||||
|
| Bot framework | [grammY](https://grammy.dev) |
|
||||||
|
| Transport | Telegram Bot API, webhook mode |
|
||||||
|
| Shared API | One platform backend (not implemented in this repo) |
|
||||||
|
|
||||||
|
Scaffolding comes after the backend spec sheet stabilizes with the other client repos.
|
||||||
|
|
||||||
|
## In scope
|
||||||
|
|
||||||
|
- Trip / manifest push on assign
|
||||||
|
- Inline status: picked up → en route → arrived → delivered / failed
|
||||||
|
- Proof of delivery (photo + recipient name)
|
||||||
|
- Real-time status write-back (via shared backend)
|
||||||
|
- Driver identity = Telegram account, enrolled by Ops (no separate login)
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Admin Dispatch Board UI (Admin Console)
|
||||||
|
- Customer Telegram notifications / Mini App (Ecom)
|
||||||
|
- Health/medical modules, community, AI Phase 2+ (platform exclusions)
|
||||||
|
- Continuous live GPS heartbeat in v1 (poor fit for chat UX; see open items in the backend spec)
|
||||||
|
|
||||||
|
## Remote
|
||||||
|
|
||||||
|
`https://gitea.yaltopia.com/Gishen/Gishen-Dispatch-Bot.git`
|
||||||
282
docs/BACKEND_SPEC.md
Normal file
282
docs/BACKEND_SPEC.md
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
# 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. |
|
||||||
106
docs/FEATURE_BRIEF.md
Normal file
106
docs/FEATURE_BRIEF.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# Gishen Dispatch Bot — Feature Brief
|
||||||
|
|
||||||
|
**Source:** Yaltopia Tech Proposal (Rev. 2.0) + Gishen E-Commerce PRD — platform feature inventory, Admin § Dispatch Bot
|
||||||
|
**Repo:** `Gishen-Dispatch-Bot`
|
||||||
|
**Channel assumption:** Telegram (same family as customer notification bots; separate bot instance for drivers)
|
||||||
|
|
||||||
|
This document is the build brief for **this repo only**. Backend contracts live in [BACKEND_SPEC.md](./BACKEND_SPEC.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Product role
|
||||||
|
|
||||||
|
Replace a dedicated rider / driver mobile app for v1. Drivers receive trip work and report progress entirely inside Telegram. Ops enrolls drivers by linking their Telegram account; there is no separate driver login or app install beyond Telegram.
|
||||||
|
|
||||||
|
The Admin **Dispatch Board** (desk console) remains in the Admin Console repo. This bot is the rider-facing surface only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Trip assignment push
|
||||||
|
|
||||||
|
**When:** Ops assigns (or reassigns) a trip on the Dispatch Board.
|
||||||
|
|
||||||
|
**Then:** The bot delivers a trip/order manifest to the assigned driver’s Telegram chat, including:
|
||||||
|
|
||||||
|
- Trip id and branch / zone context
|
||||||
|
- Ordered stops with addresses
|
||||||
|
- Line items per stop (customer-safe names/qty; no clinical prescription narrative beyond what Ops already exposes to riders)
|
||||||
|
- COD amount if applicable
|
||||||
|
- Special delivery notes if present
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
|
||||||
|
- [ ] Assigned driver receives the manifest promptly after assign
|
||||||
|
- [ ] Reassign removes or supersedes the previous driver’s active trip message / buttons
|
||||||
|
- [ ] Cancelled trip notifies the driver and disables further status actions
|
||||||
|
|
||||||
|
### 2. Inline status updates
|
||||||
|
|
||||||
|
Driver advances the trip with inline action buttons:
|
||||||
|
|
||||||
|
`picked_up` → `en_route` → `arrived` → `delivered` / `failed`
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
|
||||||
|
- [ ] Only legal next transitions are shown (no skipping without backend allowance)
|
||||||
|
- [ ] Each successful press updates the shared backend; Dispatch Board and customer tracking reflect the new status in near real time
|
||||||
|
- [ ] Failed path requires or allows an exception reason (unreachable, wrong address, refused, partial delivery, other)
|
||||||
|
- [ ] Stale / already-advanced buttons are rejected safely (idempotent or clear error)
|
||||||
|
|
||||||
|
### 3. Proof of delivery (POD)
|
||||||
|
|
||||||
|
On `delivered` (and optionally on partial success paths if Ops requires):
|
||||||
|
|
||||||
|
- Driver sends a **photo** in the bot conversation
|
||||||
|
- Driver types **recipient name** in chat
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
|
||||||
|
- [ ] POD photo + recipient name are attached to the trip/stop record via the backend
|
||||||
|
- [ ] Delivery cannot be finalized without required POD fields (unless Ops configures an exception path later)
|
||||||
|
- [ ] Driver gets clear confirmation when POD is accepted
|
||||||
|
|
||||||
|
### 4. Driver identity & enrollment
|
||||||
|
|
||||||
|
- No separate driver credentials
|
||||||
|
- Identity = Telegram `user_id` (and username if available), linked to a platform `rider` record by an Operations admin
|
||||||
|
- Unlinked Telegram users cannot receive trips or mutate status
|
||||||
|
|
||||||
|
**Acceptance**
|
||||||
|
|
||||||
|
- [ ] Unenrolled user gets a clear “not enrolled — contact Ops” message
|
||||||
|
- [ ] Enrolled driver can list / see their active trip(s)
|
||||||
|
- [ ] Ops can unlink a driver; subsequent bot actions from that Telegram account are rejected
|
||||||
|
|
||||||
|
### 5. Real-time write-back
|
||||||
|
|
||||||
|
Status and POD updates from the bot must flow to:
|
||||||
|
|
||||||
|
- Admin Dispatch Board
|
||||||
|
- Customer order tracking (web / mobile / Mini App)
|
||||||
|
|
||||||
|
Transport is the shared backend (webhooks/events/websockets as the API team chooses). This bot does not talk to customer clients directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Non-goals (this repo / phase)
|
||||||
|
|
||||||
|
| Item | Owner / note |
|
||||||
|
| --- | --- |
|
||||||
|
| Dispatch Board UI, batching, route map, drag-reassign | Admin Console |
|
||||||
|
| Customer Telegram order alerts / Mini App commerce | Ecom Web |
|
||||||
|
| Continuous live GPS heartbeat | Deferred for v1 (see backend open items) |
|
||||||
|
| Health trackers, community, AI Phase 2+ | Platform exclusions |
|
||||||
|
| Flutter dedicated rider app | Superseded by this bot for v1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open items to flag upward
|
||||||
|
|
||||||
|
1. **Telegram confirmed** as the driver channel (default yes). Flag if Ops wants WhatsApp or a native app instead.
|
||||||
|
2. This repo owns its **own** backend spec sheet — not folded into Admin’s.
|
||||||
|
3. Customer notification bot vs dispatch bot: **separate bots**, shared worker/patterns recommended.
|
||||||
|
4. Live location: out of v1 unless Dispatch Board requires continuous map pins from riders.
|
||||||
|
5. Platform open items (checkout entitlement-split, Procurement vs Stock transfers, Mini App spec ownership) are **N/A** to this repo.
|
||||||
Reference in New Issue
Block a user