Document portal auth, profile, and pharmacy ID contracts in backend specs.

Keep the living API sheet aligned with demo personas, avatars, ID-card QR scan, list filters, and Admin/Ecom coordination.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
kirukib 2026-08-06 22:10:07 +03:00
parent a522916ff1
commit 8fe6d58a09
11 changed files with 209 additions and 37 deletions

View File

@ -28,12 +28,14 @@ For single-org sessions, `orgId` is implicit from JWT/session. Multi-org users (
| Mode (this phase) | Description |
|-------------------|-------------|
| Mock demo | `/login` role selector sets session cookie with role + org + user |
| Mock demo | `/login` **test-user dropdown** (`demo_user_id`) sets session cookie with user + roles + org; legacy `demo_role` still accepted |
| Production (future) | OAuth2 / magic link / SSO — TBD with platform auth |
Demo personas: `user_super`, `user_hr`, `user_fin`, `user_mem` — see [`endpoints/auth.md`](endpoints/auth.md) (`GET /v1/auth/demo-profiles`).
### Session shape
See [`entities/session-user.md`](entities/session-user.md).
See [`entities/session-user.md`](entities/session-user.md). Includes `avatar_url`, `roles[]`, and `active_role` for multi-role demo users.
### JWT claims (future)
@ -42,6 +44,7 @@ See [`entities/session-user.md`](entities/session-user.md).
"sub": "usr_01H...",
"org_id": "org_01H...",
"roles": ["HR_ADMIN", "FINANCE"],
"active_role": "HR_ADMIN",
"locale": "am",
"iat": 1710000000,
"exp": 1710003600
@ -52,10 +55,12 @@ See [`entities/session-user.md`](entities/session-user.md).
| Resource / action | SUPER_USER | HR_ADMIN | FINANCE | MEMBER |
|-------------------|:----------:|:--------:|:-------:|:------:|
| Own profile (`/v1/me/profile`) | ✓ | ✓ | ✓ | ✓ |
| Org profile (commercial read) | ✓ | ✓ (active only) | ✓ (active only) | — |
| Org registration submit | ✓ (public path) | — | — | — |
| Departments CRUD | ✓ | ✓ | read | — |
| Members CRUD / import / invite | ✓ | ✓ | read metadata | own row |
| Pharmacy ID card (print / QR payload) | ✓ | ✓ | — | own only |
| Packages CRUD | ✓ | ✓ | read | own assignment |
| Migration jobs | ✓ | ✓ | read history | — |
| Finance spend / statements | ✓ | read aggregates | ✓ | — |
@ -138,7 +143,15 @@ Default `page_size`: 25. Max: 100.
## Filtering & date ranges
Finance and spend endpoints accept:
List endpoints support search + filters (portal toolbar / modal when >3 filters):
| Domain | Query params (common) |
|--------|------------------------|
| Members | `q`, `status`, `department_id`, `package_id`, `member_type`, `has_dependants` |
| Finance spend | `from`, `to`, `department_id`, `category`, `coverage`, `member_id`, `approval_status` |
| Statements | `year`, `status` |
| Migration | `status`, `dataset_type`, `has_errors`, `created_by` |
| Packages / departments | `q`, `status` / site |
```
?from=2026-01-01&to=2026-01-31&department_id=dept_01H...
@ -146,6 +159,24 @@ Finance and spend endpoints accept:
ISO 8601 dates in org timezone (default `Africa/Addis_Ababa`).
## Pharmacy ID card (platform contract)
Members carry a printable verification card used at Gishen pharmacy / Ecom checkout:
| Field | Purpose |
|-------|---------|
| `avatar_url` | Photo on card + portal UI |
| `card_id` | Human-readable card number (e.g. `GSH-1042`) |
| `verification_id` | Org verification token encoded in QR |
**QR payload (canonical):**
```
gishen://member/{member_id}?v={verification_id}
```
Admin / POS scans the QR to resolve the member for covered purchase. Clinical detail is **not** on the card. See [`endpoints/members.md`](endpoints/members.md#get-v1-membersidid-card) and [`../coordination/gishen-admin.md`](../coordination/gishen-admin.md).
## File uploads
Multipart for Rx images and migration files:

View File

@ -8,9 +8,9 @@ REST-style API contract for Gishen-B2B. All paths prefixed with `/v1` unless not
| Group | Doc | Description |
|-------|-----|-------------|
| Auth | [auth.md](auth.md) | Login, session, locale |
| Auth | [auth.md](auth.md) | Demo login personas, session, locale, `/me/profile` |
| Organisation | [org.md](org.md) | Org profile, registration, departments, verification |
| Members | [members.md](members.md) | CRUD, import, invites, join |
| Members | [members.md](members.md) | CRUD, import, invites, join, pharmacy ID card |
| Packages | [packages.md](packages.md) | Benefit plans |
| Migration | [migration.md](migration.md) | Bulk import jobs |
| Finance | [finance.md](finance.md) | Spend, statements, approvals |
@ -53,6 +53,7 @@ Endpoints returning prescription or order clinical detail check session roles. H
| Path pattern | Auth |
|--------------|------|
| `/v1/auth/login` | Public |
| `/v1/auth/demo-profiles` | Public |
| `/v1/org/register*` | Public |
| `/v1/invite/:token` (GET) | Public |
| `/v1/join` (POST) | Public or member session |

View File

@ -38,12 +38,14 @@ Production (future): `{ "email", "password" }` or OAuth code exchange.
"data": {
"token": "mock_jwt_...",
"user": {
"id": "usr_01HHR",
"email": "hr.demo@acme.et",
"full_name": "Demo HR Admin",
"org_id": "org_01DEMO",
"id": "user_hr",
"email": "hr@yaltopia.com",
"full_name": "Hanna HR",
"org_id": "org_yaltopia",
"roles": ["HR_ADMIN"],
"active_role": "HR_ADMIN",
"locale": "en",
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_hr",
"org_status": "active",
"permissions": ["org:read", "org:write_hr", "members:write", "packages:write", "migration:write", "finance:read"]
}
@ -55,7 +57,8 @@ Production (future): `{ "email", "password" }` or OAuth code exchange.
| Code | HTTP | When |
|------|------|------|
| `VALIDATION_FAILED` | 400 | Invalid `demo_role` |
| `VALIDATION_FAILED` | 400 | Invalid `demo_user_id` / `demo_role` |
| `NOT_FOUND` | 404 | Unknown demo persona |
| `UNAUTHENTICATED` | 401 | Production bad credentials |
### Clinical withhold
@ -166,24 +169,38 @@ List pre-seeded demo personas for `/login` test-user dropdown.
"full_name": "Selam Super",
"demo_role": "SUPER_USER",
"roles": ["SUPER_USER", "HR_ADMIN", "FINANCE", "MEMBER"],
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_super",
"label": "Super User",
"description": "Full portal access including clinical detail"
},
{
"id": "user_hr",
"email": "hr@yaltopia.com",
"full_name": "Hanna HR",
"demo_role": "HR_ADMIN",
"roles": ["HR_ADMIN"],
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_hr",
"label": "HR Administrator",
"description": "Members, departments, packages — no clinical detail"
},
{
"id": "user_fin",
"email": "finance@yaltopia.com",
"full_name": "Fikru Finance",
"demo_role": "FINANCE",
"roles": ["FINANCE"],
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_fin",
"label": "Finance Approver",
"description": "Spend, statements, approvals — no clinical detail"
},
{
"id": "user_mem",
"email": "member@yaltopia.com",
"full_name": "Abebe Member",
"demo_role": "MEMBER",
"roles": ["MEMBER"],
"member_id": "mbr_01HMEM001",
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_mem",
"label": "Member",
"description": "Own allowance, orders, prescriptions"
}
@ -217,7 +234,9 @@ Current user’s profile (account page).
"roles": ["HR_ADMIN"],
"active_role": "HR_ADMIN",
"locale": "en",
"avatar_initials": "HH"
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_hr",
"avatar_initials": "HH",
"member_id": null
}
}
```
@ -226,7 +245,7 @@ Current user’s profile (account page).
## PATCH /v1/me/profile
Update display preferences (locale / name stub).
Update display preferences (locale / name stub / active role for multi-role users).
| | |
|---|---|
@ -236,22 +255,33 @@ Update display preferences (locale / name stub).
### Request
```json
{ "locale": "am" }
{
"locale": "am",
"active_role": "SUPER_USER"
}
```
`active_role` must be one of the user’s `roles[]` (demo: `user_super` holds all four).
### Response 200
Updated profile object.
### Errors
| Code | HTTP | When |
|------|------|------|
| `VALIDATION_FAILED` | 400 | Invalid locale or role not in `roles[]` |
---
## UI routes
| Route | Purpose |
|-------|---------|
| `/login` | Demo test-user dropdown + locale switcher |
| `/profile` | Own profile — role switch, sign out |
| `/login` | Yimaru-style login — test-user dropdown + locale switcher |
| `/profile` | Own profile — avatar, org, role switch, sign out |
## Clinical withhold notes
Auth endpoints do not expose clinical data. Role list on session determines withhold behavior on downstream endpoints.
Auth endpoints do not expose clinical data. Role list / `active_role` on session determines withhold behavior on downstream endpoints.

View File

@ -42,7 +42,17 @@ Paginated spend lines.
### Query
`?page=1&member_id=mbr_01H&department_id=dept_01H&approval_status=flagged`
`?page=1&member_id=mbr_01H&department_id=dept_01H&category=chronic&coverage=partial&period=2026-03&approval_status=flagged`
| Param | Notes |
|-------|--------|
| `from` / `to` | ISO date range |
| `department_id` | Site filter |
| `category` | Category code (no medicine names) |
| `coverage` | e.g. `full` \| `partial` \| `member_paid` (mock) |
| `period` | `YYYY-MM` convenience filter |
| `member_id` | Single member |
| `approval_status` | `flagged` \| `approved` \| … |
### Response 200

View File

@ -19,7 +19,18 @@ List members.
### Query
`?page=1&status=active&department_id=dept_01H&q=abel&member_type=primary`
`?page=1&status=active&department_id=dept_01H&package_id=pkg_01HGOLD&q=abel&member_type=primary&has_dependants=true`
| Param | Notes |
|-------|--------|
| `q` | Search name, email, phone, employee_id |
| `status` | `invited` \| `active` \| `inactive` \| `offboarded` |
| `department_id` | Filter by department |
| `package_id` | Filter by assigned package |
| `member_type` | `primary` \| `dependant` |
| `has_dependants` | `true` \| `false` — primaries with/without dependants |
Portal UI: when more than three filters are shown, they open in an Apply modal (client-only); API still accepts all query params above.
### Response 200
@ -91,6 +102,59 @@ Member detail.
HR/Finance: no linked prescriptions. SUPER_USER: may include `prescriptions_summary`. MEMBER own: full allowance, order pointers.
Detail payload includes `avatar_url`, `card_id`, `verification_id` for pharmacy ID card (safe for HR).
---
## GET /v1/members/:id/id-card
Printable pharmacy verification card payload (B2B print UI + Admin/POS scan).
| | |
|---|---|
| **Auth** | Required |
| **Roles** | SUPER_USER, HR_ADMIN; MEMBER (own `member_id` only) |
### Response 200
```json
{
"data": {
"member_id": "mbr_01HMEM001",
"full_name": "Abel Mekonnen",
"employee_id": "EMP-1042",
"card_id": "GSH-1042",
"verification_id": "EMP-1042",
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=mbr_01HMEM001",
"org_name": "Acme Bank",
"org_join_code": "ACME-2026",
"package_name": "Gold Executive",
"status": "active",
"qr_payload": "gishen://member/mbr_01HMEM001?v=EMP-1042"
}
}
```
### QR / scan contract
| | |
|---|---|
| **Format** | `gishen://member/{member_id}?v={verification_id}` |
| **Consumer** | Gishen-Admin POS / pharmacist checkout — resolve covered member |
| **Not included** | Prescription images, medicine lines, diagnosis |
### Errors
| Code | HTTP |
|------|------|
| `FORBIDDEN` | 403 |
| `NOT_FOUND` | 404 |
| `MEMBER_INACTIVE` | 422 — offboarded / inactive cards may still print with watermark (product TBD) |
### Clinical withhold
ID card is verification metadata only — allowed for HR. No clinical fields.
---
## PATCH /v1/members/:id
@ -108,10 +172,13 @@ Update member.
{
"department_id": "dept_01HNEW",
"package_id": "pkg_01HSILVER",
"avatar_url": "https://cdn.gishen.../avatars/mbr_01H.png",
"overrides": { "copay_percent": 5 }
}
```
`avatar_url` may also be set via future `POST /v1/members/:id/avatar` (multipart). Mock phase uses deterministic portrait URLs.
---
## POST /v1/members/:id/offboard
@ -297,10 +364,10 @@ Add dependant.
| Route | Endpoint(s) |
|-------|-------------|
| `/members` | GET `/v1/members` |
| `/members` | GET `/v1/members` (search + filters; modal when >3) |
| `/members/new` | POST `/v1/members` |
| `/members/import` | POST `/v1/members/import` |
| `/members/[id]` | GET/PATCH + invite/offboard/status mock actions |
| `/members/[id]` | GET/PATCH + invite/offboard; **ID card** tab → GET `/v1/members/:id/id-card` |
| `/invite/[token]` | GET/POST invite |
| `/join` | POST `/v1/join` |

View File

@ -11,7 +11,7 @@ Data model entities required by Gishen-B2B. Types below are canonical for mocks
| Organisation | [organisation.md](organisation.md) | All roles (scoped) |
| Org registration | [org-registration.md](org-registration.md) | Public signup, Admin |
| Department | [department.md](department.md) | HR, Finance (aggregates) |
| Member | [member.md](member.md) | HR, Member; Finance (metadata only) |
| Member | [member.md](member.md) | HR, Member; Finance (metadata); pharmacy ID card (`avatar_url`, `card_id`, `verification_id`) |
| Package | [package.md](package.md) | HR, Member |
| Migration job | [migration-job.md](migration-job.md) | HR, SUPER_USER |
| Finance | [finance.md](finance.md) | Finance, HR (read), SUPER_USER |
@ -32,6 +32,8 @@ usr_01HXXXXXXXXXXXXXX
ULID-style prefixes for human readability in logs.
Printed pharmacy card numbers use org-scoped display IDs (e.g. `GSH-1042`) on `Member.card_id` — not a separate entity.
## Common field types
| Type | Format |

View File

@ -147,4 +147,5 @@ SUPER_USER and MEMBER (own record) receive full clinical linkage via prescriptio
## Related endpoints
- [`../endpoints/members.md`](../endpoints/members.md)
- [`../endpoints/members.md`](../endpoints/members.md) — including `GET /v1/members/:id/id-card`
- Pharmacy QR scan consumed by Admin / POS — see [`../../coordination/gishen-admin.md`](../../coordination/gishen-admin.md)

View File

@ -8,7 +8,7 @@ Authenticated user context for B2B portal — demo mock now, JWT/session later.
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `id` | `string` | ✓ | `usr_*` |
| `id` | `string` | ✓ | `usr_*` or demo `user_*` |
| `email` | `string` | ✓ | |
| `full_name` | `string` | ✓ | |
| `phone` | `Phone` | | |
@ -16,10 +16,11 @@ Authenticated user context for B2B portal — demo mock now, JWT/session later.
| `member_id` | `string` | | Link to Member row if enrolled |
| `customer_id` | `string` | | Shared Gishen customer (Ecom/Mob) |
| `roles` | `PortalRole[]` | ✓ | One or more |
| `active_role` | `PortalRole` | ✓ | Role used for nav + withhold (must ∈ `roles`) |
| `locale` | `Locale` | ✓ | `en` \| `am` |
| `avatar_url` | `string` | | |
| `avatar_url` | `string` | | Portrait URL (demo: deterministic DiceBear) |
| `org_status` | `OrgStatus` | ✓ | Denormalized for gating |
| `permissions` | `string[]` | | Computed from roles |
| `permissions` | `string[]` | | Computed from `active_role` / roles |
| `demo_profile` | `DemoProfile` | | Mock only |
### PortalRole
@ -35,7 +36,7 @@ SUPER_USER | HR_ADMIN | FINANCE | MEMBER
| `label` | `string` |
| `description` | `string` |
Pre-seeded demo profiles per role against a shared mock org.
Pre-seeded demo personas: `user_super`, `user_hr`, `user_fin`, `user_mem` against a shared mock org. Listed via `GET /v1/auth/demo-profiles`.
## Computed permissions
@ -50,30 +51,37 @@ Pre-seeded demo profiles per role against a shared mock org.
| `finance:approve` | SUPER_USER, FINANCE |
| `clinical:read` | SUPER_USER, MEMBER (own) |
| `prescriptions:write` | MEMBER (own), SUPER_USER |
| `id_card:print` | SUPER_USER, HR_ADMIN, MEMBER (own) |
## Locale preference
`locale` is persisted on the user record and returned in session. Updated via `PATCH /v1/auth/me/locale`. Synced to client cookie/localStorage for next-intl.
`locale` is persisted on the user record and returned in session. Updated via `PATCH /v1/auth/me/locale` or `PATCH /v1/me/profile`. Synced to client cookie/localStorage for next-intl.
## Active role
Multi-role users (demo Super User) can set `active_role` on `/profile`. Portal nav and clinical withhold follow `active_role` for the session.
## Clinical withhold
Session drives RBAC. If `roles` includes HR_ADMIN or FINANCE and **not** SUPER_USER, all prescription and clinical serializers apply withhold filters automatically.
Session drives RBAC. If effective role is HR_ADMIN or FINANCE and **not** SUPER_USER, all prescription and clinical serializers apply withhold filters automatically.
## Sample session (mock demo — HR)
```json
{
"id": "usr_01HHR",
"email": "hr.demo@acme.et",
"full_name": "Demo HR Admin",
"id": "user_hr",
"email": "hr@yaltopia.com",
"full_name": "Hanna HR",
"phone": "+251900000001",
"org_id": "org_01DEMO",
"org_id": "org_yaltopia",
"member_id": null,
"customer_id": null,
"roles": ["HR_ADMIN"],
"active_role": "HR_ADMIN",
"locale": "en",
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_hr",
"org_status": "active",
"permissions": ["org:read", "org:write_hr", "members:write", "packages:write", "migration:write", "finance:read"],
"permissions": ["org:read", "org:write_hr", "members:write", "packages:write", "migration:write", "finance:read", "id_card:print"],
"demo_profile": {
"label": "HR Administrator",
"description": "Manage members, departments, and packages"
@ -85,9 +93,13 @@ Session drives RBAC. If `roles` includes HR_ADMIN or FINANCE and **not** SUPER_U
```json
{
"id": "usr_01HSUPER",
"roles": ["SUPER_USER"],
"id": "user_super",
"email": "super@yaltopia.com",
"full_name": "Selam Super",
"roles": ["SUPER_USER", "HR_ADMIN", "FINANCE", "MEMBER"],
"active_role": "SUPER_USER",
"locale": "am",
"avatar_url": "https://api.dicebear.com/9.x/lorelei/svg?seed=user_super",
"permissions": ["*"],
"demo_profile": {
"label": "Super User",
@ -98,4 +110,4 @@ Session drives RBAC. If `roles` includes HR_ADMIN or FINANCE and **not** SUPER_U
## Related endpoints
- [`../endpoints/auth.md`](../endpoints/auth.md)
- [`../endpoints/auth.md`](../endpoints/auth.md) — login, demo-profiles, `/me/profile`

View File

@ -12,6 +12,7 @@ How Gishen-B2B interacts with **Gishen-Admin** (pharmacy ops, commercial activat
| Org creation (legacy) | Offline sales creates org + invites SUPER_USER | B2B receives invite link |
| Registration queue | Review `org.registration_submitted` / `org.registration_requested` | Admin contacts client, completes setup, activates |
| Pharmacist Rx queue | Review submitted prescriptions | B2B member sees status updates via `prescription.review_updated` |
| **Pharmacy ID scan** | POS / pharmacist scans member QR at purchase | Resolve `member_id` + `verification_id` from `gishen://member/{id}?v={verification_id}`; apply org package entitlement at checkout |
| Credit / invoice oversight | Monthly billing, suspensions | B2B Finance sees statements; `org.suspended` blocks new orders |
| Price lists | Corporate pricing | Referenced by `organisation.commercial.price_list_id` — read-only in B2B |
@ -49,8 +50,22 @@ How Gishen-B2B interacts with **Gishen-Admin** (pharmacy ops, commercial activat
| `Organisation.status` | Admin | All authenticated |
| `Prescription` review fields | Admin | Member, SUPER_USER |
| `Member`, `Department`, `Package` | B2B (HR) | B2B |
| `Member.verification_id` / `card_id` / `avatar_url` | B2B (HR) | B2B portal + Admin POS scan |
| `Statement` | Admin/billing engine | B2B Finance |
## Pharmacy ID card (scan contract)
B2B prints employee pharmacy ID cards from `GET /v1/members/:id/id-card` (UI: `/members/[id]` → ID card tab).
| Item | Spec |
|------|------|
| QR payload | `gishen://member/{member_id}?v={verification_id}` |
| Card fields | Photo, name, employee ID, `card_id`, package, status, org |
| Admin responsibility | Implement scanner → resolve member → covered checkout (with Ecom entitlement) |
| Clinical | Card has **no** Rx or medicine data |
See [`../backend/endpoints/members.md`](../backend/endpoints/members.md) and [`../backend/OVERVIEW.md`](../backend/OVERVIEW.md#pharmacy-id-card-platform-contract).
## Clinical withhold alignment
Admin pharmacists see full Rx detail. B2B HR/Finance never receive clinical payloads — enforced in B2B API layer and documented in entity/endpoint specs.
@ -64,6 +79,7 @@ SUPER_USER in B2B may view member Rx for support; does not replace Admin review
- [ ] Pharmacist queue consumes `prescription.submitted`
- [ ] Admin review emits `prescription.review_updated`
- [ ] Statement PDF generation exposes URLs consumed by B2B `/statements`
- [ ] POS / Admin scanner accepts `gishen://member/{id}?v={verification_id}` and resolves covered member
## Contact / escalation

View File

@ -80,6 +80,7 @@ Mock phase: static link + shared demo customer.
| `GET /v1/customers/:id/allowance` | Ecom checkout, B2B `/me` |
| `POST /v1/checkout/entitlement-preview` | Ecom cart |
| `GET /v1/customers/:id/orders` | B2B `/me/orders` (member view) |
| Resolve pharmacy QR `gishen://member/{id}?v={verification_id}` | Admin POS / Ecom staff checkout → same entitlement preview |
Document exact shapes in Ecom spec sheet; B2B references via this coordination doc.
@ -94,3 +95,4 @@ Telegram Mini App rides **Ecom's backend spec**, not B2B. No B2B action required
- [ ] Checkout split UI implemented once in Ecom
- [ ] Order events feed B2B finance aggregates (redacted)
- [ ] Confirm SSO/deep-link from B2B member portal to Ecom
- [ ] Staff / pharmacist flow accepts B2B pharmacy ID QR at checkout

View File

@ -99,7 +99,7 @@ All primary tables include in-card **search + filter** toolbar (`TableToolbar`):
## Detail UX
Yimaru-style `DetailHero` + `DetailSection` layouts with demo mutations (invite/offboard, package activate, migration commit/rollback, statement issue/pay, approval approve/flag). Dashboard uses Recharts placeholders from statements/spend/departments/packages.
Yimaru-style `DetailHero` + `DetailSection` layouts with demo mutations (invite/offboard, package activate, migration commit/rollback, statement issue/pay, approval approve/flag). Dashboard uses Recharts placeholders from statements/spend/departments/packages. Member detail includes printable pharmacy ID card (`GET /v1/members/:id/id-card`).
## Spec file paths
```