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