Skip to content

ARCHITECTURE

Last updated: 10 April 2026

Overview

The HOW Integration Platform orchestrates blood-test workflows for House of Wellness UK. It connects three external systems — Randox (laboratory), OptimalDX (report interpretation), and GoHighLevel/CRM (patient identity & communications) — through a central Azure-hosted backend, with a private staff portal for operator access.

Production URL: https://results.houseofwellnessuk.com

1. High-Level Architecture

graph TB
    subgraph "Staff Portal"
        SWA["Azure Static Web App<br/>(React + Vite)"]
    end

    subgraph "Azure Backend"
        FUNC["Azure Functions<br/>(.NET 10, isolated worker)"]
        TS["Table Storage<br/>(Cases, Tags, Notes, Bookings)"]
        BS["Blob Storage<br/>(artifacts container)"]
        KV["Azure Key Vault<br/>(API keys, credentials)"]
    end

    subgraph "External Systems"
        RANDOX["Randox Health<br/>(Laboratory API)"]
        ODX["OptimalDX<br/>(Report Engine API)"]
        GHL["GoHighLevel / CRM<br/>(Patient Records + Comms)"]
    end

    SWA -->|"/api/* (SWA proxy)"| FUNC
    FUNC -->|"Read/Write entities"| TS
    FUNC -->|"Read/Write artifacts"| BS
    FUNC -->|"DefaultAzureCredential"| KV

    FUNC -->|"Orders, results, catalogue"| RANDOX
    FUNC -->|"Patients, HL7 results, reports"| ODX
    FUNC -->|"Contacts, emails, SMS"| GHL

    style SWA fill:#e8f5e9
    style FUNC fill:#e3f2fd
    style RANDOX fill:#fff3e0
    style ODX fill:#fce4ec
    style GHL fill:#f3e5f5

2. Azure Resource Layout

graph LR
    subgraph "prod-spoke-uks (Production)"
        subgraph "how-prod-integration-rg"
            PF["how-prod-api-func<br/>Function App (Y1, UK South)"]
            PS["how-prod-portal<br/>Static Web App (West Europe)"]
            PST["howprodintegration<br/>Storage Account"]
            PKV["how-prod-kv<br/>Key Vault"]
        end
    end

    DNS["results.houseofwellnessuk.com"] --> PS
    PS -->|"linked backend"| PF
    PF -->|"Managed Identity"| PKV
    PF -->|"Managed Identity"| PST

3. End-to-End Case Lifecycle

sequenceDiagram
    participant Staff as Portal Staff
    participant API as Functions API
    participant CRM as GoHighLevel
    participant R as Randox
    participant ODX as OptimalDX
    participant Store as Azure Storage

    Note over Staff,Store: 1. Case Creation
    Staff->>API: Search/create patient
    API->>CRM: GET/POST contacts
    CRM-->>API: Contact details
    Staff->>API: POST /api/cases
    API->>Store: Insert CaseEntity (Table)

    Note over Staff,Store: 2. Randox Order
    Staff->>API: Create Randox order
    API->>CRM: Get patient demographics (DOB, sex)
    API->>R: POST CreatePendingOrder
    R-->>API: { orderId, orderNumber }
    API->>R: POST UpdateOrder (sample details)
    API->>Store: Update case (randoxOrderId, status=Submitted)
    API->>Store: Generate TOF PDF (Blob)

    Note over Staff,Store: 3. Automated Polling (hourly)
    loop Every hour (timer trigger)
        API->>Store: Load in-flight cases (status 2/3)
        API->>R: POST GetOrderStatus
        R-->>API: { statusId, statusDescription }
        API->>Store: Update case status
    end

    Note over Staff,Store: 4. Results & Report (automated on completion)
    R-->>API: Status = Completed (≥4)
    API->>R: POST GetOrderResultDetail
    R-->>API: Results JSON
    API->>Store: Save randox/raw/{caseId}.json (Blob)

    alt Standard blood panel
        API->>ODX: POST patient (create/sync)
        API->>ODX: POST patient test (HL7 ORU^R01)
        API->>Store: Save odx/hl7/{caseId}.hl7 (Blob)
        API->>ODX: POST FunctionalHealthReport
        ODX-->>API: PDF bytes
        API->>Store: Save odx/pdf/{caseId}/report.pdf (Blob)
    else PDF-only panel (microbiome/NGS)
        API->>R: POST GetOrderResultReports
        R-->>API: Base64 PDF
        API->>Store: Save randox/pdf/{caseId}/report.pdf (Blob)
    end

    API->>CRM: Upload PDF attachment
    API->>CRM: Send Email + SMS to patient
    API->>Store: Update case (emailed timestamps)

