Business Flows
Core business flows for Lab Forms — config creation, consent lifecycle, value sync, AOE updates, MIS export, and renderer config
👤 Ritu Kataria📅 Updated: Mar 13, 2026🏷️ feature
Core Business Flows
Form Configuration Creation
Admin → POST config/new
├─ Validate form_type, lab, process_type
├─ Validate linked instances (if instance-dependent type)
├─ Check for duplicate processes
├─ Validate TTL (consent only, Patient type only)
├─ BEGIN TRANSACTION
│ ├─ Save Process
│ ├─ Bulk create/update SubProcesses
│ │ ├─ Validate names, communication flags
│ │ ├─ Check duplicates
│ │ └─ Create LinkedSubProcess entries with sequence
│ ├─ Bulk create/update Questions
│ │ ├─ Validate field_type, question_code
│ │ ├─ Validate & prepare attributes (options, skip logic, prefilling, etc.)
│ │ └─ Create QuestionAttribute entries
│ ├─ Bulk create ProcessLinkedInstances (if applicable)
│ └─ Bulk disable removed questions
├─ COMMIT
└─ Update cache (form config + renderer config)Patient Consent Creation
Billing event → POST patient-consent/<bill_id>/new
├─ Validate lab_id, patient_id, bill_id
├─ Find applicable processes:
│ ├─ Patient-type processes (check TTL expiry across all patient bills)
│ ├─ Bill-type processes
│ └─ Test/Profile-type processes (matched via billingInfo test IDs → ProcessLinkedInstances)
├─ Create LabForm (form_mode=Online, form_status=Pending)
├─ After save:
│ ├─ Link processes → create LabFormLinkedProcesses entries
│ ├─ Generate QR code → upload to S3
│ └─ Send consent email to patient (if feature enabled)
└─ Return serialized LabFormQuestion Value Capture (Sync)
For instance-dependent process types (for example Test, Profile, and Promotion), values are always persisted per lab report. Because reports are test-based, stored values are written against each applicable test's report ID.
In practice:
- If a profile is added to billing and AOE is configured at profile level, AOE may be captured once, but values are saved separately for each test/report under that profile.
- If there is no direct AOE mapping on the profile, but individual tests inside that profile have AOE mapping, values are captured only for those mapped tests and saved against their respective report IDs.
The same behavior applies to Consent and Promotion process types.
Mobile/Web → POST questions/values/bulk
├─ Validate lab_id, bill_id, values[]
├─ Validate process IDs belong to lab
├─ Validate report IDs (for test-level processes)
├─ For each value:
│ ├─ Validate question_id, process_id
│ ├─ Resolve subprocess_sequence from LinkedSubProcess
│ ├─ Prepare QuestionValue instance
│ └─ Track communication questions (email, sms, whatsApp, fax, address)
├─ Delete previous values (per delete_filters, default: bill_id)
├─ Bulk create QuestionValue records
├─ After bulk create:
│ └─ Update PatientConsent flags (if consent form with communication questions)
└─ Return created valuesQuestion Value Update (AOE)
Web → PUT <bill_id>/questions/values/bulk/update
├─ Allowed only for 'aoe' form_type
├─ Separate values into create vs update batches
├─ For creates: check if matching value already exists → convert to update
├─ For updates: load existing, compare values, track diffs
├─ Bulk update changed values
├─ Bulk create new values
├─ Generate diff report (old value → new value per question)
└─ Log activity with diff JSONConsent Revocation
Admin → POST patient-consent/<lab_form_id>/revoke
├─ Load LabForm
├─ BEGIN TRANSACTION
│ ├─ Set form_status = "Revoked"
│ ├─ Mark all LabFormLinkedProcesses as is_revoked=True
│ ├─ Identify communication question values
│ ├─ Revoke PatientConsent flags for affected channels
│ ├─ Send revocation email (if enabled)
│ └─ Log activity
└─ COMMITAdd Test to Existing Consent
POST patient-consent/<bill_id>/add-test
├─ Get existing LabForm for bill
├─ Get test_ids to add
├─ Find Test/Profile processes linked to those test_ids via ProcessLinkedInstances
├─ Exclude already-linked processes
├─ Exclude expired patient-level processes
├─ Link new processes to existing LabForm
└─ Return updated LabFormMIS Export
GET values?from=<timestamp>&to=<timestamp>
├─ Validate date range (max 1 month)
├─ Execute raw SQL query joining:
│ QuestionValue → Process → Question → SubProcess → userDetails → billing → allTests
├─ For branch-scoped users: filter via ES patient branch lookup
└─ Return flat row dataForm Renderer Config
GET renderer/config?process_ids=...
├─ Load processes with linked subprocesses and questions
├─ Prepare question renderer fields:
│ ├─ Map field_type → component type (Text, Select, CheckBox, etc.)
│ ├─ Apply attributes (options, validators, skip conditions, prefilling)
│ └─ Build subprocess → question order mapping
├─ Build process → subprocess order mapping
└─ Return config JSON for frontend form engineAdditional Patient Info — Configuration
Admin → POST config/new (form_type="additional_patient_info")
├─ Process.validate() auto-sets process_type = "Patient"
├─ Limited to 1 subprocess per process (enforced in Process.after_save)
├─ Create Process + SubProcess + Questions + Attributes
└─ Update cache (form config + renderer config)
Admin → POST additional-patient-info/settings/bulk-create
├─ Validate process IDs (must be active additional_patient_info forms)
├─ Validate page type (appointment | registration | home_collection | cc_registration)
├─ Check custom_page validity (if custom registration page)
├─ Check for duplicate settings (same lab/page/process)
├─ Bulk create AdditionalPatientInfoSettings
└─ Set default process per page (if specified)Additional Patient Info — Data Capture
Patient registration/update → UserDetails.after_save()
├─ Calls handle_additional_patient_info(payload, session)
├─ Payload: {"additional_patient_info": {"values": {process_id: {question_id: value}}, "comments": "..."}}
│
├─ NEW patient (is_new_instance=True):
│ ├─ AdditionalPatientInfo.bulk_create()
│ ├─ Validates all process IDs exist and are active additional_patient_info forms
│ ├─ Creates one AdditionalPatientInfo row per (process, question, patient, value)
│ └─ Logs activity with category_id=977
│
└─ EXISTING patient:
├─ AdditionalPatientInfo.bulk_update()
├─ Fetches existing records for the patient
├─ If no existing records → falls through to bulk_create
├─ If records exist → updates changed values, creates new ones
├─ Tracks diffs (old_value vs new_value) for activity logging
└─ Logs with category_id=978 and diff JSONAdditional Patient Info — Data Retrieval
Two retrieval patterns:
Simple (for edit forms): GET /additional-patient-info/<patient_id>
Returns: {process_id: {question_id: value}}
└─ Flat dict of current values
└─ Used by registration/edit UI to pre-populate form fieldsDetailed (for display/PDF): GET /additional-patient-info/<lab_id>/<patient_id>?with_attributes=1
Returns: [{id, name, icon, description, questions: [{...}]}]
├─ _build_process_config() reconstructs full form structure
├─ _get_questions_with_value() serializes questions via QuestionSerializer
├─ Pre-signed S3 URLs generated for file/signature/camera field types
└─ Includes edited_by, comments, updated_at per questionAdditional Patient Info — MIS Export
GET values?from=<timestamp>&to=<timestamp>&form_type=additional_patient_info
├─ LabFormMISView dispatches to AdditionalPatientInfo.get_form_values()
├─ Converts millisecond timestamps to seconds (AdditionalPatientInfo uses seconds, QuestionValue uses ms)
├─ get_form_values_query() builds raw SQL:
│ ├─ JOINs AdditionalPatientInfo → Process → userDetails → Question → SubProcess
│ ├─ Does NOT join to billing/LabForm tables (unlike QuestionValue queries)
│ └─ Omits iteration and form_status columns (not applicable)
├─ For branch-scoped users: filters via Elasticsearch user_details index
└─ Returns branch-annotated flat row dataAdditional Patient Info — Labcorp Integration
Labcorp order submission → integration/Labcorp/views.py
├─ Reads AdditionalPatientInfo for the patient
├─ Extracts specific question values by question text matching:
│ ├─ "gender identities" → Gender identity
│ ├─ "sexual orientation" → Sexual orientation
│ ├─ "patient class" → Patient class
│ └─ "courtesy_copy" → Courtesy copy preferences
└─ Maps values into Labcorp order payload