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 065423efbf Expand schemas.md into a full platform backend schema pack
Cover shared envelopes/events, Admin ops DTOs, B2B portal entities, and Ecom retail shapes so one pack backs all Gishen backend sheets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-08 00:55:48 +03:00

28 KiB
Raw Blame History

Gishen — Platform schema pack

Living schemas for the shared pharmacy platform backend and Admin ops UI.
Deeper per-repo narrative lives in sibling sheets; this file is the field-level / TypeScript source for wire shapes.

Field Value
Home Gishen-Admin/docs/schemas.md
Admin API / modules admin-backend-spec.md
Cross-app map GISHEN-MASTER-SPEC.md
B2B entities Gishen-B2B/docs/backend/entities/
Ecom API sheet Gishen-Ecom/docs/backend.md
Wire format application/json (multipart for uploads)
Timestamps ISO-8601 UTC
Money { "amount": "1250.00", "currency": "ETB" } decimal string

Sources: Admin src/types/, src/mocks/catalog.ts, src/mocks/data.ts · B2B entity docs · Ecom backend sheet.

Rule: Change field shapes here + the owning sheet (admin-backend-spec / B2B entity / Ecom backend.md) + master schema index in the same change.

Changelog

Date Change
2026-08-08 Platform pack: Shared primitives; Admin ops (POS, procurement, marketing, loyalty, FAQ, stock txns); B2B portal entities; Ecom/retail DTOs; event catalog; backend ownership map
2026-08-08 Initial Admin-focused schema pack

0. Index

Shared (platform)

Schema Section
Primitives (Money, Phone, Locale, IDs) §1
API envelopes + DomainEvent §2
Principal actors (Staff / Doctor / B2B session / Customer) §3

Admin / ops backend

Schema Section
Branch · Organisation · CommercialTerms · Doctor §4
Prescription · Order §5
MedicationItem · Stock · Procurement §6
POS · Customer · Loyalty · Marketing · FAQ §7
Settlement · Dispatch · Migration (platform) · Audit §8

B2B portal backend

Schema Section
SessionUser · Org registration · Department · Member · Package §9
B2B Finance · B2B Migration · B2B Prescription view §10

Ecom / retail backend

Schema Section
Catalog product · Cart/Order create · Entitlement · Payments · Identity §11

Ownership

| Map | §12 |


1. Shared primitives

/** Canonical money — Admin API + B2B agree. Prefer over raw number ETB on the wire. */
interface Money {
  amount: string   // "1250.00" major units
  currency: 'ETB'
}

type Timestamp = string  // ISO-8601 UTC
type DateOnly = string   // YYYY-MM-DD
type Phone = string      // E.164 e.g. +251911234567
type Locale = 'en' | 'am'

/** Opaque ids — prefer prefix for logs (B2B style); Admin demos may use short ids */
// org_* mbr_* pkg_* dept_* rx_* usr_* adm_* ord_* cus_* mig_* evt_*
Type Format Notes
Money { amount, currency: "ETB" } Never JSON floats for money
Phone E.164 Required for guests
Locale en | am Staff preferredLocale / B2B locale

Admin UI demos may keep creditLimitEtb / totalEtb numbers — sync from Money / commercial when wiring the API.


2. API envelopes & domain events

Envelopes

type DataEnvelope<T> = { data: T }

/** Target platform (B2B). Prefer when consolidating. */
type ListEnvelope<T> = {
  data: T[]
  pagination: {
    page: number
    page_size: number
    total_items: number
    total_pages: number
  }
}

/** Legacy Admin sketches — migrate to ListEnvelope */
type LegacyListEnvelope<T> = {
  data: T[]
  meta: { page: number; limit: number; total: number }
}

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

Domain event envelope

type DomainEvent<T = Record<string, unknown>> = {
  id: string                 // evt_*
  type: string
  occurred_at: Timestamp
  org_id?: string | null
  actor_id?: string | null
  payload: T
  schema_version: string     // "1"
}

Event catalog (backend bus)

