From 80e81975392b2c16896280ad63e5c66c945a5391 Mon Sep 17 00:00:00 2001 From: Kirubel-Kibru-Yaltopia Date: Sat, 8 Aug 2026 00:40:03 +0300 Subject: [PATCH] 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 --- README.md | 1 + docs/GISHEN-MASTER-SPEC.md | 40 ++- docs/admin-backend-spec.md | 53 +++ docs/schemas.md | 687 +++++++++++++++++++++++++++++++++++++ 4 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 docs/schemas.md diff --git a/README.md b/README.md index 9d79c5b..6119bc8 100644 --- a/README.md +++ b/README.md @@ -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`). - **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 # One-time: enable in-repo pre-push reminder for this clone diff --git a/docs/GISHEN-MASTER-SPEC.md b/docs/GISHEN-MASTER-SPEC.md index d0e9639..b88b983 100644 --- a/docs/GISHEN-MASTER-SPEC.md +++ b/docs/GISHEN-MASTER-SPEC.md @@ -15,6 +15,7 @@ | 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-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 | | --- | --- | --- | --- | --- | -| **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 | | **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 | @@ -182,6 +183,42 @@ Admin helper: `src/lib/dosingSchedule.ts`. | Rx/Controlled | Catalogue auto-excludes loyalty eligibility (Admin) | | 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 @@ -233,6 +270,7 @@ Accurate as of **2026-08-08** skim. Prefer fixing sheets over inventing a third | Doc | Purpose | | --- | --- | | [`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 | | `src/lib/dosingSchedule.ts` | Frequency → default times helpers | | `src/config/navigation.ts` | Page registry | diff --git a/docs/admin-backend-spec.md b/docs/admin-backend-spec.md index 72e60f0..9e06e01 100644 --- a/docs/admin-backend-spec.md +++ b/docs/admin-backend-spec.md @@ -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. +**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 | 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-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 | @@ -58,6 +61,56 @@ | Errors | `{ "error": { "code": string, "message": string, "details"?: object } }` | | 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 `pharmacist` · `stock_manager` · `procurement` · `finance` · `marketing_manager` · `operations` · `super_admin` diff --git a/docs/schemas.md b/docs/schemas.md new file mode 100644 index 0000000..0d020ce --- /dev/null +++ b/docs/schemas.md @@ -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 = { data: T } +``` + +### Success (list) — target platform + +```ts +type ListEnvelope = { + 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 + request_id?: string + } +} +``` + +### Domain event + +```ts +type DomainEvent> = { + 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 + 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 // 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 +} +``` + +--- + +## 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 |