4. Data Flow — Randox Integration (Deep Dive)

flowchart LR
    subgraph "HOW Backend"
        AUTH["Auth: POST /auth/token<br/>(username + password + client_id)"]
        CREATE["CreatePendingOrder<br/>→ patient demographics<br/>→ panels, tests, reasons<br/>→ externalNumber (FML001-XXXXXXXX)"]
        UPDATE["UpdateOrder<br/>→ sample collection details<br/>→ tubes, lab, date"]
        POLL["GetOrderStatus<br/>→ orderId / orderNumber<br/>← statusId (2=Submitted,<br/>3=Pending, 4+=Complete)"]
        PULL["GetOrderResultDetail<br/>→ orderId / orderNumber<br/>← analytes, results, units,<br/>ref ranges, dates"]
        PDF["GetOrderResultReports<br/>→ orderId / orderNumber<br/>← base64 PDF"]
    end

    subgraph "Randox API"
        RAPI["api-e endpoints<br/>(APIM gateway)"]
    end

    subgraph "Storage"
        BLOB_RAW["randox/raw/{caseId}.json"]
        BLOB_PDF["randox/pdf/{caseId}/*.pdf"]
        BLOB_TOF["randox/tof/{caseId}.pdf"]
        TABLE["Cases table<br/>(randoxOrderId, randoxOrderRef,<br/>randoxOrderStatusId, etc.)"]
    end

    AUTH --> RAPI
    CREATE --> RAPI
    UPDATE --> RAPI
    POLL --> RAPI
    PULL --> RAPI
    PDF --> RAPI

    PULL --> BLOB_RAW
    PDF --> BLOB_PDF
    CREATE --> TABLE
    UPDATE --> BLOB_TOF
    POLL --> TABLE

    style RAPI fill:#fff3e0

Randox Authentication

Field Source
username Key Vault: Randox-Prod-Username
password Key Vault: Randox-Prod-Password
client_id Key Vault: Randox-Prod-ClientID
subscription_key Key Vault: Randox-SubscriptionKey (header: Ocp-Apim-Subscription-Key)
Base URL Key Vault: Randox-Prod-BaseURL

Randox Order Status Codes

Code Description Action
2 Submitted Poll continues
3 Pending Results Poll continues
4+ Completed Pull results, trigger downstream

5. Data Flow — OptimalDX Integration (Deep Dive)

flowchart LR
    subgraph "HOW Backend"
        SYNC["Sync Patient<br/>→ firstName, lastName,<br/>DOB, gender, email<br/>→ externalPatientId = crmContactId"]
        HL7["Submit Results (HL7 ORU^R01)<br/>→ labProfileId, testDate<br/>→ OBX segments per analyte<br/>→ code^name^99RDX system"]
        FHR["Generate Report<br/>→ practiceId, patientId,<br/>patientTestId, reportIds[]<br/>← PDF bytes"]
        UPLOAD["Upload PDF<br/>→ multipart file upload<br/>→ patientId, type=BloodTestResults"]
    end

    subgraph "OptimalDX API"
        OAPI["REST API v1<br/>(apiKey header auth)"]
    end

    subgraph "Storage"
        BLOB_HL7["odx/hl7/{caseId}.hl7"]
        BLOB_PDF["odx/pdf/{caseId}/*.pdf"]
        TABLE["Cases table<br/>(odxPatientId, odxPatientTestId,<br/>odxReportId, etc.)"]
    end

    SYNC --> OAPI
    HL7 --> OAPI
    FHR --> OAPI
    UPLOAD --> OAPI

    HL7 --> BLOB_HL7
    FHR --> BLOB_PDF
    SYNC --> TABLE
    HL7 --> TABLE

    style OAPI fill:#fce4ec

OptimalDX Authentication