Event Emitter Consumers Payload (key fields)
org.registration_submitted B2B Admin registration_id, org_id, company, super_user email
org.registration_requested B2B Admin CRM lead contact + channel
org.activated Admin B2B, Ecom org_id, commercial, activated_by, activated_at
org.suspended Admin B2B org_id, reason?
prescription.submitted B2B / Ecom Admin prescription_id, member_id?, customer_id, page_count (no images)
prescription.review_updated Admin B2B, Ecom, Mob prescription_id, status, days_supply?, query_message?
migration.job_* B2B Admin audit job_id, dataset, commit_result
order.completed Platform / Ecom B2B finance, loyalty order_id, customer_id, amounts
trip.assigned / trip.status_updated Admin dispatch Bot, tracking trip_id, riderId, status

3. Principals & actors

StaffUser (Admin pharmacy)

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?: Locale
  avatarUrl?: string
  authProvider?: AuthProvider
  phone?: string
}

Login response: { accessToken, refreshToken, user: StaffUser }.

Doctor (external clinical — not StaffRole)

interface Doctor {
  id: string
  name: string
  specialty: string
  licenseNumber: string
  phone: string
  email: string
  status: 'active' | 'inactive'
  orgId: string              // hospital | clinic Organisation
  orgName: string
  branchId?: string
  branchName?: string
  hasLinkedAccount: boolean
  createdAt: Timestamp
  updatedAt: Timestamp
  avatarUrl?: string
}

B2B SessionUser

type PortalRole = 'SUPER_USER' | 'HR_ADMIN' | 'FINANCE' | 'MEMBER'

interface SessionUser {
  id: string                 // usr_*
  email: string
  full_name: string
  phone?: Phone
  org_id: string
  member_id?: string
  customer_id?: string
  roles: PortalRole[]
  locale: Locale
  avatar_url?: string
  org_status: OrgStatus
  permissions: string[]
}

Deep fields: B2B session-user.md.

Retail Customer (CRM / loyalty)

interface Customer {
  id: string                 // cus_* / c* demos — shared customer_id
  name: string
  phone: Phone
  tier: string
  points: number
  branch: string
  joinedAt: Timestamp
  orders: number
  spendEtb: number           // demo; prefer Money on API
  lastOrderAt: Timestamp | null
  avatarUrl?: string
}

4. Admin — organisations & doctors

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'
/** + rejected for denied Admin queue entries */
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?: DateOnly
  contract_end?: DateOnly
  activated_at?: Timestamp
  activated_by?: string
}

interface Organisation {
  id: string
  name: string
  orgType: OrganisationType
  tin: string
  vatNumber?: string
  businessLicense?: string
  commercialRegistration?: string
  status: OrgStatus
  commercial: CommercialTerms | null
  creditLimitEtb: number     // demo mirror
  usedEtb: number
  billingContact: string
  source: 'admin' | 'self_register'
  createdAt: Timestamp
  reviewedBy?: string
  reviewedAt?: Timestamp
  decisionNote?: string
}

type OrgActivateBody = {
  commercial: Omit<CommercialTerms, 'credit_used' | 'activated_at' | 'activated_by'> & {
    credit_limit: Money
  }
  admin_notes?: string
}

Align with B2B organisation.md. Admin adds orgType, KYC ids, source, rejected.

Endpoints: POST /admin/organisations, .../activate, .../suspend, .../commercial — see Admin sheet § Finance & organisations.


5. Admin — prescriptions & orders

Prescription

type DosingFrequency = 'QD' | 'BID' | 'TID' | 'QID' | 'QXH' | 'custom'
type PrescriptionStatus =
  | 'draft' | 'submitted' | 'under_review' | 'approved' | 'queried' | 'rejected'

interface PrescriptionItem {
  name: string
  qty: number
  controlled?: boolean
  frequency?: DosingFrequency
  intervalHours?: number
  times?: string[]           // HH:mm 24h
  sku?: string
  dosage?: string
  instructions?: string
}

interface Prescription {
  id: string
  customerName: string
  branchId: string
  status: PrescriptionStatus
  items: PrescriptionItem[]
  submittedAt: Timestamp
  prescriber?: string
  doctorId?: string
  orgId?: string
  customerId?: string
  memberId?: string
  reviewStartedAt?: Timestamp
  reviewedBy?: string
  reviewedAt?: Timestamp
  decisionNote?: string
  query_message?: string
  rejection_reason?: string
  days_supply?: number
  refill_due_at?: DateOnly
}

type PrescriptionVerifyBody = {
  status: 'approved' | 'queried' | 'rejected'
  medicine_lines?: PrescriptionItem[]
  notes?: string
  query_message?: string
  rejection_reason?: string
  days_supply?: number
}

