Integration Spec¶
Last updated: 10 April 2026
Entities¶
Case¶
Represents a single test/report workflow instance.
Fields (Table Storage: Cases, PartitionKey = CrmLocationId, RowKey = CaseId)
Core: - caseId (string, GUID) - createdUtc (datetime) - status (enum, see below) - crmLocationId (string) // GoHighLevel location/sub-account ID - crmContactId (string) // canonical patient ID - patientName (string, optional) - tagId (string, optional) // case grouping label - lastError (string, optional)
Randox: - randoxOrderId (long, optional) // Randox numeric order ID - randoxOrderRef (string, optional) // e.g., FML001-00455077 - randoxOrderStatusId (long, optional) // 2=Submitted, 3=Pending, 4+=Complete - randoxOrderStatusLabel (string, optional) - randoxSampleCollectionDate (string, optional) - randoxResultsUploadDate (string, optional) - randoxTestClinicLocationId (int, optional) - randoxTestClinicLocationName (string, optional) - isHealthCheckPanelReport (bool) // true = Randox PDF-only panel - isManuallyLinkedRandoxOrder (bool) - randoxPanelIdsCsv (string, optional) - randoxTestIdsCsv (string, optional) - randoxPanelLabelsCsv (string, optional) - randoxTestLabelsCsv (string, optional) - randoxSampleTubesJson (string, optional) - randoxOrderFormContextJson (string, optional)
OptimalDX: - odxPracticeId (string, optional) - odxExternalPatientId (string, optional) // = crmContactId - odxPatientId (string, optional) - odxReportId (string, optional) // back-compat; prefer odxPatientTestId - odxPatientTestId (string, optional) // case-linked test anchor - odxUnitType (string, optional) // persisted at submission; avoids re-fetching from ODX - odxPatientTestPdfImportId (string, optional) // for PDF-upload path - odxUploadedAtUtc (datetime, optional) - odxSubmittedAtUtc (datetime, optional) - odxPatientReportEmailedAtUtc (datetime, optional) - odxPdfSendHistoryJson (string, optional) // JSON array of send events
Randox PDF: - randoxReportPdfEmailedAtUtc (datetime, optional)
Artifacts: - randoxRawBlobPath (string, optional) - odxPayloadBlobPath (string, optional) - pdfBlobPath (string, optional) // legacy - odxPatientPdfBlobPath (string, optional) - odxPractitionerPdfBlobPath (string, optional) - odxPatientPdfCacheKey (string, optional) - odxPractitionerPdfCacheKey (string, optional) - randoxTofBlobPath (string, optional) // deleted on completion
Status enum (CaseStatus): - Created - RandoxOrderLinked - RandoxOrderSubmitted - RandoxResultsPulled - OdxPatientSynced - OdxResultsSubmitted - OdxReportGenerated - Completed - Failed
Other Tables¶
- ContactNotes (PartitionKey = CrmContactId, RowKey = NoteId)
- Tags (PartitionKey = "tag", RowKey = TagId)
- SandboxClinicalBookings (booking audit log)
Core workflows¶
Workflow A: Automated (production — default)¶
1) Staff creates case linked to CRM contact 2) Staff creates Randox order (patient demographics pulled from CRM) 3) Hourly timer polls Randox for status updates (status 2/3) 4) On Randox completion (status ≥ 4): auto-pull results, store raw JSON 5) Auto-submit results to ODX via HL7 (sync patient + create patient test) 6) Auto-generate Practitioner PDF (Select All reports) 7) Auto-email + SMS patient via CRM with attached PDF 8) Delete cached Test Order Form blob
Workflow B: Manual / historical orders¶
Same as A but order is manually linked (not created). Auto-generation and auto-email are not triggered for manually-linked orders unless explicitly configured.
Workflow C: PDF-only panels (microbiome/NGS/health check)¶
1–4 same as Workflow A 5) Fetch Randox report PDF instead of submitting to ODX 6) Email Randox PDF to patient via CRM
API Endpoints (Functions API)¶
All endpoints under /api.
Health & diagnostics¶
- GET /health — returns { status: "ok" }
- GET /kv-test — reads a Key Vault secret, returns { status: "ok" } (never returns secret values)
- GET /diag/* — diagnostic endpoints (Admin/Staff only)
Cases¶
- POST /cases — create case (body: { crmLocationId, crmContactId, patientName? })
- GET /cases — list recent cases
- GET /cases/{caseId} — case detail
- PATCH /cases/{caseId} — update case fields
Randox¶
- POST /cases/{caseId}/randox/create-order — create Randox order (demographics from CRM)
- POST /cases/{caseId}/randox/link-order — link existing order reference
- POST /cases/{caseId}/randox/refresh-status — poll Randox for current status
- POST /cases/{caseId}/randox/pull-results — pull and store raw results
- GET /cases/{caseId}/randox/results — view stored results
- GET /cases/{caseId}/randox/catalog — Randox panel/test catalog
OptimalDX¶
- POST /cases/{caseId}/odx/sync-patient — create/sync patient in ODX
- POST /cases/{caseId}/odx/submit-results — submit HL7 ORU^R01 to ODX
- POST /cases/{caseId}/odx/reports/practitioner-pdf — generate practitioner report PDF
- POST /cases/{caseId}/odx/reports/patient-pdf — generate patient report PDF
- GET /cases/{caseId}/odx/reports/available — list available report types
- POST /cases/{caseId}/odx/send-email — send PDF via CRM email
CRM¶
- GET /crm/contacts/search — search CRM contacts
- GET /crm/contacts/{contactId} — get contact detail
- POST /crm/contacts — create CRM contact
- PUT /crm/contacts/{contactId} — update CRM contact
- GET /crm/contacts/{contactId}/notes — get clinical notes
- POST /crm/contacts/{contactId}/notes — add clinical note
System¶
- GET /system/users — list SWA users
- POST /system/users/invite — generate SWA invite link
- GET /system/tags — list tags
- POST /system/tags — create tag
Timer triggers¶
- PollRandoxStatuses — runs every hour (0 0 /1 * * ), polls in-flight orders and auto-processes completions
Mapping & data rules¶
- Store raw Randox response unmodified.
- HbA1c: always elementId 781 (mmol/mol, IFCC). No unit conversions.
- Randox order numbers: default format FML001-XXXXXXXX (8 digits, uniqueness guard). Numeric-only values indicate legacy orders or RandoxExternalNumberFormat=numeric12 override.
API caching strategy (IMemoryCache)¶
All caches are in-process (IMemoryCache, singleton). A Function App restart clears all caches.
| Client | Method | Cache Key | TTL | Purpose |
|---|---|---|---|---|
| OptimalDxClient | GetPartnerLabsAsync | odx:partner:labs | 24h | Reference data — rarely changes |
| OptimalDxClient | GetLabProfilesAsync | odx:practice:{id}:labprofiles | 24h | Reference data — rarely changes |
| OptimalDxClient | GetPracticeAvailableReportsAsync | odx:reports:{group}:practice:{id} | 1h | Report catalog |
| OptimalDxClient | GetAvailableReportsAsync | odx:reports:{group} | 1h | Report catalog |
| OdxPatientService | EnsureOdxPatientAsync | odx:patient:crm:{crmContactId} | 30min | Patient resolution (avoids 2-20+ API call waterfall) |
| RandoxClient | GetRandoxCatalogAsync | randox:catalog | 24h | Panel/test catalog |
| RandoxClient | GetOrderStatusAsync | per orderId | 5min | Reduces redundant status polls |
| GhlClient | various | per endpoint | 30min | CRM contact data |
Additionally, OdxUnitType is persisted on the Case entity during HL7 submission, so subsequent accesses to a completed case make zero ODX API calls for test resolution.
Portal gating rules¶
- Do not allow "View results" or "Generate/Send PDF" while Randox status is 2/3.
- Provide a "Refresh status" action for status 2/3.
- Only allow results actions once Randox status is complete (>= 4) and results are stored.
Security¶
- No secrets in code.
- Functions use Managed Identity to read Key Vault secrets (DefaultAzureCredential).
- Portal auth: Entra ID via SWA built-in auth.
- Roles: How_Admin, How_Staff, How_ReadOnly (enforced in staticwebapp.config.json + server-side via x-ms-client-principal).
- All HttpClients use Microsoft.Extensions.Http.Resilience (retry, circuit breaker, timeout).
- HTTPS-only enforced on Function App.