14 KiB
PX360 Journey & Survey Engine
Overview
The Journey & Survey Engine is the core of PX360's patient experience tracking system. It implements an event-driven architecture where external systems (HIS, Lab, Radiology, Pharmacy) send integration events that trigger journey stage completions, which in turn automatically send stage-specific surveys to patients.
Architecture
Components
- Journey Templates - Define the stages for each journey type (EMS/Inpatient/OPD)
- Journey Instances - Track actual patient encounters
- Stage Templates - Define individual stages with trigger events
- Stage Instances - Track completion of each stage
- Inbound Events - Integration events from external systems
- Survey Templates - Define surveys for each stage
- Survey Instances - Actual surveys sent to patients
Flow Diagram
External System (HIS/Lab/etc.)
↓
POST /api/integrations/events/
↓
InboundEvent (status: PENDING)
↓
Celery Task: process_inbound_event
↓
Find Journey Instance (by encounter_id)
↓
Find Matching Stage (by trigger_event_code)
↓
Complete Stage → Update timestamps
↓
If auto_send_survey=True
↓
Create SurveyInstance
↓
Send Survey (SMS/WhatsApp/Email)
↓
Patient Completes Survey
↓
If score < threshold → Create PXAction
Journey Types
1. EMS (Emergency Medical Services)
Tracks emergency patient pathway from arrival to discharge.
Example Stages:
- Triage
- Emergency Physician Consultation
- Diagnostic Tests
- Treatment
- Discharge
2. Inpatient
Tracks admitted patient journey from admission to discharge.
Example Stages:
- Admission
- Ward Assignment
- Physician Rounds
- Nursing Care
- Diagnostic Services
- Treatment
- Discharge
3. OPD (Outpatient Department)
Tracks outpatient visit from registration to checkout.
Example Stages:
- Registration
- MD Consultation
- Lab Tests
- Radiology
- Pharmacy
- Checkout
Integration Event Contracts
Event Structure
All integration events must follow this structure:
{
"source_system": "his|lab|radiology|pharmacy|moh|chi|other",
"event_code": "EVENT_CODE",
"encounter_id": "UNIQUE_ENCOUNTER_ID",
"patient_identifier": "MRN_OR_PATIENT_ID",
"payload_json": {
"physician_license": "PHY001",
"department_code": "DEPT_OPD",
"timestamp": "2025-12-14T10:30:00Z",
"additional_data": {}
}
}
Standard Event Codes
OPD Events
OPD_REGISTRATION_COMPLETED- Patient registeredOPD_VISIT_COMPLETED- MD consultation completedOPD_CHECKOUT_COMPLETED- Patient checked out
Lab Events
LAB_ORDER_CREATED- Lab order placedLAB_SAMPLE_COLLECTED- Sample collectedLAB_ORDER_COMPLETED- Results ready
Radiology Events
RADIOLOGY_ORDER_CREATED- Radiology order placedRADIOLOGY_EXAM_COMPLETED- Exam performedRADIOLOGY_REPORT_FINALIZED- Report signed off
Pharmacy Events
PHARMACY_ORDER_RECEIVED- Prescription receivedPHARMACY_DISPENSED- Medication dispensed
Inpatient Events
ADMISSION_COMPLETED- Patient admittedWARD_TRANSFER- Patient transferred to wardDISCHARGE_COMPLETED- Patient discharged
EMS Events
EMS_ARRIVAL- Patient arrived via EMSTRIAGE_COMPLETED- Triage assessment doneER_PHYSICIAN_CONSULT- ER physician saw patient
Example Event Payloads
OPD Visit Completed
{
"source_system": "his",
"event_code": "OPD_VISIT_COMPLETED",
"encounter_id": "ENC20251214001",
"patient_identifier": "MRN123456",
"payload_json": {
"physician_license": "PHY001",
"department_code": "DEPT_OPD",
"visit_date": "2025-12-14",
"visit_time": "10:30:00",
"chief_complaint": "Follow-up",
"diagnosis_codes": ["Z00.00"],
"timestamp": "2025-12-14T10:45:00Z"
}
}
Lab Order Completed
{
"source_system": "lab",
"event_code": "LAB_ORDER_COMPLETED",
"encounter_id": "ENC20251214001",
"patient_identifier": "MRN123456",
"payload_json": {
"department_code": "DEPT_LAB",
"order_id": "LAB789",
"test_codes": ["CBC", "BMP"],
"collection_time": "2025-12-14T11:00:00Z",
"result_time": "2025-12-14T14:30:00Z",
"timestamp": "2025-12-14T14:30:00Z"
}
}
Pharmacy Dispensed
{
"source_system": "pharmacy",
"event_code": "PHARMACY_DISPENSED",
"encounter_id": "ENC20251214001",
"patient_identifier": "MRN123456",
"payload_json": {
"department_code": "DEPT_PHARMACY",
"prescription_id": "RX456",
"medications": [
{"name": "Medication A", "quantity": 30},
{"name": "Medication B", "quantity": 60}
],
"dispensed_time": "2025-12-14T15:00:00Z",
"timestamp": "2025-12-14T15:00:00Z"
}
}
Journey Configuration
Creating a Journey Template
Step 1: Create Journey Template
from apps.journeys.models import PatientJourneyTemplate, JourneyType
from apps.organizations.models import Hospital
hospital = Hospital.objects.get(code='HOSP001')
journey_template = PatientJourneyTemplate.objects.create(
name='Standard OPD Journey',
name_ar='رحلة العيادات الخارجية القياسية',
journey_type=JourneyType.OPD,
hospital=hospital,
is_active=True,
is_default=True,
description='Standard outpatient journey with MD, Lab, Radiology, and Pharmacy stages'
)
Step 2: Create Stage Templates
from apps.journeys.models import PatientJourneyStageTemplate
from apps.surveys.models import SurveyTemplate
# Get survey templates
md_survey = SurveyTemplate.objects.get(name='MD Consultation Survey')
lab_survey = SurveyTemplate.objects.get(name='Lab Experience Survey')
# Stage 1: MD Consultation
PatientJourneyStageTemplate.objects.create(
journey_template=journey_template,
name='MD Consultation',
name_ar='استشارة الطبيب',
code='OPD_MD_CONSULT',
order=1,
trigger_event_code='OPD_VISIT_COMPLETED',
survey_template=md_survey,
auto_send_survey=True,
survey_delay_hours=2, # Send 2 hours after visit
requires_physician=True,
requires_department=True,
is_optional=False,
is_active=True
)
# Stage 2: Lab
PatientJourneyStageTemplate.objects.create(
journey_template=journey_template,
name='Laboratory',
name_ar='المختبر',
code='LAB',
order=2,
trigger_event_code='LAB_ORDER_COMPLETED',
survey_template=lab_survey,
auto_send_survey=True,
survey_delay_hours=1,
requires_physician=False,
requires_department=True,
is_optional=True, # Not all patients need lab
is_active=True
)
# Add more stages as needed...
Creating a Journey Instance
When a patient encounter starts:
from apps.journeys.models import PatientJourneyInstance
from apps.organizations.models import Patient
patient = Patient.objects.get(mrn='MRN123456')
journey_instance = PatientJourneyInstance.objects.create(
journey_template=journey_template,
patient=patient,
encounter_id='ENC20251214001',
hospital=hospital,
department=opd_department
)
# Stage instances are automatically created for all active stages
Event Processing
Processing Logic
When an event is received:
-
Event Receipt
- POST to
/api/integrations/events/ - Event stored with
status=PENDING - Audit log created
- Celery task queued
- POST to
-
Event Processing (Celery Task)
- Find journey instance by
encounter_id - Find matching stage where:
stage_template.trigger_event_code == event.event_codestage_instance.status IN [PENDING, IN_PROGRESS]
- Extract physician and department from payload
- Complete the stage:
- Set
status=COMPLETED - Set
completed_at=now() - Attach
physician,department,event - Store event payload in metadata
- Set
- Find journey instance by
-
Survey Triggering
- If
stage_template.auto_send_survey=True - If
stage_template.survey_templateexists - Create
SurveyInstance - Queue survey send task with delay
- If
-
Journey Completion
- Check if all required stages completed
- If yes, mark journey as
COMPLETED
Error Handling
Event Not Found:
- Status:
IGNORED - Reason: "No journey instance found for encounter {encounter_id}"
Stage Not Found:
- Status:
IGNORED - Reason: "No pending stage found with trigger {event_code}"
Processing Error:
- Status:
FAILED - Error message stored
- Retry 3 times with exponential backoff
- Can be manually reprocessed by PX Admin
API Endpoints
Journey Templates
GET /api/journeys/templates/ # List templates
POST /api/journeys/templates/ # Create template
GET /api/journeys/templates/{id}/ # Get template
PUT /api/journeys/templates/{id}/ # Update template
DELETE /api/journeys/templates/{id}/ # Delete template
Journey Instances
GET /api/journeys/instances/ # List instances
POST /api/journeys/instances/ # Create instance
GET /api/journeys/instances/{id}/ # Get instance
GET /api/journeys/instances/{id}/progress/ # Get progress summary
Integration Events
POST /api/integrations/events/ # Create event (external systems)
POST /api/integrations/events/bulk_create/ # Bulk create events
GET /api/integrations/events/ # List events (PX Admin)
GET /api/integrations/events/{id}/ # Get event details
POST /api/integrations/events/{id}/reprocess/ # Reprocess failed event
Testing
Test Scenario: OPD Journey
1. Create Journey Instance
curl -X POST http://localhost:8000/api/journeys/instances/ \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"journey_template": "TEMPLATE_UUID",
"patient": "PATIENT_UUID",
"encounter_id": "ENC20251214001",
"hospital": "HOSPITAL_UUID"
}'
2. Send MD Visit Completed Event
curl -X POST http://localhost:8000/api/integrations/events/ \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source_system": "his",
"event_code": "OPD_VISIT_COMPLETED",
"encounter_id": "ENC20251214001",
"patient_identifier": "MRN123456",
"payload_json": {
"physician_license": "PHY001",
"department_code": "DEPT_OPD",
"timestamp": "2025-12-14T10:45:00Z"
}
}'
3. Verify Stage Completion
curl http://localhost:8000/api/journeys/instances/JOURNEY_UUID/progress/ \
-H "Authorization: Bearer YOUR_TOKEN"
Expected Response:
{
"journey_id": "...",
"encounter_id": "ENC20251214001",
"patient": "John Doe",
"journey_type": "opd",
"status": "active",
"completion_percentage": 25,
"is_complete": false,
"stages": [
{
"name": "MD Consultation",
"order": 1,
"status": "completed",
"completed_at": "2025-12-14T10:45:00Z",
"survey_sent": true
},
{
"name": "Laboratory",
"order": 2,
"status": "pending",
"completed_at": null,
"survey_sent": false
}
]
}
Configuration Best Practices
1. Survey Delay
- Immediate (0 hours): For time-sensitive feedback (ER, urgent care)
- 2-4 hours: For outpatient visits (allows patient to leave facility)
- 24 hours: For inpatient daily surveys
- 48-72 hours: For discharge surveys
2. Optional vs Required Stages
- Required: Core pathway stages (MD consultation, admission, discharge)
- Optional: Ancillary services (lab, radiology, pharmacy) - not all patients need these
3. Trigger Event Codes
- Use consistent naming:
{SERVICE}_{ACTION}_COMPLETED - Examples:
OPD_VISIT_COMPLETED,LAB_ORDER_COMPLETED - Document all codes in integration contracts
4. Survey Templates
- Create specific surveys for each stage
- Keep surveys short (5-7 questions max)
- Use NPS or Likert scales
- Include open-ended feedback field
Monitoring & Troubleshooting
Check Event Processing Status
# List pending events
curl http://localhost:8000/api/integrations/events/?status=pending \
-H "Authorization: Bearer YOUR_TOKEN"
# List failed events
curl http://localhost:8000/api/integrations/events/?status=failed \
-H "Authorization: Bearer YOUR_TOKEN"
Reprocess Failed Event
curl -X POST http://localhost:8000/api/integrations/events/EVENT_UUID/reprocess/ \
-H "Authorization: Bearer YOUR_TOKEN"
View Journey Progress
curl http://localhost:8000/api/journeys/instances/?encounter_id=ENC123 \
-H "Authorization: Bearer YOUR_TOKEN"
Audit Trail
All events are logged:
- Event receipt →
integration_event - Stage completion →
stage_completed - Journey completion →
journey_completed - Survey sent →
survey_sent
View audit logs in Django admin or via API.
Performance Considerations
Database Indexes
encounter_id- Fast journey lookuptrigger_event_code- Fast stage matchingstatus+received_at- Efficient pending event queries
Celery Configuration
- Events processed asynchronously
- Retry logic with exponential backoff
- Max 100 events per batch in periodic task
- Separate queue for event processing (optional)
Optimization Tips
- Use
select_relatedfor foreign keys - Use
prefetch_relatedfor reverse relations - Batch process events in periodic task
- Monitor Celery queue length
- Scale Celery workers as needed
Security
API Authentication
- JWT tokens for internal systems
- API keys for external systems (TODO: implement)
- Rate limiting on event endpoint (TODO: implement)
Data Validation
- All events validated before storage
- Payload JSON validated
- Encounter ID format validation
- Source system whitelist
Troubleshooting
Event Not Processing
- Check event status in admin
- Check Celery worker logs
- Verify journey instance exists
- Verify stage template has matching trigger code
- Check stage instance status (must be PENDING or IN_PROGRESS)
Survey Not Sent
- Verify
auto_send_survey=Trueon stage template - Verify
survey_templateis set - Check survey instance created
- Check notification logs (Phase 4)
Stage Not Completing
- Verify event code matches
trigger_event_code - Check stage instance status
- Verify physician/department codes if required
- Check event processing logs
For API documentation, see: /api/docs/
For implementation status, see: IMPLEMENTATION_STATUS.md