HH/docs/external-api.md
2026-07-19 12:27:55 +03:00

748 lines
19 KiB
Markdown

# PX360 External API Documentation
Base URL: `/api/v1/external/`
## Authentication
All requests require an `X-API-Key` header.
```
X-API-Key: <your-api-key>
```
API keys are managed through the Django admin panel. Each key can be scoped to:
- A specific hospital (restricts data access to that hospital)
- Specific entities (`complaints`, `inquiries`, `observations`, `appreciations`, `suggestions`, `doctor_ratings`)
- Empty `allowed_entities` = access to all entities
### Creating an API Key
Via Django admin or programmatically:
```python
from apps.integrations.models import ExternalAPIKey
api_key, raw_key = ExternalAPIKey.create_key(
name="Partner Integration",
hospital=hospital_object, # Optional: scope to a hospital
allowed_entities=["complaints", "inquiries"], # Optional: empty = all
rate_limit=60, # Requests per minute
description="Integration with partner system",
)
# raw_key is shown ONCE - store it securely
```
### Error Responses
| HTTP Status | Meaning |
|---|---|
| `401` | Missing or invalid API key |
| `403` | API key does not have access to the requested entity, or hospital scope mismatch |
| `404` | Requested resource not found |
| `400` | Validation error (missing/invalid fields) |
---
## Lookup Endpoints
These endpoints provide dropdown/reference data for building forms.
### List Hospitals
```
GET /api/v1/external/hospitals/
```
**Response:**
```json
{
"count": 3,
"results": [
{ "id": "uuid", "name": "Al Nuzha", "code": "HH-N" },
{ "id": "uuid", "name": "Al Olya", "code": "HH-A" },
{ "id": "uuid", "name": "Al Suwaidi", "code": "HH-S" }
]
}
```
### List Location Types
```
GET /api/v1/external/location-types/
```
**Response:**
```json
{
"count": 4,
"results": [
{ "value": "OP", "label": "Outpatient", "label_ar": "خارجي" },
{ "value": "IP", "label": "Inpatient", "label_ar": "تنويم" },
{ "value": "ER", "label": "Emergency", "label_ar": "طوارئ" },
{ "value": "GENERAL", "label": "General", "label_ar": "عام" }
]
}
```
### List Areas
```
GET /api/v1/external/areas/?hospital=<hospital_name>
```
| Parameter | Required | Description |
|---|---|---|
| `hospital` | Yes | Hospital name (case-insensitive) |
| `location_type` | No | Filter by location type (`OP`, `IP`, `ER`, `GENERAL`) |
**Response:**
```json
{
"count": 27,
"results": [
{ "id": "uuid", "name": "Emergency", "name_ar": "", "code": "emergency" }
]
}
```
### List Departments
```
GET /api/v1/external/departments/?hospital=<hospital_name>
```
| Parameter | Required | Description |
|---|---|---|
| `hospital` | Yes | Hospital name (case-insensitive) |
**Response:**
```json
{
"count": 37,
"results": [
{ "id": "uuid", "name": "Critical Care Department", "name_en": "Critical Care Department", "name_ar": "", "code": "hh_n_critical_care_department" }
]
}
```
### List Sections
```
GET /api/v1/external/sections/?hospital=<hospital_name>&department=<department_name>
```
| Parameter | Required | Description |
|---|---|---|
| `hospital` | Yes | Hospital name (case-insensitive) |
| `department` | Yes | Department name (case-insensitive) |
**Response:**
```json
{
"count": 3,
"results": [
{ "id": "uuid", "name": "ICU", "name_ar": "", "code": "hh_n_critical_care_department__icu" }
]
}
```
---
## Complaints
### Create a Complaint
```
POST /api/v1/external/complaints/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name (case-insensitive) |
| `title` | Yes | string | Complaint title (max 500 chars) |
| `description` | Yes | string | Detailed complaint description |
| `contact_name` | Yes | string | Reporter's name (max 200 chars) |
| `contact_phone` | Yes | string | Reporter's phone (max 20 chars) |
| `contact_email` | No | string | Reporter's email |
| `relation_to_patient` | No | string | One of: `patient`, `relative`, `friend`, `other` |
| `patient_name` | No | string | Patient name |
| `national_id` | No | string | National ID number |
| `incident_date` | No | date | Date of incident (`YYYY-MM-DD`) |
| `expected_result` | No | string | Expected resolution |
| `location_type` | No | string | One of: `OP`, `IP`, `ER`, `GENERAL` |
| `area` | No | string | Area name (resolved by hospital) |
| `department` | No | string | Department name (resolved by hospital) |
| `section` | No | string | Section name (requires department) |
**Example:**
```json
{
"hospital": "Al Nuzha",
"title": "Long wait time in ER",
"description": "I waited 3 hours in the emergency room without being seen.",
"contact_name": "John Doe",
"contact_phone": "+966501234567",
"contact_email": "john@example.com",
"location_type": "ER",
"area": "Emergency",
"department": "Critical Care Department",
"section": "ICU"
}
```
**Response (201):**
```json
{
"success": true,
"reference_number": "CMP-20260609-167152",
"status": "open"
}
```
### List Complaints
```
GET /api/v1/external/complaints/list/
```
| Parameter | Required | Description |
|---|---|---|
| `page` | No | Page number (default: 1) |
| `page_size` | No | Items per page (default: 20, max: 100) |
| `created_from` | No | Filter by created date from (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) |
| `created_to` | No | Filter by created date to (date-only values auto-extend to end of day) |
| `updated_from` | No | Filter by updated date from |
| `updated_to` | No | Filter by updated date to |
| `status` | No | Filter by status value |
### Retrieve a Complaint
```
GET /api/v1/external/complaints/<reference_number>/
```
**Response:**
```json
{
"id": "uuid",
"reference_number": "CMP-20260609-167152",
"title": "Long wait time in ER",
"description": "I waited 3 hours...",
"status": "open",
"severity": "medium",
"priority": "medium",
"contact_name": "John Doe",
"contact_phone": "+966501234567",
"contact_email": "john@example.com",
"relation_to_patient": "",
"patient_name": "",
"incident_date": null,
"expected_result": "",
"resolution": "",
"satisfaction": "",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:38:03.808618+03:00",
"updated_at": "2026-06-09T13:38:03.808683+03:00"
}
```
#### Resolution & Satisfaction Fields
| Field | Description |
|---|---|
| `resolution` | Free-text description of the resolution taken (empty if unresolved) |
| `satisfaction` | Patient satisfaction: `satisfied`, `neutral`, `dissatisfied`, `no_response`, or empty |
### Set Patient Satisfaction
Sets the patient satisfaction level for a complaint. **A resolution must be recorded first** — returns `400` if the complaint has no resolution.
```
PATCH /api/v1/external/complaints/<reference_number>/satisfaction/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `satisfaction` | Yes | string | One of: `satisfied`, `neutral`, `dissatisfied`, `no_response` |
**Example:**
```json
{
"satisfaction": "satisfied"
}
```
**Response (200):**
```json
{
"success": true,
"reference_number": "CMP-20260609-167152",
"satisfaction": "satisfied"
}
```
---
## Inquiries
### Create an Inquiry
```
POST /api/v1/external/inquiries/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name |
| `subject` | Yes | string | Inquiry subject (max 500 chars) |
| `message` | Yes | string | Inquiry message |
| `contact_name` | Yes | string | Contact name |
| `contact_phone` | Yes | string | Contact phone |
| `contact_email` | No | string | Contact email |
| `category` | No | string | One of: `appointment`, `billing`, `medical_records`, `general`, `other` |
| `location_type` | No | string | One of: `OP`, `IP`, `ER`, `GENERAL` |
| `area` | No | string | Area name |
| `department` | No | string | Department name |
| `section` | No | string | Section name (requires department) |
**Response (201):**
```json
{
"success": true,
"reference_number": "INQ-20260609-124293",
"status": "open"
}
```
### List / Retrieve Inquiries
```
GET /api/v1/external/inquiries/list/
GET /api/v1/external/inquiries/<reference_number>/
```
Same pagination and date/status filters as complaints.
**Retrieve Response:**
```json
{
"id": "uuid",
"reference_number": "INQ-20260609-124293",
"subject": "Appointment rescheduling",
"message": "I need to reschedule my cardiology appointment.",
"category": "appointment",
"status": "open",
"contact_name": "Jane Smith",
"contact_phone": "+966509876543",
"contact_email": "jane@example.com",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:36:22.730962+03:00",
"updated_at": "2026-06-09T13:36:22.731020+03:00"
}
```
---
## Observations
### Create an Observation
```
POST /api/v1/external/observations/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name |
| `description` | Yes | string | Observation description |
| `title` | No | string | Title (max 300 chars) |
| `severity` | No | string | One of: `low`, `medium`, `high`, `critical` (default: `medium`) |
| `category` | No | UUID | Observation category UUID |
| `location_text` | No | string | Free-text location description |
| `incident_datetime` | No | datetime | When the incident occurred |
| `contact_name` | No | string | Reporter name (anonymous supported) |
| `contact_phone` | No | string | Reporter phone |
| `contact_email` | No | string | Reporter email |
| `reporter_staff_id` | No | string | Staff ID of reporter |
| `patient_file_number` | No | string | Patient file number |
**Response (201):**
```json
{
"success": true,
"reference_number": "OBS-O8TCIC",
"status": "new"
}
```
### List / Retrieve Observations
```
GET /api/v1/external/observations/list/
GET /api/v1/external/observations/<tracking_code>/
```
**Retrieve Response:**
```json
{
"id": "uuid",
"reference_number": "OBS-O8TCIC",
"title": "",
"description": "Wet floor near the main entrance, no warning sign posted.",
"severity": "medium",
"status": "new",
"location_text": "Main entrance lobby",
"incident_datetime": "2026-06-09T13:36:23.542046+03:00",
"contact_name": "Anonymous Reporter",
"contact_phone": "",
"contact_email": "",
"reporter_staff_id": "",
"patient_file_number": "",
"hospital_name": "Al Nuzha",
"category_name": null,
"created_at": "2026-06-09T13:36:23.540024+03:00",
"updated_at": "2026-06-09T13:36:23.540079+03:00"
}
```
---
## Appreciations
### Create an Appreciation
```
POST /api/v1/external/appreciations/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name |
| `message` | Yes | string | Appreciation message |
| `contact_name` | Yes | string | Submitter name |
| `contact_phone` | Yes | string | Submitter phone |
**Response (201):**
```json
{
"success": true,
"reference_number": "APR-202607-0001",
"status": "draft"
}
```
### List / Retrieve Appreciations
```
GET /api/v1/external/appreciations/list/
GET /api/v1/external/appreciations/<reference_number>/
```
**Retrieve Response:**
```json
{
"id": "uuid",
"reference_number": "APR-202607-0001",
"message_en": "Dr. Ahmed was incredibly kind and thorough.",
"status": "draft",
"hospital_name": "Al Nuzha",
"is_anonymous": false,
"created_at": "2026-06-09T13:36:23.788028+03:00",
"updated_at": "2026-06-09T13:36:23.788077+03:00"
}
```
---
## Suggestions
### Create a Suggestion
```
POST /api/v1/external/suggestions/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name |
| `message` | Yes | string | Suggestion message |
| `contact_name` | Yes | string | Submitter name |
| `contact_phone` | Yes | string | Submitter phone |
| `title` | No | string | Title (auto-generated from message if omitted) |
| `category` | No | string | One of: `clinical_care`, `staff_service`, `facility`, `communication`, `appointment`, `billing`, `food_service`, `cleanliness`, `technology`, `general`, `other` |
| `rating` | No | integer | Rating 1-5 |
**Response (201):**
```json
{
"success": true,
"reference_number": "SGT-202607-0001",
"status": "submitted"
}
```
### List / Retrieve Suggestions
```
GET /api/v1/external/suggestions/list/
GET /api/v1/external/suggestions/<reference_number>/
```
**Retrieve Response:**
```json
{
"id": "uuid",
"reference_number": "SGT-202607-0001",
"title": "Digital queue system",
"message": "Please implement a digital queue management system.",
"category": "technology",
"rating": null,
"status": "submitted",
"feedback_type": "suggestion",
"sentiment": "neutral",
"contact_name": "Suggestor",
"contact_phone": "+966504445566",
"hospital_name": "Al Nuzha",
"created_at": "2026-06-09T13:36:24.051998+03:00",
"updated_at": "2026-06-09T13:36:24.052047+03:00"
}
```
---
## Doctor Ratings
### Submit a Doctor Rating
Submit a patient rating for a doctor. The `doctor_id` should identify the doctor (existing staff matched by employee ID, license number, or name; otherwise a new Staff record is auto-created — see below).
```
POST /api/v1/external/doctor-ratings/
```
**Request Body:**
| Field | Required | Type | Description |
|---|---|---|---|
| `hospital` | Yes | string | Hospital name (case-insensitive) |
| `doctor_id` | Yes | string | Doctor's employee ID |
| `rating` | Yes | integer | Rating from 1 to 5 |
| `doctor_name` | Conditional | string | Doctor name. Required when `doctor_id` does not match an existing staff record (see auto-creation below). Otherwise stored for reference. |
| `feedback` | No | string | Patient feedback text |
| `rating_date` | No | date | Date of rating (`YYYY-MM-DD`, defaults to current date) |
| `patient_uhid` | No | string | Patient unique health ID |
| `patient_name` | No | string | Patient name |
| `patient_type` | No | string | One of: `IP`, `OP`, `ER`, `DC` |
| `department_name` | No | string | Department name |
| `admit_date` | No | date | Admission date (`YYYY-MM-DD`) |
| `discharge_date` | No | date | Discharge date (`YYYY-MM-DD`) |
**Staff lookup & auto-creation:** When a rating is submitted, the server looks up the doctor by (in order) `employee_id`, `license_number`, then `doctor_name` (exact / contains / first+last name split). If no existing staff is matched, a new Staff record is **auto-created** with `staff_type=physician`, `job_title="Physician"`, `employee_id=doctor_id`, and the provided name. In that case `doctor_name` is required; omitting it returns `400`. The auto-creation is logged as an `external_staff_auto_created` audit event.
**Example:**
```json
{
"hospital": "Al Nuzha",
"doctor_id": "10738",
"doctor_name": "Dr. Omaymah Yaqoub",
"rating": 4,
"feedback": "Great doctor, very caring and attentive.",
"rating_date": "2026-06-10",
"patient_uhid": "UHID12345",
"patient_name": "Ahmed Ali",
"patient_type": "OP",
"department_name": "Internal Medicine"
}
```
**Response (201):**
```json
{
"success": true,
"rating_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"rating": 4
}
```
**Error Responses:**
| HTTP Status | Condition |
|---|---|
| `400` | `doctor_id` matches no existing staff AND `doctor_name` is missing (cannot auto-create) |
| `400` | `rating` outside 1-5 range |
| `400` | `hospital` not found |
| `403` | Hospital does not match API key scope |
**Error Example (cannot auto-create without a name):**
```json
{
"doctor_name": [
"Staff not found for this doctor_id; provide doctor_name to auto-create the staff record."
]
}
```
### List Doctor Ratings
Returns a paginated list of doctor ratings, scoped to the API key's hospital.
```
GET /api/v1/external/doctor-ratings/list/
```
**Query Parameters (all optional):**
| Parameter | Description |
|---|---|
| `doctor_id` | Filter by HIS employee ID (exact match) |
| `rating` | Filter by rating value 1-5 (exact match) |
| `rating_from` | Rating-date lower bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) |
| `rating_to` | Rating-date upper bound (`YYYY-MM-DD` or `YYYY-MM-DDTHH:MM:SS`) |
| `created_from` / `created_to` | Created-at date range |
| `updated_from` / `updated_to` | Updated-at date range |
| `page` | Page number (default `1`) |
| `page_size` | Page size (default `20`, max `100`) |
**Response (200):**
```json
{
"count": 42,
"page": 1,
"page_size": 20,
"results": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"staff_id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
"hospital_name": "Al Nuzha",
"source": "his_api",
"source_reference": "",
"doctor_id": "10738",
"doctor_name": "OMAYMAH YAQOUB ELAMEIAN",
"doctor_name_raw": "10738 - OMAYMAH YAQOUB ELAMEIAN",
"doctor_name_display": "OMAYMAH YAQOUB ELAMEIAN",
"department_name": "Internal Medicine",
"patient_uhid": "UHID12345",
"patient_name": "Ahmed Ali",
"patient_type": "OP",
"admit_date": null,
"discharge_date": null,
"rating": 4,
"feedback": "Great doctor, very caring and attentive.",
"rating_date": "2026-06-10T00:00:00+03:00",
"is_aggregated": false,
"aggregated_at": null,
"created_at": "2026-06-10T14:22:11+03:00",
"updated_at": "2026-06-10T14:22:11+03:00"
}
]
}
```
### Retrieve a Doctor Rating
Retrieve a single rating by its UUID (returned in the `rating_id` field on creation).
```
GET /api/v1/external/doctor-ratings/<rating_id>/
```
**Response (200):** Same object shape as a single item from the list endpoint.
**Error Responses:**
| HTTP Status | Condition |
|---|---|
| `404` | Rating does not exist, or is outside the API key's hospital scope |
| `403` | API key does not have access to `doctor_ratings` |
---
## Common Patterns
### Location Hierarchy
Locations follow a cascading hierarchy. Use lookup endpoints to populate dependent dropdowns:
```
1. GET /hospitals/ → pick hospital
2. GET /location-types/ → pick location type
3. GET /areas/?hospital=X → pick area (filtered by hospital)
4. GET /departments/?hospital=X → pick department
5. GET /sections/?hospital=X&department=Y → pick section
```
### Date Filtering
All list endpoints support date range filtering:
```
GET /complaints/list/?created_from=2026-01-01&created_to=2026-06-30
GET /complaints/list/?updated_from=2026-06-01T08:00:00&updated_to=2026-06-09T17:00:00
```
- Date-only values (`YYYY-MM-DD`): `created_from` starts at 00:00, `created_to` extends to 23:59:59
- Full datetime values (`YYYY-MM-DDTHH:MM:SS`) are also accepted
### Pagination
All list endpoints return paginated results:
```json
{
"count": 150,
"page": 1,
"page_size": 20,
"results": [...]
}
```
| Parameter | Default | Max |
|---|---|---|
| `page` | 1 | - |
| `page_size` | 20 | 100 |
### Reference Number Formats
| Entity | Prefix | Format |
|---|---|---|
| Complaint | `CMP-` | `CMP-YYYYMMDD-XXXXXX` |
| Inquiry | `INQ-` | `INQ-YYYYMMDD-XXXXXX` |
| Observation | `OBS-` | `OBS-XXXXXX` (auto-generated by model) |
| Appreciation | `APR-` | `APR-YYYYMM-NNNN` |
| Suggestion | `SGT-` | `SGT-YYYYMM-NNNN` |