Design Decisions
Key design constraints, architectural rationale, and extensibility guide for the Lab Forms module
Design Decisions & Architecture
Key Design Decisions & Constraints
Preset System
Presets are "global template" processes with lab_id=NULL and process_type=Preset. They serve as starting points that can be cloned into lab-specific configs. Creation is restricted to a hardcoded list of allowed account manager emails.
Communication Subprocess (Consent Only)
A special subprocess type (for_communication=True) is only valid on consent forms. Questions within it must use reserved question codes. When values are captured, the system automatically updates PatientConsent flags (email, SMS, WhatsApp, fax, mail) with TTL-based expiry.
TTL / Expiry (Consent Only)
Only Patient process type on consent forms supports TTL. Expiry is calculated from created_at using ttl value and ttl_mode (days/months/years). Expired patient-level processes are excluded when creating consent for subsequent bills.
Iteration Support
Processes with requires_pdf_iterations=True support multiple iterations of responses. Each QuestionValue carries an iteration field. PDF generation can produce per-iteration documents.
Offline/Mobile Sync
The system supports offline mobile data collection with:
BulkQuestionValueViewfor batch syncBulkQuestionValueSyncFailureViewfor logging sync failures to ElasticsearchGuestBulkQuestionValuefor unauthenticated (patient portal) submissions
Legacy instance_id on Process
The Process.instance_id column is being deprecated in favor of the ProcessLinkedInstances table. A TODO comment in the codebase marks this for removal.
Branch Scoping
For multi-branch labs:
- Lab form history is filtered by
bill.branch_id - MIS export uses Elasticsearch to resolve patient → branch mapping
- Bill-level form details check branch authorization
Email Notifications (Consent)
Patient consent emails are sent on:
created— new consent formresend— manual resendlinked_process— new processes addedrevoked— consent revoked
Emails include a QR code (uploaded to S3) linking to the patient portal consent form. Email sending requires both consent_management feature flag and patient_consent_email_communication lab setting to be enabled.
Additional Patient Info — Patient-Scoped (Not Bill-Scoped)
Unlike aoe and consent which store captured data in QuestionValue (keyed by bill_id), additional_patient_info stores data in its own AdditionalPatientInfo table keyed by patient_id. This is because patient demographic/clinical info (gender identity, sexual orientation, etc.) persists across bills and should be captured once per patient, not once per billing event.
Consequences:
- No
LabFormruntime entity —additional_patient_infodoes not createLabFormorLabFormLinkedProcessesrecords. Data flows directly from the UI toAdditionalPatientInfoviaUserDetails.handle_additional_patient_info(). - No iteration support — since data is patient-level, there is no concept of multiple iterations.
- No form_status — there is no pending/completed/revoked lifecycle. Values are simply created or updated.
- Triggered on patient save — data capture is integrated into the patient registration/update flow (via
UserDetails.after_save()), not triggered by a separate form submission event.
Additional Patient Info — Single Subprocess Constraint
Processes with form_type="additional_patient_info" are limited to exactly one subprocess. This is enforced in Process.after_save() — the process_type is also auto-set to "Patient" during validation, regardless of what the caller provides.
Additional Patient Info — Page Settings & Defaults
AdditionalPatientInfoSettings provides a mapping layer between form processes and UI pages. Each page type (appointment, registration, home_collection, cc_registration) can have multiple processes assigned, but only one default per (lab, page, custom_page) combination. The default process cannot be disabled. Custom registration pages are supported via the custom_page FK, gated by the allow_registration_custom_pages session flag.
Additional Patient Info — Access Control
- Feature flag:
lab_forms_managementin session — required for all patient-facing and settings APIs - User flag:
user_additional_patient_info_history_flagonLabUser— controls access to view history (mapped toLAB_USER_REGISTRATION_ACCESSpermission group) - Custom page access:
allow_registration_custom_pagessession flag — required whencustom_page_idis specified in settings
Architectural Rationale
One Engine, Infinite Form Types
The core motivation behind the Lab Forms architecture is building a single, generalised form engine rather than coding each form type as a separate feature. Consent, AOE and Additional Patient Info all look very different to end users, but structurally they are the same thing: a hierarchy of Processes → SubProcesses → Questions → Attributes with answers captured as QuestionValues.
By abstracting this structure once, every new form type the business needs in the future is just a new form_type string — no new tables, no new views, no new serializers. The entire config + capture + history + export + print pipeline works automatically.
Configuration Over Code
The system follows a configuration-driven philosophy:
- Lab admins define forms entirely through the UI — no developer involvement needed to add a new question, change field order, toggle mandatory/hidden, or set up skip logic.
- Question Attributes (validators, prefilling, skip conditions, options, date constraints) are stored as key-value metadata, not as columns. This means new attribute types can be introduced without schema migrations.
- Presets provide global templates so CrelioHealth can ship best-practice form configurations out of the box while still letting each lab customise.
This dramatically reduces the cost of supporting 1000+ labs, each with different compliance and workflow requirements.
Entity-Agnostic Linking
The ProcessLinkedInstances + PROCESS_TYPE_MODEL_META_MAPPER pattern decouples form configuration from the specific entities it applies to. A single Process doesn't "know" whether it's attached to a Test, Profile, Sample, Promotion, Store, or Bill — the mapper resolves the correct Django model, lab-ID key, and validation rules at runtime.
This means:
- New linkable entity types can be added by adding one entry to the mapper dict.
- The same validation, caching, and serialization code path handles every entity type.
- Business rules (e.g., "many processes per test in AOE") are expressed as simple conditionals, not duplicated codepaths.
Separation of Config-Time and Runtime
The architecture cleanly separates two concerns:
| Concern | Entities | Who uses it |
|---|---|---|
| Config-time ("what should the form look like?") | Process, SubProcess, Question, QuestionAttribute, LinkedSubProcess, ProcessLinkedInstances | Lab admins, account managers |
| Runtime ("what did the user actually fill in?") | LabForm, LabFormLinkedProcesses, QuestionValue, PatientConsent | Phlebotomists, patients, lab technicians |
This separation means config changes don't corrupt existing captured data, and the same form definition can generate responses across thousands of bills without duplication of structure.
Robustness Highlights
- Transactional integrity — Form config creation/updates are wrapped in
@transaction.atomicso a failure mid-way through subprocess or question creation rolls back cleanly, preventing orphaned records. - Soft-delete everywhere — Processes, SubProcesses, and Questions use
is_disabledrather than hard deletion, preserving referential integrity with historical QuestionValues. - Duplicate guards — Every level (Process, SubProcess, Question) enforces uniqueness checks scoped appropriately (per lab, per form type, per subprocess) to prevent configuration errors.
- Skip logic & conditional flows — The attribute-based skip system supports complex branching (jump to another process, abort, restart with value retention) without hard-coding any workflow paths.
- Offline-first design — Bulk sync endpoints, failure logging to Elasticsearch, and iteration support ensure mobile data collection works reliably even with intermittent connectivity.
- Branch-aware multi-tenancy — The same lab form infrastructure supports both single-location and multi-branch labs with proper data isolation via branch scoping.
Reusability & Extensibility
| What you can do | How |
|---|---|
| Add a completely new form type | Add a string to lab_form_types tuple — all APIs, caching, MIS export, and history views work immediately |
| Add a new question field type | Add to field_types tuple + optionally add a renderer mapping in FIELD_TYPE_MAPPER and a validation function in QuestionAttribute |
| Add a new linkable entity | Add one entry to PROCESS_TYPE_MODEL_META_MAPPER and optionally to instance_id_dependent_process_types |
| Add a new question attribute | Add to the attribute type lists and optionally implement a field-specific validator |
| Add new skip-to behaviours | Add to ALLOWED_SKIP_TO_TYPES list and handle in the renderer |
| Add a new prefilling source | Add to VALID_PREFILLING_SOURCES and implement the field resolver |
| Ship a default form for all labs | Create a Preset process (lab_id=NULL, process_type=Preset) |
Why Not a Generic Form Builder Library?
While third-party form builders exist, this custom implementation was chosen because:
- Deep integration with domain entities — Forms need to link to tests, samples, profiles, bills, promotions, and home collections with lab-specific validation. No generic library handles this.
- LIMS-specific workflows — Skip logic that aborts a process and moves to the next sample, or restarts with retained values, is domain-specific branching that generic form engines don't support.
- Communication consent lifecycle — The tight coupling between consent form answers and
PatientConsentflags with TTL-based expiry is a compliance requirement unique to healthcare. - Offline-first mobile sync — The bulk value capture with iteration support, failure recovery, and per-subprocess sequencing was designed specifically for phlebotomists working in the field.
- Performance at scale — Redis-cached config, raw SQL for MIS export, and per-process hash caching are tuned for the platform's scale requirements — something a generic solution would require heavy customisation to achieve.
- Full audit trail — Every config change and value capture is activity-logged with category-level granularity, which is critical for lab accreditation and compliance.