Field Source
apiKey Key Vault: OptimalDX-Prod-ApiKey (sent as header)
practiceId Key Vault: OptimalDX-Prod-PracticeID
Base URL Key Vault: OptimalDX-Prod-baseURL → normalized to {baseUrl}/v1

HL7 Message Structure (ORU^R01 v2.3)

Segment Content
MSH Sender: HOW, Receiver: OptimalDX
PID Patient ID (MR identifier)
OBR Order number, "RANDOX^Blood Test Results^L"
OBX (per analyte) Value type (NM/ST), code^name^99RDX, result, units, reference range, abnormal flag (H/L)

HbA1c Rule

  • Always use OptimalDX Element ID 781 (mmol/mol, IFCC standard)
  • Element ID 540 (%) is NOT used
  • No unit conversion performed

6. Data Flow — CRM / GoHighLevel (Deep Dive)

flowchart LR
    subgraph "HOW Backend"
        SEARCH["Search Contacts<br/>→ query, limit"]
        GET["Get Contact<br/>→ contactId"]
        CREATE_C["Create Contact<br/>→ name, email, phone,<br/>DOB, customFields"]
        UPDATE_C["Update Contact<br/>→ partial fields"]
        UPLOAD_F["Upload Attachment<br/>→ multipart PDF file"]
        EMAIL["Send Email<br/>→ type=Email, from, subject,<br/>html, attachments[]"]
        SMS["Send SMS<br/>→ type=SMS, message"]
    end

    subgraph "GoHighLevel API"
        GAPI["LeadConnector<br/>REST API v2021-07-28"]
    end

    subgraph "Portal"
        UI["Staff Portal<br/>(patient search, case management)"]
    end

    UI --> SEARCH
    UI --> CREATE_C
    SEARCH --> GAPI
    GET --> GAPI
    CREATE_C --> GAPI
    UPDATE_C --> GAPI
    UPLOAD_F --> GAPI
    EMAIL --> GAPI
    SMS --> GAPI

    style GAPI fill:#f3e5f5

CRM Authentication

Mode Config Secrets
OAuth2 (recommended) CrmAuthMode=oauth, CrmOAuthTokenUrl GHL-OAuth-ClientId, GHL-OAuth-ClientSecret, GHL-OAuth-RefreshToken
PIT (legacy) CrmAuthMode=pit GHL-PrivateToken

Custom Field Mappings

Field App Setting Purpose
Date of Birth CrmDobCustomFieldId Required for Randox orders
Biological Sex CrmBiologicalSexCustomFieldId Required for Randox orders
Athlete CrmAthleteCustomFieldId Optional: ODX report context

7. Data Storage Map

Table Storage

Table PartitionKey RowKey Purpose
Cases CrmLocationId CaseId (GUID) Core case workflow state, Randox/ODX mappings, artifact paths
ContactNotes CrmContactId NoteId (GUID) Free-text clinical notes per patient
Tags "tag" TagId (GUID) Case grouping/categorisation labels
SandboxClinicalBookings varies varies Sandbox booking audit log

Blob Storage (artifacts container)

Path Pattern Content Written By
randox/raw/{caseId}.json Raw Randox results JSON Poller / manual pull
randox/pdf/{caseId}/{filename}.pdf Randox report PDF (microbiome/NGS/health check) RandoxReportPdfService
randox/tof/{caseId}.pdf Test Order Form PDF (barcode label) RandoxTestOrderFormService
odx/hl7/{caseId}.hl7 HL7 ORU^R01 message submitted to ODX OdxResultsSubmissionService
odx/pdf/{caseId}/{filename}.pdf OptimalDX Functional Health Report PDF OdxReports

Key Vault Secrets

Category Secret Names
Randox Randox-Prod-BaseURL, Randox-Prod-Username, Randox-Prod-Password, Randox-Prod-ClientID, Randox-SubscriptionKey
OptimalDX OptimalDX-Prod-ApiKey, OptimalDX-Prod-PracticeID, OptimalDX-Prod-baseURL
CRM/GHL GHL-OAuth-ClientId, GHL-OAuth-ClientSecret, GHL-OAuth-RefreshToken, GHL-PrivateToken
Clinical Booking Randox-ClinicalBooking-SubscriptionKey, Randox-ClinicalBooking-ClientID

