Add entity schema pack for main Admin domain objects
Document Money, Org/Commercial, Staff, Doctor, Rx, Order, Item/UOM, Stock, and related envelopes so Admin and the cross-app master share one schema source. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
8616c03645
commit
80e8197539
|
|
@ -39,6 +39,7 @@ Demo password for all accounts: `demo123`
|
||||||
|
|
||||||
- **Cross-app master:** [`docs/GISHEN-MASTER-SPEC.md`](docs/GISHEN-MASTER-SPEC.md) — Admin / Mob / Ecom / B2B surface map, shared domain objects, auth providers, divergence notes. **Refresh before `git push`** (Cursor rule + optional `.githooks/pre-push`).
|
- **Cross-app master:** [`docs/GISHEN-MASTER-SPEC.md`](docs/GISHEN-MASTER-SPEC.md) — Admin / Mob / Ecom / B2B surface map, shared domain objects, auth providers, divergence notes. **Refresh before `git push`** (Cursor rule + optional `.githooks/pre-push`).
|
||||||
- **Admin backend contract:** [`docs/admin-backend-spec.md`](docs/admin-backend-spec.md) — updated whenever Admin modules are finalized.
|
- **Admin backend contract:** [`docs/admin-backend-spec.md`](docs/admin-backend-spec.md) — updated whenever Admin modules are finalized.
|
||||||
|
- **Entity schemas:** [`docs/schemas.md`](docs/schemas.md) — TypeScript + field tables for main domain elements (Money, Org, Rx, Order, Item, Stock, …).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# One-time: enable in-repo pre-push reminder for this clone
|
# One-time: enable in-repo pre-push reminder for this clone
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
| Date | Change |
|
| Date | Change |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| 2026-08-08 | **Entity schemas:** Added shared schema summary (§2.9) + link to Admin [`docs/schemas.md`](./schemas.md) for full field tables on Money, Staff, Org/Commercial, Doctor, Rx, Order, Item/UOM, Stock, Customer, Settlement, Dispatch. |
|
||||||
| 2026-08-08 | **Org/Money alignment:** Canonical org status `pending_activation`; Admin activate path + `CommercialTerms`/`Money`; B2B register `POST /v1/org/register` + `org.activated`/`org.suspended`. Resolved Money default (decimal major ETB). Refreshed divergence table + Sources stamp after sibling B2B/Ecom skim. |
|
| 2026-08-08 | **Org/Money alignment:** Canonical org status `pending_activation`; Admin activate path + `CommercialTerms`/`Money`; B2B register `POST /v1/org/register` + `org.activated`/`org.suspended`. Resolved Money default (decimal major ETB). Refreshed divergence table + Sources stamp after sibling B2B/Ecom skim. |
|
||||||
| 2026-08-07 | **Seed batch:** Created master spec from Admin living contract + sibling Mob/Ecom/B2B/Dispatch sheets. Documented product surface map, shared domain objects (auth, orgs, doctors, orders/guest, Rx dosing, catalogue multi-UOM + PDP, stock, loyalty), auth provider matrix, divergence & sync notes, deeper-doc links, Sources stamp, and pre-push update rule. Wired `.cursor/rules` + optional `.githooks/pre-push` reminder. Linked from Admin `README.md` and `admin-backend-spec.md`. |
|
| 2026-08-07 | **Seed batch:** Created master spec from Admin living contract + sibling Mob/Ecom/B2B/Dispatch sheets. Documented product surface map, shared domain objects (auth, orgs, doctors, orders/guest, Rx dosing, catalogue multi-UOM + PDP, stock, loyalty), auth provider matrix, divergence & sync notes, deeper-doc links, Sources stamp, and pre-push update rule. Wired `.cursor/rules` + optional `.githooks/pre-push` reminder. Linked from Admin `README.md` and `admin-backend-spec.md`. |
|
||||||
|
|
||||||
|
|
@ -40,7 +41,7 @@ Update these dates whenever you reconcile against a sibling.
|
||||||
|
|
||||||
| Source | Path (sibling area) | Found | Spec / README | Last skimmed |
|
| Source | Path (sibling area) | Found | Spec / README | Last skimmed |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| **Admin** | `Gishen-Admin/` | ✅ | [`docs/admin-backend-spec.md`](./admin-backend-spec.md) | 2026-08-08 |
|
| **Admin** | `Gishen-Admin/` | ✅ | [`docs/admin-backend-spec.md`](./admin-backend-spec.md) + [`docs/schemas.md`](./schemas.md) | 2026-08-08 |
|
||||||
| **Mob** | `Gishen-Mob/` | ✅ | [`docs/BACKEND_SPEC.md`](../../Gishen-Mob/docs/BACKEND_SPEC.md) (relative from sibling root) | 2026-08-07 |
|
| **Mob** | `Gishen-Mob/` | ✅ | [`docs/BACKEND_SPEC.md`](../../Gishen-Mob/docs/BACKEND_SPEC.md) (relative from sibling root) | 2026-08-07 |
|
||||||
| **Ecom** | `Gishen-Ecom/` | ✅ | [`docs/backend.md`](../../Gishen-Ecom/docs/backend.md) | 2026-08-08 |
|
| **Ecom** | `Gishen-Ecom/` | ✅ | [`docs/backend.md`](../../Gishen-Ecom/docs/backend.md) | 2026-08-08 |
|
||||||
| **B2B** | `Gishen-B2B/` | ✅ | [`docs/backend/OVERVIEW.md`](../../Gishen-B2B/docs/backend/OVERVIEW.md) + coordination | 2026-08-08 |
|
| **B2B** | `Gishen-B2B/` | ✅ | [`docs/backend/OVERVIEW.md`](../../Gishen-B2B/docs/backend/OVERVIEW.md) + coordination | 2026-08-08 |
|
||||||
|
|
@ -182,6 +183,42 @@ Admin helper: `src/lib/dosingSchedule.ts`.
|
||||||
| Rx/Controlled | Catalogue auto-excludes loyalty eligibility (Admin) |
|
| Rx/Controlled | Catalogue auto-excludes loyalty eligibility (Admin) |
|
||||||
| B2B | Pointer/summary only; retail owns full wallet UI |
|
| B2B | Pointer/summary only; retail owns full wallet UI |
|
||||||
|
|
||||||
|
### 2.9 Shared schema index (main elements)
|
||||||
|
|
||||||
|
Canonical **field-level schemas** live in Admin: [`docs/schemas.md`](./schemas.md).
|
||||||
|
Do not invent a third shape here — change the Admin schema pack + sibling entity docs together.
|
||||||
|
|
||||||
|
| Element | Canonical TypeScript (summary) | Full schema |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| **Money** | `{ amount: string; currency: 'ETB' }` | [schemas § Money](./schemas.md#money) |
|
||||||
|
| **CommercialTerms** | `credit_limit` / `credit_used` Money + payment terms + price list + contract dates | [§ Organisation](./schemas.md#organisation--commercialterms) |
|
||||||
|
| **OrgStatus** | `pending_activation \| active \| suspended \| closed \| rejected` | same |
|
||||||
|
| **StaffUser** | `id`, `email`, `role: StaffRole`, optional `branchId`, `preferredLocale`, `authProvider` | [§ StaffUser](./schemas.md#staffuser) |
|
||||||
|
| **Doctor** | Hospital/clinic-scoped clinician; not a `StaffRole` | [§ Doctor](./schemas.md#doctor) |
|
||||||
|
| **PrescriptionItem** | `name`, `qty`, `frequency?`, `intervalHours?`, `times?: HH:mm[]` | [§ Prescription](./schemas.md#prescription) |
|
||||||
|
| **Order** | `customerType?: registered\|guest`, `customerPhone?`, `fulfillment`, `paid`, optional `doctorId`/`orgId` | [§ Order](./schemas.md#order) |
|
||||||
|
| **MedicationItem** | Catalogue master + `uoms[]` with `conversionFactor` into `baseUnit` | [§ Item](./schemas.md#medicationitem--itemuom) |
|
||||||
|
| **StockRow** | Branch × SKU × batch qty (+ `itemId`, `erpQty`) | [§ StockRow](./schemas.md#stockrow) |
|
||||||
|
| **Customer** | Shared `customer_id`, loyalty tier/points | [§ Customer](./schemas.md#customer) |
|
||||||
|
| **DomainEvent** | `id`, `type`, `occurred_at`, `org_id?`, `actor_id?`, `payload`, `schema_version` | [§ Envelopes](./schemas.md#api-envelopes) |
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Platform Money (Admin + B2B agreement)
|
||||||
|
interface Money { amount: string; currency: 'ETB' }
|
||||||
|
|
||||||
|
// Org commercial (Admin writes; B2B reads)
|
||||||
|
interface CommercialTerms {
|
||||||
|
credit_limit: Money
|
||||||
|
credit_used: Money
|
||||||
|
payment_terms_days: number
|
||||||
|
price_list_id?: string
|
||||||
|
contract_start?: string
|
||||||
|
contract_end?: string
|
||||||
|
activated_at?: string
|
||||||
|
activated_by?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Auth provider matrix
|
## 3. Auth provider matrix
|
||||||
|
|
@ -233,6 +270,7 @@ Accurate as of **2026-08-08** skim. Prefer fixing sheets over inventing a third
|
||||||
| Doc | Purpose |
|
| Doc | Purpose |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| [`docs/admin-backend-spec.md`](./admin-backend-spec.md) | Living Admin API/UI contract (primary depth) |
|
| [`docs/admin-backend-spec.md`](./admin-backend-spec.md) | Living Admin API/UI contract (primary depth) |
|
||||||
|
| [`docs/schemas.md`](./schemas.md) | **Entity schemas** — Money, Org, Rx, Order, Item, Stock, … |
|
||||||
| [`README.md`](../README.md) | Runbook, demo accounts, deploy |
|
| [`README.md`](../README.md) | Runbook, demo accounts, deploy |
|
||||||
| `src/lib/dosingSchedule.ts` | Frequency → default times helpers |
|
| `src/lib/dosingSchedule.ts` | Frequency → default times helpers |
|
||||||
| `src/config/navigation.ts` | Page registry |
|
| `src/config/navigation.ts` | Page registry |
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@
|
||||||
|
|
||||||
**Cross-app master:** [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) — product surface map, shared domain objects, auth matrix, and divergence notes vs Mob / Ecom / B2B. Refresh the master (and this sheet) before pushes; see its Update rule.
|
**Cross-app master:** [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) — product surface map, shared domain objects, auth matrix, and divergence notes vs Mob / Ecom / B2B. Refresh the master (and this sheet) before pushes; see its Update rule.
|
||||||
|
|
||||||
|
**Entity schemas:** [`schemas.md`](./schemas.md) — TypeScript + field tables for Money, Staff, Organisation/Commercial, Doctor, Prescription, Order, Catalogue Item, Stock, Customer, Settlement, Dispatch, envelopes.
|
||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
| Date | Change |
|
| Date | Change |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| 2026-08-08 | **Entity schemas pack:** [`schemas.md`](./schemas.md) for main domain elements; linked from Conventions + master |
|
||||||
| 2026-08-08 | **B2B org alignment:** `pending_activation`, `POST .../activate` + `CommercialTerms`/`Money`, events `org.activated`/`org.suspended`; B2B register path `POST /v1/org/register` |
|
| 2026-08-08 | **B2B org alignment:** `pending_activation`, `POST .../activate` + `CommercialTerms`/`Money`, events `org.activated`/`org.suspended`; B2B register path `POST /v1/org/register` |
|
||||||
| 2026-08-07 | Pointer to cross-app [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) + pre-push refresh rule |
|
| 2026-08-07 | Pointer to cross-app [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) + pre-push refresh rule |
|
||||||
| 2026-08-06 | Scaffold: conventions, roles, auth, empty module sections |
|
| 2026-08-06 | Scaffold: conventions, roles, auth, empty module sections |
|
||||||
|
|
@ -58,6 +61,56 @@
|
||||||
| Errors | `{ "error": { "code": string, "message": string, "details"?: object } }` |
|
| Errors | `{ "error": { "code": string, "message": string, "details"?: object } }` |
|
||||||
| Pagination | `?page=&limit=` → `{ data, meta: { page, limit, total } }` |
|
| Pagination | `?page=&limit=` → `{ data, meta: { page, limit, total } }` |
|
||||||
|
|
||||||
|
### Entity schemas (setup)
|
||||||
|
|
||||||
|
Full schemas for the main domain objects live in **[`schemas.md`](./schemas.md)** (TypeScript interfaces + field tables + samples). Keep that file in sync with `src/types/index.ts`, `src/mocks/catalog.ts`, and `src/mocks/data.ts`.
|
||||||
|
|
||||||
|
| Element | Schema anchor |
|
||||||
|
| --- | --- |
|
||||||
|
| Money / envelopes / events | [`schemas.md#money`](./schemas.md#money) · [envelopes](./schemas.md#api-envelopes) |
|
||||||
|
| StaffUser · Branch | [`#staffuser`](./schemas.md#staffuser) · [`#branch`](./schemas.md#branch) |
|
||||||
|
| Organisation · CommercialTerms | [`#organisation--commercialterms`](./schemas.md#organisation--commercialterms) |
|
||||||
|
| Doctor | [`#doctor`](./schemas.md#doctor) |
|
||||||
|
| Prescription · dosing lines | [`#prescription`](./schemas.md#prescription) |
|
||||||
|
| Order · guest/registered | [`#order`](./schemas.md#order) |
|
||||||
|
| MedicationItem · ItemUom | [`#medicationitem--itemuom`](./schemas.md#medicationitem--itemuom) |
|
||||||
|
| StockRow · Customer · Settlement | [`#stockrow`](./schemas.md#stockrow) · [`#customer`](./schemas.md#customer) · [`#settlement`](./schemas.md#settlement) |
|
||||||
|
| RiderTrip · MigrationJob | [`#rider--ridertrip`](./schemas.md#rider--ridertrip) · [`#migrationjob`](./schemas.md#migrationjob) |
|
||||||
|
|
||||||
|
#### Quick reference — Money & Org (canonical)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Money { amount: string; currency: 'ETB' }
|
||||||
|
|
||||||
|
type OrgStatus = 'pending_activation' | 'active' | 'suspended' | 'closed' | 'rejected'
|
||||||
|
|
||||||
|
interface CommercialTerms {
|
||||||
|
credit_limit: Money
|
||||||
|
credit_used: Money
|
||||||
|
payment_terms_days: number
|
||||||
|
price_list_id?: string
|
||||||
|
contract_start?: string
|
||||||
|
contract_end?: string
|
||||||
|
activated_at?: string
|
||||||
|
activated_by?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Quick reference — Prescription line dosing
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DosingFrequency = 'QD' | 'BID' | 'TID' | 'QID' | 'QXH' | 'custom'
|
||||||
|
|
||||||
|
interface PrescriptionItem {
|
||||||
|
name: string
|
||||||
|
qty: number
|
||||||
|
controlled?: boolean
|
||||||
|
frequency?: DosingFrequency
|
||||||
|
intervalHours?: number
|
||||||
|
times?: string[] // HH:mm — Mob reminder seed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Staff roles
|
## Staff roles
|
||||||
|
|
||||||
`pharmacist` · `stock_manager` · `procurement` · `finance` · `marketing_manager` · `operations` · `super_admin`
|
`pharmacist` · `stock_manager` · `procurement` · `finance` · `marketing_manager` · `operations` · `super_admin`
|
||||||
|
|
|
||||||
687
docs/schemas.md
Normal file
687
docs/schemas.md
Normal file
|
|
@ -0,0 +1,687 @@
|
||||||
|
# Gishen Admin — Entity schemas
|
||||||
|
|
||||||
|
**Living schema pack** for the Admin backend contract and cross-app master.
|
||||||
|
**Spec sheet:** [`admin-backend-spec.md`](./admin-backend-spec.md) · **Master:** [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| **Source of types** | `src/types/index.ts`, `src/mocks/catalog.ts`, `src/mocks/data.ts` |
|
||||||
|
| **Wire format** | JSON over REST (`application/json`) |
|
||||||
|
| **Timestamps** | ISO-8601 UTC (`2026-08-06T08:30:00Z`) unless noted |
|
||||||
|
| **IDs** | Prefer prefix + opaque id (`org_*`, `rx_*`, `ord_*`, `adm_*`) or UUID — pick one platform-wide |
|
||||||
|
|
||||||
|
When you change a field on a main element, update **this file**, the Admin sheet changelog, and the master schema summary in the same change.
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
| Date | Change |
|
||||||
|
| --- | --- |
|
||||||
|
| 2026-08-08 | Initial schema pack for Money, Staff, Org/Commercial, Doctor, Rx, Order, Catalogue Item/UOM, Stock, Customer, Settlement, Dispatch trip, envelopes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Index
|
||||||
|
|
||||||
|
| Schema | Kind | Cross-app |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| [Money](#money) | Shared value object | B2B `Money` |
|
||||||
|
| [Envelopes](#api-envelopes) | List / error / event | B2B pagination preferred long-term |
|
||||||
|
| [StaffUser](#staffuser) | Admin principal | — |
|
||||||
|
| [Branch](#branch) | Location | Ecom `/branches` |
|
||||||
|
| [Organisation](#organisation--commercialterms) | B2B / hospital account | B2B `organisation.md` |
|
||||||
|
| [Doctor](#doctor) | External clinical actor | Open IdP |
|
||||||
|
| [Prescription](#prescription) | Rx queue | B2B Rx clinical withhold |
|
||||||
|
| [Order](#order) | Branch fulfilment | Ecom/Mob orders |
|
||||||
|
| [MedicationItem](#medicationitem--itemuom) | Catalogue master | Mob/Ecom PDP + catalog |
|
||||||
|
| [StockRow](#stockrow) | Branch inventory | ERP sync |
|
||||||
|
| [Customer](#customer) | CRM / loyalty | Shared `customer_id` |
|
||||||
|
| [Settlement](#settlement) | Finance batch | — |
|
||||||
|
| [RiderTrip](#rider--ridertrip) | Dispatch | Dispatch Bot |
|
||||||
|
| [MigrationJob](#migrationjob) | Admin imports | Distinct from B2B tenant migration |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Money
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Money {
|
||||||
|
amount: string // decimal major units, e.g. "1250.00"
|
||||||
|
currency: 'ETB'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `amount` | `string` | ✓ | Decimal string — never float JSON numbers for money |
|
||||||
|
| `currency` | `"ETB"` | ✓ | Only ETB in v1 |
|
||||||
|
|
||||||
|
Admin UI may keep parallel numeric major-ETB fields (`creditLimitEtb`, `totalEtb`) for demos — **API wiring uses `Money`** (or syncs numbers from `commercial`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API envelopes
|
||||||
|
|
||||||
|
### Success (single)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DataEnvelope<T> = { data: T }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success (list) — target platform
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ListEnvelope<T> = {
|
||||||
|
data: T[]
|
||||||
|
pagination: {
|
||||||
|
page: number
|
||||||
|
page_size: number
|
||||||
|
total_items: number
|
||||||
|
total_pages: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Legacy Admin sketches: `?page=&limit=` → `{ data, meta: { page, limit, total } }`. Prefer `page_size` / `pagination` when consolidating with B2B.
|
||||||
|
|
||||||
|
### Error
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ErrorEnvelope = {
|
||||||
|
error: {
|
||||||
|
code: string
|
||||||
|
message: string
|
||||||
|
details?: Array<{ field?: string; code?: string; message: string }> | Record<string, unknown>
|
||||||
|
request_id?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Domain event
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DomainEvent<T = Record<string, unknown>> = {
|
||||||
|
id: string
|
||||||
|
type: string // e.g. "org.activated", "prescription.review_updated"
|
||||||
|
occurred_at: string // ISO timestamp
|
||||||
|
org_id?: string | null
|
||||||
|
actor_id?: string | null
|
||||||
|
payload: T
|
||||||
|
schema_version: string // "1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StaffUser
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type StaffRole =
|
||||||
|
| 'pharmacist'
|
||||||
|
| 'stock_manager'
|
||||||
|
| 'procurement'
|
||||||
|
| 'finance'
|
||||||
|
| 'marketing_manager'
|
||||||
|
| 'operations'
|
||||||
|
| 'super_admin'
|
||||||
|
|
||||||
|
type AuthProvider = 'email' | 'google' | 'phone' | 'telegram'
|
||||||
|
|
||||||
|
interface StaffUser {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
role: StaffRole
|
||||||
|
branchId?: string
|
||||||
|
branchName?: string
|
||||||
|
preferredLocale?: 'en' | 'am'
|
||||||
|
avatarUrl?: string
|
||||||
|
authProvider?: AuthProvider
|
||||||
|
phone?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `id` | `string` | ✓ | Staff principal id |
|
||||||
|
| `name` | `string` | ✓ | Display name |
|
||||||
|
| `email` | `string` | ✓ | Login / Google subject email |
|
||||||
|
| `role` | `StaffRole` | ✓ | Single primary role in Admin SPA |
|
||||||
|
| `branchId` | `string` | | Required for pharmacist (and optionally stock) |
|
||||||
|
| `preferredLocale` | `"en" \| "am"` | | UI language |
|
||||||
|
| `authProvider` | `AuthProvider` | | How this session was established |
|
||||||
|
|
||||||
|
**Not** a StaffRole: Doctor, B2B `SUPER_USER` / `HR_ADMIN` / `FINANCE` / `MEMBER`, retail `customer`.
|
||||||
|
|
||||||
|
### Login response sketch
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "...",
|
||||||
|
"refreshToken": "...",
|
||||||
|
"user": {
|
||||||
|
"id": "adm_01H...",
|
||||||
|
"name": "Hana Pharmacist",
|
||||||
|
"email": "pharmacist@gishen.et",
|
||||||
|
"role": "pharmacist",
|
||||||
|
"branchId": "br-bole",
|
||||||
|
"branchName": "Bole Branch",
|
||||||
|
"preferredLocale": "en",
|
||||||
|
"authProvider": "email"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branch
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Branch {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
zone: string
|
||||||
|
phone: string
|
||||||
|
lat?: number
|
||||||
|
lng?: number
|
||||||
|
hours?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Organisation & CommercialTerms
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type OrganisationType = 'corporate' | 'hospital' | 'clinic' | 'ngo' | 'other'
|
||||||
|
type OrgStatus = 'pending_activation' | 'active' | 'suspended' | 'closed' | 'rejected'
|
||||||
|
|
||||||
|
interface CommercialTerms {
|
||||||
|
credit_limit: Money
|
||||||
|
credit_used: Money
|
||||||
|
payment_terms_days: number
|
||||||
|
price_list_id?: string
|
||||||
|
contract_start?: string // ISO date YYYY-MM-DD
|
||||||
|
contract_end?: string
|
||||||
|
activated_at?: string
|
||||||
|
activated_by?: string // admin user id
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Organisation {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
orgType: OrganisationType
|
||||||
|
tin: string
|
||||||
|
vatNumber?: string
|
||||||
|
businessLicense?: string
|
||||||
|
commercialRegistration?: string
|
||||||
|
status: OrgStatus
|
||||||
|
commercial: CommercialTerms | null
|
||||||
|
/** Demo UI only — keep in sync with commercial when set */
|
||||||
|
creditLimitEtb: number
|
||||||
|
usedEtb: number
|
||||||
|
billingContact: string
|
||||||
|
source: 'admin' | 'self_register'
|
||||||
|
createdAt: string
|
||||||
|
reviewedBy?: string
|
||||||
|
reviewedAt?: string
|
||||||
|
decisionNote?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `orgType` | `OrganisationType` | ✓ | `hospital` / `clinic` unlock Doctors roster |
|
||||||
|
| `status` | `OrgStatus` | ✓ | Canonical lifecycle (not bare `pending`) |
|
||||||
|
| `commercial` | `CommercialTerms \| null` | ✓ | `null` until activate / admin-create |
|
||||||
|
| `tin` | `string` | ✓ | 10-digit Ethiopian TIN |
|
||||||
|
| `source` | `admin \| self_register` | ✓ | Who created the org |
|
||||||
|
|
||||||
|
### Activate request body
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type OrgActivateBody = {
|
||||||
|
commercial: {
|
||||||
|
credit_limit: Money
|
||||||
|
payment_terms_days: number
|
||||||
|
price_list_id?: string
|
||||||
|
contract_start?: string
|
||||||
|
contract_end?: string
|
||||||
|
}
|
||||||
|
admin_notes?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Emits `org.activated`. Sample commercial payload matches B2B `organisation.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Doctor
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Doctor {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
specialty: string
|
||||||
|
licenseNumber: string
|
||||||
|
phone: string
|
||||||
|
email: string
|
||||||
|
status: 'active' | 'inactive'
|
||||||
|
orgId: string
|
||||||
|
orgName: string
|
||||||
|
branchId?: string
|
||||||
|
branchName?: string
|
||||||
|
hasLinkedAccount: boolean
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Affiliation org **must** be `orgType` `hospital` or `clinic`. Auth: `POST /auth/doctor/login` (separate JWT / `actor=doctor`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prescription
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type DosingFrequency = 'QD' | 'BID' | 'TID' | 'QID' | 'QXH' | 'custom'
|
||||||
|
type PrescriptionStatus =
|
||||||
|
| 'draft' // B2B/Ecom may create drafts
|
||||||
|
| 'submitted'
|
||||||
|
| 'under_review'
|
||||||
|
| 'approved'
|
||||||
|
| 'queried'
|
||||||
|
| 'rejected'
|
||||||
|
|
||||||
|
interface PrescriptionItem {
|
||||||
|
name: string
|
||||||
|
qty: number
|
||||||
|
controlled?: boolean
|
||||||
|
frequency?: DosingFrequency
|
||||||
|
intervalHours?: number // when frequency === 'QXH'
|
||||||
|
times?: string[] // HH:mm 24h — Mob reminder seed
|
||||||
|
sku?: string
|
||||||
|
dosage?: string
|
||||||
|
instructions?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Prescription {
|
||||||
|
id: string
|
||||||
|
customerName: string
|
||||||
|
branchId: string
|
||||||
|
status: Exclude<PrescriptionStatus, 'draft'> | PrescriptionStatus
|
||||||
|
items: PrescriptionItem[]
|
||||||
|
submittedAt: string
|
||||||
|
prescriber?: string
|
||||||
|
doctorId?: string
|
||||||
|
orgId?: string
|
||||||
|
customerId?: string // shared platform identity when known
|
||||||
|
memberId?: string // B2B member when from institutional portal
|
||||||
|
reviewStartedAt?: string
|
||||||
|
reviewedBy?: string
|
||||||
|
reviewedAt?: string
|
||||||
|
decisionNote?: string
|
||||||
|
query_message?: string
|
||||||
|
rejection_reason?: string
|
||||||
|
days_supply?: number
|
||||||
|
refill_due_at?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Rule | Notes |
|
||||||
|
| --- | --- |
|
||||||
|
| Clinical withhold | HR/Finance B2B never receive `items`, images, diagnosis |
|
||||||
|
| Verify | `POST /admin/prescriptions/:id/verify` → `prescription.review_updated` |
|
||||||
|
| Dosing helper | `src/lib/dosingSchedule.ts` |
|
||||||
|
|
||||||
|
### Verify body
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type PrescriptionVerifyBody = {
|
||||||
|
status: 'approved' | 'queried' | 'rejected'
|
||||||
|
medicine_lines?: PrescriptionItem[]
|
||||||
|
qtyAdjustments?: unknown
|
||||||
|
substitute?: unknown
|
||||||
|
notes?: string
|
||||||
|
query_message?: string
|
||||||
|
rejection_reason?: string
|
||||||
|
days_supply?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Order
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type OrderCustomerType = 'registered' | 'guest'
|
||||||
|
type OrderFulfillment = 'delivery' | 'pickup'
|
||||||
|
/** Suggested shared status pipeline (align with Ecom) */
|
||||||
|
type OrderStatus =
|
||||||
|
| 'draft'
|
||||||
|
| 'requested'
|
||||||
|
| 'pending' // demo alias
|
||||||
|
| 'pending_approval'
|
||||||
|
| 'confirmed'
|
||||||
|
| 'picking'
|
||||||
|
| 'out_for_delivery'
|
||||||
|
| 'ready_for_pickup'
|
||||||
|
| 'completed'
|
||||||
|
| 'cancelled'
|
||||||
|
|
||||||
|
interface OrderLine {
|
||||||
|
sku: string
|
||||||
|
name: string
|
||||||
|
qty: number
|
||||||
|
unitEtb: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Order {
|
||||||
|
id: string
|
||||||
|
customerName: string
|
||||||
|
customerType?: OrderCustomerType
|
||||||
|
customerId?: string
|
||||||
|
customerPhone?: string
|
||||||
|
notes?: string
|
||||||
|
branchId: string
|
||||||
|
fulfillment: OrderFulfillment
|
||||||
|
status: string // OrderStatus in production
|
||||||
|
totalEtb: number // demo major ETB; prefer Money later
|
||||||
|
channel: string // web | telegram | mobile | b2b | pos | doctor | …
|
||||||
|
assignedRiderId?: string
|
||||||
|
zone?: string
|
||||||
|
address?: string
|
||||||
|
createdAt: string
|
||||||
|
paid: boolean
|
||||||
|
rxApproved?: boolean
|
||||||
|
doctorId?: string
|
||||||
|
orgId?: string
|
||||||
|
lines?: OrderLine[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Rule | Notes |
|
||||||
|
| --- | --- |
|
||||||
|
| Guest | Requires `customerPhone`; no `customerId`; loyalty gated |
|
||||||
|
| Registered | Prefer `customerId` + shared platform identity |
|
||||||
|
| Doctor-authored | Set `doctorId` + hospital `orgId` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MedicationItem & ItemUom
|
||||||
|
|
||||||
|
Catalogue master (`/admin/catalog/items`, UI `/items`). Retail reads as `/catalog/products`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface ItemUom {
|
||||||
|
uom: string
|
||||||
|
conversionFactor: number // how many base/stock units = 1 of this UOM
|
||||||
|
isStockUom?: boolean
|
||||||
|
isPurchaseUom?: boolean
|
||||||
|
isSalesUom?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActiveIngredient {
|
||||||
|
name: string
|
||||||
|
strength: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MedicationItem {
|
||||||
|
id: string
|
||||||
|
sku: string
|
||||||
|
genericName: string
|
||||||
|
brandName: string
|
||||||
|
manufacturer: string
|
||||||
|
countryOfOrigin: string
|
||||||
|
efdaRegistrationNumber: string
|
||||||
|
barcodes: { value: string; label?: string }[]
|
||||||
|
|
||||||
|
productType: string
|
||||||
|
therapeuticClass: string
|
||||||
|
ingredients: ActiveIngredient[]
|
||||||
|
dosageForm: string
|
||||||
|
routeOfAdministration?: string
|
||||||
|
controlledSchedule?: string
|
||||||
|
|
||||||
|
packSize: string
|
||||||
|
baseUnit: string
|
||||||
|
sellByUnit: string
|
||||||
|
packUnit: string
|
||||||
|
uoms: ItemUom[]
|
||||||
|
parentItemId?: string
|
||||||
|
|
||||||
|
costPriceEtb: number
|
||||||
|
sellingPriceEtb: number
|
||||||
|
currency: 'ETB'
|
||||||
|
sellingPriceOwner: 'platform' | 'erp' | string
|
||||||
|
vatApplicable: boolean
|
||||||
|
discountEligible: boolean
|
||||||
|
b2bPriceListId?: string
|
||||||
|
|
||||||
|
prescriptionRequired: boolean
|
||||||
|
efdaStatus: string
|
||||||
|
controlledSubstance: boolean
|
||||||
|
advertisingRestricted: boolean
|
||||||
|
ageRestriction: string
|
||||||
|
|
||||||
|
shortDescription: string
|
||||||
|
dosageGuidance: string
|
||||||
|
sideEffects: string
|
||||||
|
sideEffectsMild?: string[]
|
||||||
|
sideEffectsSevere?: string[]
|
||||||
|
warnings: string
|
||||||
|
storageInstructions: string
|
||||||
|
storageNotes?: { title: string; body: string }[]
|
||||||
|
directions?: { title: string; body: string }[]
|
||||||
|
pharmacistTip?: string
|
||||||
|
useCase?: string
|
||||||
|
storefrontUnit?: string
|
||||||
|
compareAtPriceEtb?: number
|
||||||
|
alternativeItemIds: string[]
|
||||||
|
|
||||||
|
defaultReorderPoint: number
|
||||||
|
defaultExpiryAlertDays: number
|
||||||
|
|
||||||
|
images: string[]
|
||||||
|
thumbnailUrl?: string
|
||||||
|
badges: string[]
|
||||||
|
seoSlug: string
|
||||||
|
metaTitle: string
|
||||||
|
metaDescription: string
|
||||||
|
|
||||||
|
ownership: Record<string, string> // FieldGroupOwnership
|
||||||
|
lastErpSyncAt?: string
|
||||||
|
erpSyncError?: string | null
|
||||||
|
|
||||||
|
b2bEligible: boolean
|
||||||
|
loyaltyEligible: boolean
|
||||||
|
|
||||||
|
createdBy: string
|
||||||
|
createdAt: string
|
||||||
|
updatedBy: string
|
||||||
|
updatedAt: string
|
||||||
|
status: 'active' | 'draft' | 'archived'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Stock qty is always in `baseUnit`. Use `toStockQty` / `fromStockQty` for sales/purchase UOM conversions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## StockRow
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface StockRow {
|
||||||
|
sku: string
|
||||||
|
name: string
|
||||||
|
category: string
|
||||||
|
branchId: string
|
||||||
|
qty: number // baseUnit
|
||||||
|
batch: string
|
||||||
|
expiry: string // YYYY-MM
|
||||||
|
erpQty: number
|
||||||
|
unitEtb: number
|
||||||
|
itemId?: string // → MedicationItem.id
|
||||||
|
reserved?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Customer
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Customer {
|
||||||
|
id: string // platform customer_id
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
tier: string
|
||||||
|
points: number
|
||||||
|
branch: string
|
||||||
|
joinedAt: string
|
||||||
|
orders: number
|
||||||
|
spendEtb: number
|
||||||
|
lastOrderAt: string | null
|
||||||
|
avatarUrl?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same `id` links B2B `member.customer_id` and retail sessions when enrolled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Settlement
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Settlement {
|
||||||
|
id: string
|
||||||
|
channel: string // Chapa | Telebirr | COD | M-Pesa | …
|
||||||
|
date: string // YYYY-MM-DD
|
||||||
|
volumeEtb: number
|
||||||
|
status: 'reconciled' | 'pending' | 'matched' | 'unmatched' | 'refunded' | string
|
||||||
|
txnCount: number
|
||||||
|
feesEtb: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rider & RiderTrip
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Rider {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
telegramId?: string
|
||||||
|
branchId: string
|
||||||
|
zone: string
|
||||||
|
avatarUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RiderTrip {
|
||||||
|
id: string
|
||||||
|
orderId: string
|
||||||
|
customerName: string
|
||||||
|
zone: string
|
||||||
|
assignedAt: string
|
||||||
|
completedAt: string | null
|
||||||
|
status: 'delivered' | 'failed' | 'en_route' | 'returned'
|
||||||
|
minutes: number
|
||||||
|
onTime: boolean
|
||||||
|
codEtb: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Dispatch board assigns riders; Telegram bot consumes `trip.assigned` / status updates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MigrationJob
|
||||||
|
|
||||||
|
Admin **platform** import (stock, catalog, riders…) — not the same as B2B HR `MigrationJob` tenant imports.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface MigrationJob {
|
||||||
|
id: string
|
||||||
|
templateId: string // stock | catalog | hr | branches | riders | generic
|
||||||
|
templateLabel: string
|
||||||
|
filename: string
|
||||||
|
rowsTotal: number
|
||||||
|
rowsImported: number
|
||||||
|
rowsFailed: number
|
||||||
|
status: 'completed' | 'partial' | 'failed' | 'running'
|
||||||
|
runBy: string
|
||||||
|
runAt: string
|
||||||
|
durationSec: number
|
||||||
|
mapping: Record<string, string>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Sample: Organisation (active)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "org-1",
|
||||||
|
"name": "Horizon Bank",
|
||||||
|
"orgType": "corporate",
|
||||||
|
"tin": "0001234567",
|
||||||
|
"vatNumber": "VAT-ET-0001234567",
|
||||||
|
"businessLicense": "BL-AA/48291/2014",
|
||||||
|
"commercialRegistration": "CR/015842/2014",
|
||||||
|
"status": "active",
|
||||||
|
"commercial": {
|
||||||
|
"credit_limit": { "amount": "500000.00", "currency": "ETB" },
|
||||||
|
"credit_used": { "amount": "124000.00", "currency": "ETB" },
|
||||||
|
"payment_terms_days": 30,
|
||||||
|
"price_list_id": "pl_corporate_2026",
|
||||||
|
"contract_start": "2026-01-01",
|
||||||
|
"contract_end": "2026-12-31",
|
||||||
|
"activated_at": "2026-05-02T09:40:00Z",
|
||||||
|
"activated_by": "adm_finance"
|
||||||
|
},
|
||||||
|
"creditLimitEtb": 500000,
|
||||||
|
"usedEtb": 124000,
|
||||||
|
"billingContact": "finance@horizon.et",
|
||||||
|
"source": "admin",
|
||||||
|
"createdAt": "2026-05-01T00:00:00Z",
|
||||||
|
"reviewedBy": "Yonas Finance",
|
||||||
|
"reviewedAt": "2026-05-02T09:40:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sample: Prescription (under review)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "rx-1001",
|
||||||
|
"customerName": "Abebe Kebede",
|
||||||
|
"branchId": "br-bole",
|
||||||
|
"status": "under_review",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Amoxicillin 500mg",
|
||||||
|
"qty": 21,
|
||||||
|
"frequency": "TID",
|
||||||
|
"times": ["08:00", "14:00", "20:00"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"submittedAt": "2026-08-06T08:10:00Z",
|
||||||
|
"prescriber": "Dr. Selam",
|
||||||
|
"doctorId": "doc-1",
|
||||||
|
"orgId": "org-3"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
| Doc | Role |
|
||||||
|
| --- | --- |
|
||||||
|
| [`admin-backend-spec.md`](./admin-backend-spec.md) | Endpoints + modules using these schemas |
|
||||||
|
| [`GISHEN-MASTER-SPEC.md`](./GISHEN-MASTER-SPEC.md) | Cross-app map + schema summary |
|
||||||
|
| B2B `docs/backend/entities/` | Portal-facing entities (clinical withhold, members, packages) |
|
||||||
|
| Ecom `docs/backend.md` | Retail order/catalog contract |
|
||||||
Reference in New Issue
Block a user