App routes
Mounted at /app. App routes use JWT audience snr_usr (set on the server). Same Authorization header pattern as admin (encrypted token), with a user-scoped JWT.
POST /app/how
Section titled “POST /app/how”Returns static bilingual “how it works” copy. Language is selected by the lang header:
lang | Language |
|---|---|
0, missing, or other numeric (except 1) | English |
1 | Arabic |
en / ar (case-insensitive) | English / Arabic |
Headers
Section titled “Headers”| Header | Required | Notes |
|---|---|---|
Authorization | yes | Encrypted token (hex ciphertext) |
lang | no | 0 / 1 or en / ar |
Content-Type | no | Body is ignored; application/json is fine |
Response (data)
Section titled “Response (data)”| Field | Type | Notes |
|---|---|---|
lang | 0 | 1 | Echo of effective language used |
title | string | Screen title |
subtitle | string | Short intro |
steps | array | Ordered steps: seq, title, icon |
cta.submit_now | string | Primary CTA label |
Example (200)
Section titled “Example (200)”{ "status": 1000, "message": "ok", "data": { "lang": 0, "title": "How second opinion works", "subtitle": "A few simple steps to get an expert medical review of your case.", "steps": [ { "seq": 1, "title": "Share your medical documents and details securely.", "icon": "document" }, { "seq": 2, "title": "Our coordinators match you with the right specialists.", "icon": "users" }, { "seq": 3, "title": "Expert doctors review your case and records.", "icon": "stethoscope" }, { "seq": 4, "title": "Receive your second opinion summary and next steps.", "icon": "check_circle" } ], "cta": { "submit_now": "Submit your case now" } }}POST /app/list-reviewtypes
Section titled “POST /app/list-reviewtypes”Returns localized screen copy plus all active review types for the picker UI (not paginated; capped at 100 rows). Same lang header rules as POST /app/how. Requires D1 migration that adds review_types.icon (see migrations/0004_review_types_add_icon.sql).
Request body may be {}. Headers: Authorization (required); lang selects English vs Arabic strings for title, subtitle, and each item’s title, description, and duration.
Response — data shape
Section titled “Response — data shape”| Field | Type | Notes |
|---|---|---|
lang | 0 | 1 | Effective language |
title | string | Screen title |
subtitle | string | Screen subtitle |
items | array | Active Status rows only, newest first |
Each element of items:
| Field | Type | Notes |
|---|---|---|
rt_id | string | review_type_id |
title | string | Localized name |
description | string | Localized description |
duration | string | Localized duration from min/max review days |
doctor_count | number | ReviewedDoctorsCount |
price | number | Same as admin |
icon | string | From DB, or "document" if unset |
Example (200)
Section titled “Example (200)”{ "status": 1000, "message": "ok", "data": { "lang": 0, "title": "Select review type", "subtitle": "Get expert medical opinion from our panel", "items": [ { "rt_id": "550e8400-e29b-41d4-a716-446655440000", "title": "Standard Review", "description": "Reviewed by 3 specialist oncologists", "doctor_count": 3, "duration": "8 - 12 days", "price": 900, "icon": "document" } ] }}POST /app/list-diseases
Section titled “POST /app/list-diseases”Lists active disease categories for pickers (e.g. “Select Disease Category”). Uses the same lang header rules as POST /app/how (0 / en → English speciality label, 1 / ar → Arabic).
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
q | string | no | Optional filter on English / Arabic specialty (similar to admin list) |
Response — data
Section titled “Response — data”| Field | Type | Notes |
|---|---|---|
lang | 0 | 1 | Effective language |
items | array | Each element: { "key", "value" } — key = disease id (dc_id), value = localized speciality label |
Up to 300 active rows.
Example (200)
Section titled “Example (200)”{ "status": 1000, "message": "ok", "data": { "lang": 1, "items": [ { "key": "501df25c-1cff-4e46-951c-6be32ed0159b", "value": "أمراض القلب" } ] }}Review cases (review_cases)
Section titled “Review cases (review_cases)”Stored in D1 table review_cases (see migrations/0005_create_review_cases.sql; rejected / reject_reason: 0007_review_cases_reject.sql; case_coordinator_id: 0008_review_case_coordinator_second_opinions.sql). user_id is taken from the JWT (AuthUser.id); do not send it in the body.
| Column / field | Notes |
|---|---|
case_id | UUID primary key (returned on create) |
rt_id | Active review type (review_types.review_type_id) |
dc_id | Active disease category (disease_categories.dc_id) |
patient_age | Integer 0–150 |
patient_gender | male | female |
status | draft | review | rejected (review = submitted for processing; rejected set by admin) |
medical_history | Optional string |
medical_documents | JSON array of objects (e.g. { "url", "key", "name" }), max 50 items |
reject_reason | Set when status is rejected; otherwise null |
case_coordinator_id | Provider / employee id of assigned coordinator; set by POST /admin/assign-coordinator; otherwise null |
is_notified | 0 until a 24-hour draft reminder is sent, then 1 (migration 0035_review_cases_is_notified.sql) |
Table second_opinions (migrations 0008 / 0009 / 0010): assignment history — sop_id, case_id, doctor_id, requested_documents (JSON array of strings), created_date, active, is_coordinator (integer 0 | 1, D1 boolean).
POST /app/save-review-case
Section titled “POST /app/save-review-case”Creates a new case. JWT audience snr_usr.
Request body
Section titled “Request body”| Field | Type | Required |
|---|---|---|
rt_id | string | yes |
dc_id | string | yes |
patient_age | number | yes |
patient_gender | string | yes (male / female) |
status | string | yes (draft / review) |
medical_history | string | no |
medical_documents | array | no (default []) |
Response (200)
Section titled “Response (200)”{ "status": 1000, "message": "Case saved", "data": { "case_id": "uuid" }}Invalid rt_id / dc_id → 400 (status 1002). Server error → 500 (1001).
GET /app/draftNotification
Section titled “GET /app/draftNotification”Processes reminders for cases still in draft status at least 24 hours after creation. Each eligible case receives the UCP98 draft notification once; review_cases.is_notified prevents duplicate sends. Failed notification calls are released for retry on a later invocation.
Response (200)
Section titled “Response (200)”{ "status": 1000, "message": "Draft notifications processed successfully", "data": { "eligible": 1, "notified": 1 }}Requires D1 migration 0035_review_cases_is_notified.sql.
POST /app/edit-review-case
Section titled “POST /app/edit-review-case”Updates an existing case only while status on the row is draft. After the case is review, edits return 409. JWT audience snr_usr.
Request body
Section titled “Request body”Same fields as save, plus:
| Field | Type | Required |
|---|---|---|
case_id | string | yes |
POST /app/list-review-cases
Section titled “POST /app/list-review-cases”Lists the authenticated user’s cases, newest first.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
page | number | no | default 1 |
page_size | number | no | default 15, max 100 |
Response
Section titled “Response”data is an array of case objects (same shape as detail). Includes pagination: total, page, page_size.
POST /app/update-meddocs
Section titled “POST /app/update-meddocs”Appends med_docs into existing medical_documents for a draft case.
Request body
Section titled “Request body”| Field | Type | Required | Notes |
|---|---|---|---|
case_id | string | yes | |
med_docs | object[] | yes | Array entries must include non-empty url, name, key. Max 50 total docs per case after append. |
Stored in D1 as JSON object array and appended in-place.
Response (200)
Section titled “Response (200)”{ "status": 1000, "message": "Medical documents updated", "data": {}}404 if the case is not yours; 409 if the case is not draft.
Example
Section titled “Example”{ "case_id": "550e8400-e29b-41d4-a716-446655440000", "med_docs": [ { "url": "https://example.com/storage/report.pdf", "name": "primary-physician-report.pdf", "key": "uploads/user123/report.pdf" }, { "url": "https://example.com/storage/lab.jpg", "name": "lab.jpg", "key": "uploads/user123/lab.jpg" } ]}POST /app/review-case-details
Section titled “POST /app/review-case-details”Single case for the authenticated user.
Request body
Section titled “Request body”| Field | Type | Required |
|---|---|---|
case_id | string | yes |
Not found → 404 (status 1002 in body per common envelope).
List and detail responses include reject_reason and case_coordinator_id (string or null) when present in D1.