B2B member upload shape (pages, clinical withhold): prescription.md.

Order

type OrderCustomerType = 'registered' | 'guest'
type OrderFulfillment = 'delivery' | 'pickup'
type OrderStatus =
  | 'draft' | 'requested' | 'pending' | '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
  totalEtb: number
  channel: string            // web | mobile | telegram | b2b | pos | doctor | …
  assignedRiderId?: string
  zone?: string
  address?: string
  createdAt: Timestamp
  paid: boolean
  rxApproved?: boolean
  doctorId?: string
  orgId?: string
  lines?: OrderLine[]
}

6. Admin — catalogue, stock & procurement

MedicationItem & ItemUom

interface ItemUom {
  uom: string
  conversionFactor: number   // base units per 1 of this UOM
  isStockUom?: boolean
  isPurchaseUom?: boolean
  isSalesUom?: boolean
}

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: { name: string; strength: string }[]
  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: 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>
  lastErpSyncAt?: Timestamp
  erpSyncError?: string | null
  b2bEligible: boolean
  loyaltyEligible: boolean
  createdBy: string
  createdAt: Timestamp
  updatedBy: string
  updatedAt: Timestamp
  status: 'active' | 'draft' | 'archived'
}

API: /admin/catalog/items · UI /items. Retail maps to Ecom /catalog/products.

StockRow & stock movements

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
  reserved?: number
}

type StockTxnType = 'receipt' | 'delivery' | 'transfer' | 'adjustment'
type StockTxnStatus = 'draft' | 'submitted' | 'cancelled'

interface StockTransaction {
  id: string
  type: StockTxnType
  status: StockTxnStatus
  sku: string
  branchId: string
  qty: number
  note?: string
  createdAt: Timestamp
  createdBy?: string
}

interface StockMovement {
  id: string
  at: Timestamp
  type: string               // sale | receive | adjust | reserve | …
  qty: number
  note: string
}

Inventory receive body (Admin sheet): { itemId, branchId, batch, expiresAt, qty, uom?, reorderPoint? }.

ProcurementRequest

interface ProcurementRequest {
  id: string
  sku: string
  name: string
  qty: number
  branchId: string
  status: 'open' | 'ordered' | 'received'
  priority: 'low' | 'medium' | 'high'
  requestedBy: string
  notes?: string
  createdAt: Timestamp
}

7. Admin — POS, CRM, loyalty & marketing

POS

type PosResolveBody = { code: string }  // gishen-customer:{id} | phone | bare id

interface PosPurchaseBody {
  customerId: string
  branchId: string
  lines: { sku: string; qty: number }[]
  channel?: 'pos'
}

interface PurchaseTicketLine {
  sku: string
  name: string
  qty: number
  unitEtb: number
}

interface PurchaseTicket {
  id: string
  source: string
  status: 'pending' | 'accepted' | 'rejected'
  customerHint?: string
  lines: PurchaseTicketLine[]
  createdAt: Timestamp
}

interface CounterScan {
  id: string
  at: Timestamp
  source: 'camera' | 'manual' | 'simulated' | 'ticket'
  raw: string
  customerId?: string
}

interface InvoiceAssociateBody {
  lines: unknown[]
  association: { type: 'procurement' | 'supplier' | 'branch'; id: string }
  note?: string
}

Loyalty

interface LoyaltyLedgerEntry {
  id: string
  customerId: string
  customerName: string
  activity: string
  points: number             // signed: earn +, redeem −
  orderId?: string
  branch: string
  at: Timestamp
}

interface LoyaltyActivity {
  id: string
  code: string
  label: string
  points: number
  active: boolean
  description?: string
}

interface LoyaltyConfig {
  earnRules: LoyaltyActivity[]
  tiers: { name: string; thresholdPoints: number; multiplier: number }[]
  referralBonusPoints?: number
}

Marketing

type MarketingChannel = 'SMS' | 'Telegram' | 'Push' | 'Email'

interface MarketingSegment {
  id: string
  name: string
  criteria: string
  size: number
  channel: MarketingChannel
  status: 'active' | 'paused' | 'draft'
  engagementRate: number
  lastUsedAt: Timestamp | null
  owner: string
  createdAt: Timestamp
  refresh: 'live' | 'daily' | 'manual'
}

