Keep entity schemas aligned with Admin Rx flagging after merging remote schema pack. Co-authored-by: Cursor <cursoragent@cursor.com>
1258 lines
29 KiB
Markdown
1258 lines
29 KiB
Markdown
# 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`](./admin-backend-spec.md) |
|
||
| **Cross-app map** | [`GISHEN-MASTER-SPEC.md`](./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](#1-shared-primitives) |
|
||
| API envelopes + DomainEvent | [§2](#2-api-envelopes--domain-events) |
|
||
| Principal actors (Staff / Doctor / B2B session / Customer) | [§3](#3-principals--actors) |
|
||
|
||
### Admin / ops backend
|
||
|
||
| Schema | Section |
|
||
| --- | --- |
|
||
| Branch · Organisation · CommercialTerms · Doctor | [§4](#4-admin--organisations--doctors) |
|
||
| Prescription · Order | [§5](#5-admin--prescriptions--orders) |
|
||
| MedicationItem · Stock · Procurement | [§6](#6-admin--catalogue-stock--procurement) |
|
||
| POS · Customer · Loyalty · Marketing · FAQ | [§7](#7-admin--pos-crm-loyalty--marketing) |
|
||
| Settlement · Dispatch · Migration (platform) · Audit | [§8](#8-admin--finance-dispatch-imports--audit) |
|
||
|
||
### B2B portal backend
|
||
|
||
| Schema | Section |
|
||
| --- | --- |
|
||
| SessionUser · Org registration · Department · Member · Package | [§9](#9-b2b-portal-schemas) |
|
||
| B2B Finance · B2B Migration · B2B Prescription view | [§10](#10-b2b-finance-migration--rx-views) |
|
||
|
||
### Ecom / retail backend
|
||
|
||
| Schema | Section |
|
||
| --- | --- |
|
||
| Catalog product · Cart/Order create · Entitlement · Payments · Identity | [§11](#11-ecom--retail-schemas) |
|
||
|
||
### Ownership
|
||
|
||
| Map | [§12](#12-backend-ownership--path-prefixes) |
|
||
|
||
---
|
||
|
||
## 1. Shared primitives
|
||
|
||
```ts
|
||
/** 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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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)
|
||
|
||
```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?: Locale
|
||
avatarUrl?: string
|
||
authProvider?: AuthProvider
|
||
phone?: string
|
||
}
|
||
```
|
||
|
||
Login response: `{ accessToken, refreshToken, user: StaffUser }`.
|
||
|
||
### Doctor (external clinical — not StaffRole)
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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`](../../Gishen-B2B/docs/backend/entities/session-user.md).
|
||
|
||
### Retail Customer (CRM / loyalty)
|
||
|
||
```ts
|
||
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
|
||
|
||
```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'
|
||
/** + 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`](../../Gishen-B2B/docs/backend/entities/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
|
||
|
||
```ts
|
||
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`](../../Gishen-B2B/docs/backend/entities/prescription.md).
|
||
|
||
### Order
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```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
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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)
|
||
|
||
```ts
|
||
interface OrgInvoice {
|
||
id: string
|
||
period: string
|
||
amountEtb: number
|
||
status: string
|
||
pdf_url?: string // required for B2B Finance download
|
||
}
|
||
```
|
||
|
||
### 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: 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](#10-b2b-finance-migration--rx-views).
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
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`](../../Gishen-B2B/docs/backend/entities/finance.md).
|
||
|
||
### B2B MigrationJob (tenant)
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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)
|
||
|
||
```ts
|
||
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)
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
/** 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)
|
||
|
||
```ts
|
||
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
|
||
|
||
```ts
|
||
interface PaymentIntent {
|
||
id: string
|
||
order_id: string
|
||
provider: 'telebirr' | 'chapa' | 'bank' | string
|
||
amount: Money
|
||
status: string
|
||
checkout_url?: string
|
||
}
|
||
```
|
||
|
||
### Telegram Mini App
|
||
|
||
```ts
|
||
// 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)
|
||
|
||
```json
|
||
{
|
||
"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)
|
||
|
||
```json
|
||
{
|
||
"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-backend-spec.md) | Admin endpoints / modules |
|
||
| [`GISHEN-MASTER-SPEC.md`](./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) |
|