Keep entity schemas aligned with Admin Rx flagging after merging remote schema pack. Co-authored-by: Cursor <cursoragent@cursor.com>
29 KiB
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
}
/** Staff review markers — distinct from line `controlled` and workflow status. */
type PrescriptionFlagCode =
| 'controlled_substance'
| 'interaction_concern'
| 'incomplete_rx'
| 'fraud_suspicion'
| 'stock_shortage'
| 'needs_clarification'
| 'allergy_concern'
| 'dosing_concern'
| 'other'
type PrescriptionFlagSeverity = 'info' | 'warning' | 'critical'
type PrescriptionFlagStatus = 'open' | 'resolved'
interface PrescriptionFlag {
id: string
code: PrescriptionFlagCode
severity: PrescriptionFlagSeverity
note: string
createdBy: string
createdAt: Timestamp
status: PrescriptionFlagStatus
resolvedBy?: string
resolvedAt?: Timestamp
resolutionNote?: 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
flags?: PrescriptionFlag[]
}
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" }
}
Related docs
| 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) |