interface Campaign {
  id: string
  name: string
  segmentId: string
  channel: MarketingChannel
  status: string
  scheduledAt?: Timestamp
  sentAt?: Timestamp
  body_en?: string
  body_am?: string
}

FAQ

interface FaqArticle {
  id: string
  category: string
  title_en: string
  title_am: string
  body_en: string
  body_am: string
  published: boolean
  updatedAt: Timestamp
  views: number
}

8. Admin — finance, dispatch, imports & audit

Settlement & gateways

interface Settlement {
  id: string
  channel: string
  date: DateOnly
  volumeEtb: number
  status: 'reconciled' | 'pending' | 'matched' | 'unmatched' | 'refunded' | string
  txnCount: number
  feesEtb: number
}

interface PaymentGatewayConfig {
  id: string                 // chapa | arifpay | telebirr | mpesa
  label: string
  enabled: boolean
}

Org invoice (Admin view → B2B statements)

interface OrgInvoice {
  id: string
  period: string
  amountEtb: number
  status: string
  pdf_url?: string           // required for B2B Finance download
}

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: Timestamp
  completedAt: Timestamp | null
  status: 'delivered' | 'failed' | 'en_route' | 'returned'
  minutes: number
  onTime: boolean
  codEtb: number
}

type TripAssignBody = { riderId: string }

Platform MigrationJob (Admin imports)

Distinct from B2B tenant MigrationJob.

interface AdminMigrationJob {
  id: string
  templateId: 'stock' | 'catalog' | 'hr' | 'branches' | 'riders' | 'generic' | string
  templateLabel: string
  filename: string
  rowsTotal: number
  rowsImported: number
  rowsFailed: number
  status: 'completed' | 'partial' | 'failed' | 'running'
  runBy: string
  runAt: Timestamp
  durationSec: number
  mapping: Record<string, string>
}

AuditEvent

interface AuditEvent {
  id: string
  at: Timestamp
  actor: string
  actorRole?: StaffRole
  action: string
  target?: string
  category?: string
  before?: Record<string, unknown>
  after?: Record<string, unknown>
}

9. B2B portal schemas

Canonical narrative + samples: Gishen-B2B/docs/backend/entities/. Prefixes: org_ dept_ mbr_ pkg_ mig_ rx_ usr_ oreg_ orreq_.

Org registration

type RegistrationStatus = 'submitted' | 'under_review' | 'activated' | 'rejected'
type RequestChannel = 'email' | 'phone'
type RequestStatus = 'new' | 'contacted' | 'converted' | 'closed'

interface OrgRegistration {
  id: string                 // oreg_*
  org_id: string
  submitted_at: Timestamp
  company: {
    legal_name: string
    tin: string
    billing_contact: { name: string; email: string; phone: Phone }
    approx_headcount?: number
  }
  super_user: {
    full_name: string
    email: string
    phone: Phone
    // password only on write — never returned
  }
  status: RegistrationStatus
  admin_notes?: string       // Admin-only
}

interface OrgRegistrationRequest {
  id: string                 // orreq_*
  contact_name: string
  company_name?: string
  channel: RequestChannel
  email?: string
  phone?: Phone
  message?: string
  status: RequestStatus
  created_at: Timestamp
}

Endpoints: POST /v1/org/register, POST /v1/org/register/request.

Department

interface Department {
  id: string                 // dept_*
  org_id: string
  name: string
  code?: string
  parent_id?: string | null
  sub_limit?: Money | null
  sub_limit_used?: Money
  member_count?: number
  is_active: boolean
  created_at: Timestamp
  updated_at: Timestamp
  version: number
}

Member

type MemberStatus = 'invited' | 'active' | 'inactive' | 'offboarded'
type MemberType = 'primary' | 'dependant'

interface AllowanceSummary {
  period_start: DateOnly
  period_end: DateOnly
  allowance_total: Money
  allowance_used: Money
  allowance_remaining: Money
}

