Immunizations Service — Application Logic
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template
1. Commands
| Command | Use case class | Key steps | Roles |
|---|---|---|---|
RecordAdministrationCommand | RecordAdministrationUseCase | Validate patient, resolve vaccine code, check contraindications, persist record, refresh forecast, emit immunization.recorded | CLINICIAN, NURSE, VACCINATION_OFFICER |
RecordRefusalCommand | RecordRefusalUseCase | Validate patient, persist not-done record with refusal reason, emit immunization.refused | CLINICIAN, NURSE, VACCINATION_OFFICER |
AmendImmunizationCommand | AmendImmunizationUseCase | Optimistic lock check, validate allowed amendment fields (lot, site, route, notes), persist, emit immunization.updated | CLINICIAN, VACCINATION_OFFICER |
CorrectImmunizationCommand | CorrectImmunizationUseCase | Transition to entered-in-error, audit previous state, emit immunization.updated | CLINICIAN, ADMIN |
ImportHistoricalRecordCommand | ImportHistoricalRecordUseCase | Accept source reference, persist with status historical, trigger forecast refresh, emit immunization.reported | CLINICIAN, DATA_MIGRATION_ROLE |
AddContraindicationCommand | AddContraindicationUseCase | Persist ContraindicationRecord, invalidate forecast for affected antigen | CLINICIAN |
RemoveContraindicationCommand | RemoveContraindicationUseCase | Mark contraindication inactive, refresh forecast | CLINICIAN |
RefreshForecastCommand | RefreshForecastUseCase | Load EPI schedule, load patient records, compute recommendations, persist forecast, emit forecast.updated | System (internal), CLINICIAN |
SyncToRegistryCommand | SyncToRegistryUseCase | Build RegistrySyncJob, write to outbox, interop-service sends VXU | System (scheduled), ADMIN |
2. Queries
| Query | Use case class | Description | Roles |
|---|---|---|---|
GetImmunizationByIdQuery | GetImmunizationUseCase | Single record by immunizationId | CLINICIAN, NURSE, PATIENT (own) |
ListPatientImmunizationsQuery | ListPatientImmunizationsUseCase | All records for a patient; filterable by status, vaccine code, date range | CLINICIAN, NURSE, PATIENT (own) |
GetForecastQuery | GetForecastUseCase | Current forecast for a patient | CLINICIAN, NURSE, PATIENT (own) |
ListDefaultersQuery | ListDefaultersUseCase | Patients with overdue doses; filterable by facility, antigen, days overdue | VACCINATION_OFFICER, ADMIN |
GetCoverageReportQuery | GetCoverageReportUseCase | Population coverage by antigen and facility; date-range aggregation | ADMIN, ANALYST |
GetImmunizationCertificateQuery | GetImmunizationCertificateUseCase | Generate signed JWT certificate for patient | CLINICIAN, PATIENT (own) |
SearchImmunizationsQuery | SearchImmunizationsUseCase | Full-text + filter search across tenant | CLINICIAN, ADMIN |
GetRegistrySyncStatusQuery | GetRegistrySyncStatusUseCase | Sync job status and last sync timestamp | ADMIN |
3. Sequence Diagrams
3.1 Record Administration
Clinician → API: POST /v1/immunizations { patientId, vaccineCode, doseNumber, lotNumber, administeredAt, ... }
API → RecordAdministrationUseCase: execute(command)
RecordAdministrationUseCase → PatientPort: assertPatientActive(patientId)
RecordAdministrationUseCase → EpiSchedulePort: resolveVaccineCode(vaccineCode)
RecordAdministrationUseCase → ContraindicationRepository: findActive(patientId, vaccineCode)
alt contraindicated and no override
RecordAdministrationUseCase → API: throw ContraindicationViolationError (422)
end
RecordAdministrationUseCase → ImmunizationRepository: save(record) [+ outbox entry in transaction]
RecordAdministrationUseCase → RefreshForecastUseCase: enqueue(patientId)
OutboxRelay → NATS: IMMUNIZATIONS.immunization.recorded
NATS → communication-service: appointment reminder suppression update
NATS → analytics-service: coverage metric update
API → Clinician: 201 { immunizationId, status: "completed", ... }
3.2 Forecast Refresh
Trigger: RecordAdministrationUseCase OR scheduled cron (nightly)
RefreshForecastUseCase → EpiSchedulePort: loadSchedule(tenantId)
RefreshForecastUseCase → ImmunizationRepository: findAll(patientId, status: [completed, not-done])
RefreshForecastUseCase → ContraindicationRepository: findActive(patientId)
RefreshForecastUseCase → ForecastEngine: compute(records, contraindications, schedule)
ForecastEngine → RefreshForecastUseCase: recommendations[]
RefreshForecastUseCase → ForecastRepository: upsert(forecast) [+ outbox entry]
OutboxRelay → NATS: IMMUNIZATIONS.forecast.updated
NATS → communication-service: defaulter notification trigger
3.3 Registry Sync
Scheduler (cron) → SyncToRegistryUseCase: execute({ since: lastSyncAt })
SyncToRegistryUseCase → ImmunizationRepository: findModifiedSince(lastSyncAt)
SyncToRegistryUseCase → RegistrySyncRepository: createJob(records)
SyncToRegistryUseCase → OutboxTable: insert VXU sync command [in transaction]
OutboxRelay → NATS: IMMUNIZATIONS.registry.sync-requested
interop-service → NationalRegistry: HL7 VXU batch
NationalRegistry → interop-service: ACK / NACK
interop-service → NATS: IMMUNIZATIONS.registry.sync-completed
SyncToRegistryUseCase (listener) → RegistrySyncRepository: markComplete / markFailed
4. Ports (Hexagonal Boundaries)
| Port name | Direction | Purpose |
|---|---|---|
ImmunizationRepositoryPort | Outbound | Persist and query ImmunizationRecord aggregates |
ForecastRepositoryPort | Outbound | Persist and query ImmunizationForecast |
ContraindicationRepositoryPort | Outbound | Persist and query ContraindicationRecord |
EpiSchedulePort | Outbound | Load EPI national schedule from terminology-service or config |
ForecastEnginePort | Internal | Compute recommendations from schedule + records |
PatientPort | Outbound | Assert patient active status (calls registration-service) |
OutboxPort | Outbound | Write events to outbox table atomically |
EventPublisherPort | Outbound | NATS JetStream publisher (used by outbox relay) |
RegistrySyncPort | Outbound | Enqueue registry sync jobs |
CertificatePort | Outbound | Sign JWT vaccination certificates |
5. Outbox and Event Patterns
- All state-changing use cases write domain events to the
outboxtable within the same DB transaction as the aggregate mutation. - The outbox relay polls
WHERE status = 'pending', publishes to NATS JetStream, then marksdelivered. - Events are idempotent: downstream consumers must deduplicate on
eventId. RefreshForecastUseCaseis enqueued asynchronously after record creation to avoid blocking the HTTP response. Uses BullMQ queue backed by Redis.- Registry sync uses a separate scheduled job (cron expression configurable) with an exponential-backoff retry policy (max 5 retries).
6. Error Handling
| Error | HTTP code | Domain code |
|---|---|---|
| Patient not found | 404 | PATIENT_NOT_FOUND |
| Patient deceased | 422 | PATIENT_DECEASED |
| Invalid vaccine code | 422 | INVALID_VACCINE_CODE |
| Active contraindication | 422 | CONTRAINDICATION_ACTIVE |
| Optimistic lock conflict | 409 | OPTIMISTIC_LOCK_CONFLICT |
| Record already entered-in-error | 409 | IMMUNIZATION_CORRECTED |
| EPI schedule not configured | 503 | EPI_SCHEDULE_UNAVAILABLE |
| Registry sync failure | 503 | REGISTRY_SYNC_FAILED |
| Tenant not entitled | 403 | MODULE_NOT_ENTITLED |