Skip to main content

Patient Portal Service — Application Logic

Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · 03 platform-services · 02 DDD

1. Commands

CommandHandlerFR RefNotes
RegisterPortalAccountPortalAccountCommandHandlerFR-PORTAL-001Links patientId → Keycloak sub; triggers email verification
ActivatePortalAccountPortalAccountCommandHandlerFR-PORTAL-001Moves pending_verification → active after email/phone confirmation
SuspendPortalAccountPortalAccountCommandHandlerFR-PORTAL-009Admin-triggered; fires portal.account.suspended.v1
ReinstatePortalAccountPortalAccountCommandHandlerFR-PORTAL-009Admin re-activation
RequestAccountDeletionPortalAccountCommandHandlerFR-PORTAL-010Schedules close after data-retention check
GrantProxyDelegationProxyDelegationCommandHandlerFR-PORTAL-006Creates scoped ProxyDelegation; publishes event
RevokeProxyDelegationProxyDelegationCommandHandlerFR-PORTAL-006Moves delegation active → revoked
SubmitDemographicsUpdateDemographicsUpdateCommandHandlerFR-PORTAL-005Creates DemographicsUpdateRequest; routes to registration-service queue
SubmitAppointmentRequestAppointmentRequestCommandHandlerFR-PORTAL-003Forwards to scheduling-service; records portal access event
RequestFHIRExportExportCommandHandlerFR-PORTAL-011Dispatches async bundle export; publishes portal.export.requested.v1

2. Queries

QueryHandlerFR RefCacheNotes
GetPortalAccountPortalAccountQueryHandlerFR-PORTAL-001NoneReturns account status, MFA state, preferences
GetPatientSummaryPatientSummaryQueryHandlerFR-PORTAL-002Redis 60sAggregates Patient + active conditions (patient-chart-service)
GetChartSectionChartSectionQueryHandlerFR-PORTAL-002Redis 30sReturns FHIR bundle for section (allergies, meds, vitals, immunizations, problems)
GetLabResultsLabResultsQueryHandlerFR-PORTAL-007Redis 30sReturns released Observation resources from laboratory-service
GetRadiologyResultsRadiologyQueryHandlerFR-PORTAL-007Redis 30sReturns released DiagnosticReport resources from radiology-service
GetAppointmentsAppointmentQueryHandlerFR-PORTAL-003Redis 30sReturns upcoming + past Appointment resources from scheduling-service
GetMedicationsMedicationQueryHandlerFR-PORTAL-004Redis 60sReturns MedicationRequest resources from medication-service
GetCoverageAndEOBBillingQueryHandlerFR-PORTAL-008Redis 120sReturns Coverage + ExplanationOfBenefit from claims-service
GetImmunizationsImmunizationQueryHandlerFR-PORTAL-002Redis 120sReturns Immunization resources from immunizations-service
GetProxyDelegationsProxyDelegationQueryHandlerFR-PORTAL-006NoneReturns active and historical delegations for account
GetAccessLogAccessLogQueryHandlerFR-PORTAL-009NoneReturns immutable PortalAccessEvent records

3. Ports (Hexagonal Architecture)

PortDirectionAdapter
IPortalAccountRepositoryOutDrizzle ORM → PostgreSQL
IProxyDelegationRepositoryOutDrizzle ORM → PostgreSQL
IDemographicsRequestRepositoryOutDrizzle ORM → PostgreSQL
IAccessEventRepositoryOutDrizzle ORM → PostgreSQL (append-only)
IRegistrationServiceClientOutHTTP adapter → registration-service
ISchedulingServiceClientOutHTTP adapter → scheduling-service
ILaboratoryServiceClientOutHTTP adapter → laboratory-service
IRadiologyServiceClientOutHTTP adapter → radiology-service
IClaimsServiceClientOutHTTP adapter → claims-service
IMedicationServiceClientOutHTTP adapter → medication-service
IImmunizationsServiceClientOutHTTP adapter → immunizations-service
IPatientChartServiceClientOutHTTP adapter → patient-chart-service
IAIGatewayClientOutHTTP adapter → ai-gateway-service (Tier A only)
IPortalCacheOutRedis (ioredis)
IEventPublisherOutNATS JetStream via @ghasi/nats-client
IPushNotificationPortOutFCM/APNs adapter (mobile push)

4. Key Orchestration Flows

4.1 Patient Login + MFA Validation

4.2 Lab Result View with Release Policy Check

4.3 Appointment Request Submission

5. Policy Filters

All BFF query handlers apply the following policy checks before forwarding upstream requests:

CheckWhat it does
Scope enforcementJWT must contain the relevant SMART scope (e.g., patient/Observation.read)
Subject bindingsub claim must match the portal account's identityProviderSubject
Proxy scope checkIf acting as proxy, ProxyDelegation.scope must include the requested resource type
Result release policyLab/radiology results are fetched with ?releasePolicy=patient-visible; unreleased results are never returned to the patient
MFA enforcementRequests from accounts with mfaEnabled=true must have acr claim ≥ configured threshold
Account status checkSuspended or closed accounts return 403 ACCOUNT_NOT_ACTIVE

6. Outbox Pattern

All domain events (portal.*) are written atomically to the outbox table within the same Postgres transaction as the state mutation, then relayed to NATS JetStream by the outbox relay worker. This guarantees at-least-once delivery without dual-write failure risk.

7. Error Handling

ScenarioResponse
Upstream service unavailable (circuit open)503 UPSTREAM_UNAVAILABLE with retryAfterSeconds
Upstream returns stale data beyond TTLReturn cached version + X-Portal-Cache-Stale: true header
FHIR resource not found upstream404 RESOURCE_NOT_FOUND (do not leak internal error detail)
Scope check fail403 INSUFFICIENT_SCOPE
MFA not met403 MFA_REQUIRED
Module not licensed403 MODULE_NOT_LICENSED
Account suspended403 ACCOUNT_NOT_ACTIVE