interface Member {
  id: string                 // mbr_*
  org_id: string
  customer_id?: string
  employee_id?: string
  full_name: string
  email?: string
  phone: Phone
  department_id?: string
  package_id?: string
  role: PortalRole           // portal access display
  member_type: MemberType
  primary_member_id?: string | null
  status: MemberStatus
  start_date?: DateOnly
  end_date?: DateOnly | null
  overrides?: {
    allowance_cap?: Money | null
    copay_percent?: number | null
    excluded_categories?: string[]
    included_perks?: string[]
  }
  verification_id?: string
  invite_id?: string
  allowance_summary?: AllowanceSummary
  prescription_count?: number
  created_at: Timestamp
  updated_at: Timestamp
  version: number
}

Package

type PackageStatus = 'draft' | 'active' | 'archived'

interface BenefitPackage {
  id: string                 // pkg_*
  org_id: string
  name: string
  code: string
  description?: string
  status: PackageStatus
  allowance: {
    amount: Money
    period: 'monthly' | 'quarterly' | 'annual'
    rollover: boolean
  }
  copay_percent: number
  categories: { category_code: string; coverage_percent: number; max_per_order?: Money | null }[]
  perks?: { code: string; label: string; description?: string }[]
  caps?: {
    max_order_amount?: Money
    max_orders_per_month?: number
    max_rx_fills_per_month?: number
  }
  exclusions?: { type: 'sku' | 'category'; ref: string; reason?: string }[]
  member_count?: number
  created_at: Timestamp
  updated_at: Timestamp
  version: number
}

Pending orgs: packages may be draft only until org.activated (default).


10. B2B finance, migration & Rx views

Finance

type ApprovalStatus = 'none' | 'flagged' | 'approved' | 'rejected'

interface SpendLine {
  id: string                 // spd_*
  org_id: string
  member_id: string
  order_id: string
  occurred_at: Timestamp
  amount_total: Money
  amount_org_covered: Money
  amount_member_paid: Money
  category_code: string      // NOT medicine name for HR/Finance
  department_id?: string
  description: string        // redacted
  approval_status: ApprovalStatus
}

interface Statement {
  id: string                 // stmt_*
  org_id: string
  period: string             // e.g. 2026-02
  opening_balance: Money
  charges: Money
  payments: Money
  closing_balance: Money
  credit_limit: Money
  pdf_url: string
  issued_at: Timestamp
}

interface CreditUsage {
  credit_limit: Money
  credit_used: Money
  credit_available: Money
  utilization_percent: number
  period_end: DateOnly
}

interface ApprovalFlag {
  id: string
  org_id: string
  spend_line_id: string
  member_id: string
  reason: 'threshold_exceeded' | 'unusual_category' | 'first_time_high_value' | 'manual_flag'
  threshold_rule?: string
  amount: Money
  status: ApprovalStatus
  reviewed_by?: string
  reviewed_at?: Timestamp
  notes?: string
}

Deep: B2B finance.md.

B2B MigrationJob (tenant)

type MigrationDatasetType =
  | 'departments' | 'packages' | 'members' | 'overrides'
  | 'dependants' | 'verification_ids' | 'full_onboarding_pack'

type B2BMigrationJobStatus =
  | 'uploaded' | 'mapping' | 'validating' | 'validated' | 'preview_ready'
  | 'committing' | 'completed' | 'failed' | 'rolled_back'

interface B2BMigrationJob {
  id: string                 // mig_*
  org_id: string
  created_by: string
  dataset_type: MigrationDatasetType
  status: B2BMigrationJobStatus
  file_name: string
  file_storage_key?: string
  column_mapping?: Record<string, string>
  mapping_profile_id?: string
  validation?: { total_rows: number; error_count: number; warning_count: number }
  commit_result?: { created: number; updated: number; skipped: number; failed: number }
  error_report_url?: string
  rollback_until?: Timestamp
  started_at?: Timestamp
  completed_at?: Timestamp
  created_at: Timestamp
  updated_at: Timestamp
}

B2B Prescription (member portal)

Same status enum as Admin. Extra clinical fields:

interface B2BPrescriptionPage {
  id: string
  page_number: number
  url: string                // signed — clinical withhold
  mime_type: string
  uploaded_at: Timestamp
}

// HR/Finance: only status + dates (+ prescription_count on Member)
// MEMBER / SUPER_USER: pages, medicine_lines, notes

11. Ecom / retail schemas

From Gishen-Ecom/docs/backend.md. Align Money to platform Money when implementing.

Catalog (retail read model)

