This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-Admin/docs/schemas.md
Kirubel-Kibru-Yaltopia 80e8197539 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>
2026-08-08 00:40:03 +03:00

16 KiB

Gishen Admin — Entity schemas

Living schema pack for the Admin backend contract and cross-app master.
Spec sheet: admin-backend-spec.md · Master: 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 Shared value object B2B Money
Envelopes List / error / event B2B pagination preferred long-term
StaffUser Admin principal —
Branch Location Ecom /branches
Organisation B2B / hospital account B2B organisation.md
Doctor External clinical actor Open IdP
Prescription Rx queue B2B Rx clinical withhold
Order Branch fulfilment Ecom/Mob orders
MedicationItem Catalogue master Mob/Ecom PDP + catalog
StockRow Branch inventory ERP sync
Customer CRM / loyalty Shared customer_id
Settlement Finance batch —
RiderTrip Dispatch Dispatch Bot
MigrationJob Admin imports Distinct from B2B tenant migration

Money

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)

type DataEnvelope<T> = { data: T }

Success (list) — target platform

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

type ErrorEnvelope = {
  error: {
    code: string
    message: string
    details?: Array<{ field?: string; code?: string; message: string }> | Record<string, unknown>
    request_id?: string
  }
}

Domain event

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

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

{
  "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

interface Branch {
  id: string
  name: string
  zone: string
  phone: string
  lat?: number
  lng?: number
  hours?: string
}

Organisation & CommercialTerms

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

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

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

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

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

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.

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

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

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

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

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.

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)

{
  "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)

{
  "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"
}

Doc Role
admin-backend-spec.md Endpoints + modules using these schemas
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