8. Automation & Polling

flowchart TD
    TIMER["Timer Trigger<br/>Every hour (0 0 */1 * * *)"] --> LOAD["Load all cases<br/>from Table Storage"]
    LOAD --> FILTER["Filter: has Randox order<br/>AND status 2/3 (in-flight)<br/>OR status 4+ (completion work remaining)"]

    FILTER --> STATUS_CHECK{"Status < 4?"}

    STATUS_CHECK -->|Yes| POLL["Call Randox GetOrderStatus"]
    POLL --> UPDATE_STATUS["Update case status in Table"]

    STATUS_CHECK -->|"No - Completed"| RESULTS{"Results<br/>already pulled?"}

    RESULTS -->|No| PULL["Pull results from Randox<br/>Store raw JSON in Blob"]
    RESULTS -->|Yes| NEXT

    PULL --> NEXT{"Panel type?"}

    NEXT -->|"Standard blood panel"| ODX_FLOW["Submit HL7 to OptimalDX<br/>Generate FHR PDF<br/>Email via CRM"]
    NEXT -->|"PDF-only panel"| RANDOX_PDF["Fetch Randox PDF<br/>Email via CRM"]

    ODX_FLOW --> CLEANUP["Delete cached TOF blob<br/>Update case timestamps"]
    RANDOX_PDF --> CLEANUP

Automation Configuration Flags

Flag Default Purpose
RandoxStatusPollEnabled true Enable/disable the hourly poller
AutoSubmitOdxOnRandoxResultsPulled true Auto-submit results to OptimalDX when Randox completes
AutoEmailOdxPatientReportOnRandoxCompleted false Auto-email patient report after Randox completes
AutoSubmitOdxOnRandoxResultsPulledIncludeManualLinks false Include manually-linked orders in auto-submit
AutoEmailOdxPatientReportOnRandoxCompletedIncludeManualLinks false Include manually-linked orders in auto-email

9. Authentication & Security

flowchart LR
    subgraph "User Auth"
        USER["Staff User"] -->|"Entra ID login"| SWA["Static Web App<br/>(built-in auth)"]
        SWA -->|"x-ms-client-principal<br/>(roles: How_Admin/Staff/ReadOnly)"| FUNC["Functions API"]
    end

    subgraph "Service Auth"
        FUNC -->|"DefaultAzureCredential<br/>(Managed Identity)"| KV["Key Vault"]
        FUNC -->|"DefaultAzureCredential"| STORAGE["Storage Account"]
        FUNC -->|"Bearer token<br/>(OAuth2 client credentials)"| RANDOX["Randox"]
        FUNC -->|"apiKey header"| ODX["OptimalDX"]
        FUNC -->|"Bearer token<br/>(OAuth2 refresh / PIT)"| GHL["GoHighLevel"]
    end

    subgraph "CI/CD Auth"
        GHA["GitHub Actions"] -->|"OIDC federation<br/>(Entra app registration)"| AZURE["Azure (deploy)"]
    end

Role Matrix

Role Portal Access API /api/* API /api/system/* API /api/diag/*
How_ReadOnly ✅ View only ✅ Read
How_Staff ✅ Full ✅ Read/Write
How_Admin ✅ Full ✅ Read/Write
anonymous /api/health only

10. CI/CD Pipeline

flowchart LR
    RELEASE["GitHub Release<br/>(published)"] --> PROD_API["deploy-api-prod.yml"]
    RELEASE --> PROD_PORTAL["deploy-portal-prod.yml"]
    RELEASE --> DOCS["deploy-docs.yml<br/>(if docs/** changed)"]
    PROD_API -->|"OIDC login"| PROD_FUNC["how-prod-api-func"]
    PROD_PORTAL -->|"SWA deploy token"| PROD_SWA["how-prod-portal"]
    DOCS -->|"SWA deploy token"| DOCS_SWA["how-prod-docs"]

    style PROD_FUNC fill:#c8e6c9
    style PROD_SWA fill:#c8e6c9
    style DOCS_SWA fill:#c8e6c9
Trigger Workflow Method
API deploy GitHub Release published OIDC (Entra federation, tag-scoped)
Portal deploy GitHub Release published SWA deploy token
Docs deploy Push to release/* branches SWA deploy token