interface CatalogProduct {
  id: string
  slug: string
  name: string
  sku?: string
  category?: string
  badge?: string
  // Mapped from MedicationItem storefront / PDP fields when platform catalog is live
  storefrontUnit?: string
  useCase?: string
  compareAtPriceEtb?: number
  sellingPriceEtb?: number
}

interface BranchStockAvailability {
  productId: string
  sku: string
  branches: {
    branchId: string
    name: string
    qty: number
    distanceKm?: number
    hours?: string
  }[]
}

Order create (B2C)

interface EcomOrderCreateBody {
  fulfillment: 'delivery' | 'pickup'
  branchId?: string
  location?: { label: string; lat: number | null; lng: number | null }
  items: { productId: string; qty: number }[]
  prescriptionFileKey?: string | null
  notes?: string
  paymentPreference?: 'cod' | 'telebirr' | 'bank'
  referralCode?: string | null
  credit?: {
    organisationId: string | null
    coveredAmount: number       // migrate → Money
    selfPayAmount: number
    planId: string | null
  }
  splitHint?: 'auto' | 'single_branch' | 'allow_split'
}

Entitlement / checkout quote

/** POST /checkout/credit/quote or POST /v1/checkout/entitlement-preview */
interface EntitlementQuote {
  basket_total: Money
  org_covered: Money
  member_pays: Money
  organisation_id?: string
  plan_id?: string
  category_blocks?: string[]
  requires_approval?: boolean
}

Identity document (KYC)

type IdentityDocType = 'national_id' | 'passport'
type IdentityStatus = 'pending' | 'verified' | 'rejected'

interface IdentityDocument {
  id: string
  customer_id: string
  doc_type: IdentityDocType
  doc_number: string
  issuing_country: string
  file_key?: string
  status: IdentityStatus
  reviewed_at?: Timestamp
  notes?: string
}

Admin review: GET /admin/identity/queue, POST /admin/identity/:id/review.

Payments

interface PaymentIntent {
  id: string
  order_id: string
  provider: 'telebirr' | 'chapa' | 'bank' | string
  amount: Money
  status: string
  checkout_url?: string
}

Telegram Mini App

// POST /telegram/auth — validate initData → session
// GET /telegram/bootstrap — catalog lite + referral + community directory

12. Backend ownership & path prefixes

Surface Typical prefix Owns schemas
Shared platform API /v1 or / (TBD host) This pack
Admin staff /auth/staff/*, /admin/*, /pharmacy/*, /dispatch/*, /pos/* §3–8 + Admin sheet
Doctor /auth/doctor/* Doctor + Rx/Order create
B2B portal /v1/org/*, /v1/members/*, /v1/finance/*, /v1/prescriptions/* §9–10 + B2B entities
Ecom / Mob retail /catalog/*, /orders, /loyalty/*, /prescriptions, /payments/* §11 + Ecom sheet
Integrations /integrations/erp/*, /dispatch/bot/webhook Stock sync, rider bot

Clinical withhold (backend rule)

Audience Prescription / clinical
Admin pharmacist Full
B2B MEMBER (own), SUPER_USER Full
B2B HR_ADMIN / FINANCE Status/dates/aggregates only — never pages, medicine lines, diagnosis
Event bus prescription.submitted No image URLs or clinical content

Samples

Organisation activated (event payload)

{
  "type": "org.activated",
  "org_id": "org_01HNEW",
  "payload": {
    "status": "active",
    "commercial": {
      "credit_limit": { "amount": "2000000.00", "currency": "ETB" },
      "price_list_id": "pl_corporate_2026",
      "contract_start": "2026-04-01",
      "contract_end": "2027-03-31"
    }
  }
}

Entitlement split (checkout)

{
  "basket_total": { "amount": "1200.00", "currency": "ETB" },
  "org_covered": { "amount": "900.00", "currency": "ETB" },
  "member_pays": { "amount": "300.00", "currency": "ETB" }
}

Doc Role
admin-backend-spec.md Admin endpoints / modules
GISHEN-MASTER-SPEC.md Surfaces, divergences, schema index
B2B docs/backend/OVERVIEW.md Tenancy, RBAC, errors
B2B docs/backend/entities/* Portal entity depth
B2B docs/backend/events/README.md Event samples
Ecom docs/backend.md Retail API priority + stubs
Email templates design Props for transactional emails (not domain entities)