Skip to content

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.