Patient Portal Service — User Stories
Service: patient-portal-service Story prefix: PORTAL-US Last updated: 2026-04-18
Stories
PORTAL-US-001 — Register portal account
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient registers for portal account via email/phone verification |
| Epic link | PORTAL-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | portal-account, keycloak-patient-realm |
| FR references | FR-PORTAL-001 |
| Legacy FR refs | FR-PORT-001 |
| Dependencies | identity-service IDENT-US-001 |
User story: As a patient, when I visit the portal registration page, I want to create an account linked to my health record so that I can securely access my health information online.
Acceptance criteria (Gherkin):
- Given a valid patient MRN and email, when I submit the registration form, then a
PortalAccountis created withstatus = pending_verificationand a verification email is sent. - Given a verification link from the email, when I click it within 24 hours, then my account moves to
activestatus. - Given an expired or already-used verification link, when I click it, then
400 LINK_EXPIREDis returned and a new link can be requested.
Technical notes:
- Keycloak patient realm creates the user;
portal-accountis linked viaidp_subject(Keycloaksub). - Publishes
portal.account.created.v1.
Definition of Done:
- Unit + integration tests added; coverage ≥ thresholds.
- OpenAPI contract updated; Pact consumer tests green.
- Event schema registered; conformance test green.
- Telemetry spans/metrics added.
PORTAL-US-002 — Enroll and enforce MFA
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | MFA TOTP enrollment is mandatory before first portal access |
| Epic link | PORTAL-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, slice:S4 |
| Components | authentication, keycloak-patient-realm |
| FR references | FR-PORTAL-001 |
| Legacy FR refs | FR-PORT-001 |
| Dependencies | PORTAL-US-001 |
User story: As a patient, when I activate my portal account, I want to enroll a TOTP authenticator app so that my account is protected against unauthorized access.
Acceptance criteria (Gherkin):
- Given an account in
activestatus without MFA enrolled, when I try to access any portal resource, then I am redirected to MFA enrollment. - Given successful TOTP enrollment, when I log in, then I must enter a TOTP code to complete authentication.
- Given an incorrect TOTP code, when I submit, then
401 MFA_FAILEDis returned; after 5 failures the account is temporarily locked.
Technical notes:
- Keycloak TOTP policy configured;
mfaEnabledflag set onPortalAccountafter first successful TOTP login.
Definition of Done:
- Unit + integration tests added. E2E MFA enrollment test passing.
PORTAL-US-003 — Suspend and reinstate portal account
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Admin can suspend and reinstate a patient portal account |
| Epic link | PORTAL-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | portal-account, admin-api |
| FR references | FR-PORTAL-009 |
| Legacy FR refs | FR-PORT-009 |
| Dependencies | PORTAL-US-001 |
User story: As a portal administrator, when a patient account shows signs of unauthorized access, I want to suspend the account so that further access is immediately blocked.
Acceptance criteria (Gherkin):
- Given an active account, when admin calls suspend, then
status = suspendedandportal.account.suspended.v1is published. - Given a suspended account, when the patient tries to authenticate, then
403 ACCOUNT_NOT_ACTIVEis returned. - Given a suspended account, when admin calls reinstate, then
status = activeand account is accessible again.
Definition of Done: Standard DoD.
PORTAL-US-004 — Request account deletion
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient can request deletion of their portal account |
| Epic link | PORTAL-EPIC-01 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | portal-account, gdpr |
| FR references | FR-PORTAL-010 |
| Legacy FR refs | FR-PORT-010 |
| Dependencies | PORTAL-US-001 |
User story: As a patient, when I no longer wish to use the portal, I want to request deletion of my account so that my personal data is removed per data protection regulations.
Acceptance criteria (Gherkin):
- Given an active account, when I request deletion, then the account moves to
closedandportal.account.closed.v1is published. - Given a closed account, when the patient tries to log in, then
403 ACCOUNT_NOT_ACTIVEis returned. - Given a deletion request, when processed, then portal-owned PHI (demographics requests, access events older than retention period) is purged per data-retention policy.
Definition of Done: Standard DoD plus GDPR erasure verification.
PORTAL-US-005 — View patient summary and chart sections
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views health summary including allergies, meds, vitals, conditions |
| Epic link | PORTAL-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | chart-sections, fhir-bff, policy-filter |
| FR references | FR-PORTAL-002 |
| Legacy FR refs | FR-PORT-002 |
| Dependencies | registration-service, patient-chart-service, immunizations-service |
User story: As a patient, when I open my portal, I want to see a summary of my health information including active conditions, allergies, and medications so that I have an up-to-date picture of my health.
Acceptance criteria (Gherkin):
- Given an authenticated patient with SMART scope
patient/Patient.read, when I callGET /v1/portal/chart/summary, then a FHIR Bundle with Patient + conditions + allergies is returned. - Given a chart section request for
allergies, when called with valid scope, then AllergyIntolerance resources are returned in a searchset Bundle. - Given an invalid section name, when called, then
400 INVALID_SECTIONis returned.
Technical notes:
- Results cached in Redis (30s). Cache TTL re-evaluated on upstream
concept.updatedevents. - Access events appended for each section view.
Definition of Done: Standard DoD.
PORTAL-US-006 — View released lab results
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views only clinician-released lab results |
| Epic link | PORTAL-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | lab-results-bff, release-policy |
| FR references | FR-PORTAL-007 |
| Legacy FR refs | FR-PORT-007 |
| Dependencies | laboratory-service |
User story: As a patient, when I navigate to the Results section, I want to see only lab results that my care team has authorized for release so that I'm not confused by unreleased or preliminary findings.
Acceptance criteria (Gherkin):
- Given released Observation resources in laboratory-service, when I call
GET /v1/portal/results/lab, then onlypatient-visibleobservations are returned. - Given unreleased observations exist, when called, then unreleased resources are excluded from the response.
- Given a
portal.result.viewed.v1event, when generated, then it includesresourceType=Observationand correctresourceId.
Definition of Done: Standard DoD plus release-policy regression test suite green.
PORTAL-US-007 — View released radiology reports
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views only released imaging/radiology reports |
| Epic link | PORTAL-EPIC-02 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | radiology-results-bff, release-policy |
| FR references | FR-PORTAL-007 |
| Legacy FR refs | FR-PORT-007 |
| Dependencies | radiology-service |
User story: As a patient, when I navigate to the Imaging section, I want to see radiology reports that have been released by my radiologist so that I can review my imaging results.
Acceptance criteria (Gherkin):
- Given released DiagnosticReport resources, when I call
GET /v1/portal/results/radiology, then onlypatient-visiblereports are returned. - Given an unreleased report, when called, then it is absent from the response.
Definition of Done: Standard DoD.
PORTAL-US-008 — View appointments
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views upcoming and past appointments |
| Epic link | PORTAL-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | appointment-bff |
| FR references | FR-PORTAL-003 |
| Legacy FR refs | FR-PORT-003 |
| Dependencies | scheduling-service |
User story: As a patient, when I open my portal, I want to see my upcoming and past appointments so that I can track my care schedule.
Acceptance criteria (Gherkin):
- Given Appointment resources in scheduling-service, when I call
GET /v1/portal/appointments?status=booked, then upcoming appointments are returned as a FHIR Bundle. - Given no appointments, when called, then an empty searchset Bundle is returned.
Definition of Done: Standard DoD.
PORTAL-US-009 — Request new appointment
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient submits an appointment request to scheduling |
| Epic link | PORTAL-EPIC-03 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | appointment-bff, scheduling-client |
| FR references | FR-PORTAL-003 |
| Legacy FR refs | FR-PORT-003 |
| Dependencies | scheduling-service SCHED-EPIC-01 |
User story: As a patient, when I want to see a provider, I want to request an appointment from the portal so that I don't have to call the clinic.
Acceptance criteria (Gherkin):
- Given valid preferred provider, facility, and time, when I submit
POST /v1/portal/appointments/request, then201withrequestIdandstatus: pendingis returned. - Given scheduling-service is unavailable, when I submit a request, then
503 UPSTREAM_UNAVAILABLEis returned and no partial state is created. - Given a successful request, when processed, then
portal.appointment.requested.v1is published.
Definition of Done: Standard DoD.
PORTAL-US-010 — View medication list
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views active and historical medication requests |
| Epic link | PORTAL-EPIC-04 |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | medication-bff |
| FR references | FR-PORTAL-004 |
| Legacy FR refs | FR-PORT-004 |
| Dependencies | medication-service |
User story: As a patient, when I open my portal, I want to see my current and past medications so that I can track what I've been prescribed.
Acceptance criteria (Gherkin):
- Given MedicationRequest resources in medication-service, when I call
GET /v1/portal/medications?status=active, then active medications are returned.
Definition of Done: Standard DoD.
PORTAL-US-011 — Request medication refill
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient requests a prescription refill from the portal |
| Epic link | PORTAL-EPIC-04 |
| Status | To Do |
| Priority | Should |
| Story points | 5 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | medication-bff, refill-request |
| FR references | FR-PORTAL-004 |
| Legacy FR refs | FR-PORT-004 |
| Dependencies | medication-service MED-EPIC-01 |
User story: As a patient with an existing prescription, when my medication is running low, I want to submit a refill request from the portal so that I can get a new supply without an office visit.
Acceptance criteria (Gherkin):
- Given an active
MedicationRequest, when I callPOST /v1/portal/medications/{id}/refill, then201withrefillRequestIdis returned. - Given a non-active medication, when I request a refill, then
400 INVALID_REQUESTis returned.
Definition of Done: Standard DoD.
PORTAL-US-012 — Grant proxy delegation
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient grants a proxy or caregiver scoped access to their portal |
| Epic link | PORTAL-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | proxy-delegation |
| FR references | FR-PORTAL-006 |
| Legacy FR refs | FR-PORT-006 |
| Dependencies | PORTAL-US-001 |
User story: As a parent, when I need to manage my child's health records, I want to be granted proxy access to their portal account so that I can view their results and appointments on their behalf.
Acceptance criteria (Gherkin):
- Given valid proxy account ID and scope, when I call
POST /v1/portal/proxy/delegations, thenProxyDelegationcreated withstatus=activeandportal.proxy.delegation.granted.v1published. - Given a duplicate delegation (same grantor + proxy), when submitted, then
409 DELEGATION_ALREADY_EXISTSis returned. - Given a proxy session using an expired delegation, when they try to access data, then
403 PROXY_SCOPE_EXCEEDEDis returned.
Definition of Done: Standard DoD plus proxy isolation E2E test green.
PORTAL-US-013 — Revoke proxy delegation
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient revokes a proxy delegation immediately |
| Epic link | PORTAL-EPIC-05 |
| Status | To Do |
| Priority | Must |
| Story points | 2 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | proxy-delegation |
| FR references | FR-PORTAL-006 |
| Legacy FR refs | FR-PORT-006 |
| Dependencies | PORTAL-US-012 |
User story: As a patient, when I want to remove a caregiver's access, I want to revoke their delegation immediately so that they can no longer access my records.
Acceptance criteria (Gherkin):
- Given an active delegation, when I call
DELETE /v1/portal/proxy/delegations/{id}, thenstatus=revokedandportal.proxy.delegation.revoked.v1published. - Given a revoked delegation, when the proxy tries to access data, then
403 PROXY_SCOPE_EXCEEDEDis returned immediately (not cached).
Definition of Done: Standard DoD.
PORTAL-US-014 — Request PHR export
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient requests a FHIR Bundle export of their health record |
| Epic link | PORTAL-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 8 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S4 |
| Components | export-job, fhir-bundle-assembly |
| FR references | FR-PORTAL-011 |
| Legacy FR refs | FR-PORT-011 |
| Dependencies | PORTAL-EPIC-02 |
User story: As a patient, when I want to share my health records with another provider, I want to download my full health record as a FHIR file so that I can take my data with me.
Acceptance criteria (Gherkin):
- Given an authenticated patient, when I call
POST /v1/portal/export, then202withexportJobIdis returned. - Given the export job completes, when I poll the status endpoint, then
status=completeand a time-limiteddownloadUrlis returned. - Given an export job in progress, when I submit a new export request, then the existing in-progress job ID is returned (idempotent).
Definition of Done: Standard DoD plus GDPR portability validation.
PORTAL-US-015 — Download export bundle
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient downloads a completed PHR export via time-limited URL |
| Epic link | PORTAL-EPIC-06 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, slice:S4 |
| Components | export-job, object-storage |
| FR references | FR-PORTAL-011 |
| Dependencies | PORTAL-US-014 |
User story: As a patient, when my export is complete, I want to download my health record file so that I can store or share it securely.
Acceptance criteria (Gherkin):
- Given a completed export job, when I access the
downloadUrl, then the FHIR ndjson file is returned. - Given an expired download URL (24h TTL), when accessed, then
404 NOT_FOUNDis returned and a new export must be requested. - Given a download URL, when accessed from a different tenant session, then access is denied.
Definition of Done: Standard DoD.
PORTAL-US-016 — Enable AI navigation assistant
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Tenant admin enables AI navigation assistant for patient portal |
| Epic link | PORTAL-EPIC-07 |
| Status | To Do |
| Priority | Could |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, slice:S4 |
| Components | ai-navigation, feature-flag |
| FR references | FR-PORTAL-013 |
| Legacy FR refs | FR-AI-007 |
| Dependencies | ai-gateway-service AIGW-EPIC-01 |
User story: As a tenant administrator, when I want to improve patient engagement, I want to enable the AI navigation assistant so that patients can ask natural language questions about the portal.
Acceptance criteria (Gherkin):
- Given
ai.patient-assistantfeature flag enabled for tenant, when patient callsPOST /v1/portal/ai/navigate, then a navigation reply is returned. - Given feature flag disabled, when the endpoint is called, then
403 FEATURE_NOT_ENABLEDis returned. - Given ai-gateway-service unavailable, when called, then
503returned; rest of portal unaffected.
Definition of Done: Standard DoD plus PHI-in-prompt unit test green.
PORTAL-US-017 — Portal navigation via AI assistant
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient uses natural language to navigate to the right portal section |
| Epic link | PORTAL-EPIC-07 |
| Status | To Do |
| Priority | Could |
| Story points | 5 |
| Labels | service:patient-portal-service, type:api, slice:S4 |
| Components | ai-navigation |
| FR references | FR-PORTAL-013 |
| Dependencies | PORTAL-US-016 |
User story: As a patient, when I'm unsure where to find my lab results, I want to ask the portal assistant in my own words so that I'm guided to the right section quickly.
Acceptance criteria (Gherkin):
- Given a navigation question ("Where are my blood tests?"), when I call
POST /v1/portal/ai/navigate, then a reply withnavigationHintpointing to/v1/portal/results/labis returned. - Given a clinical question ("Do I have diabetes?"), when submitted, then the moderation filter intercepts and returns the fallback message.
- Given more than 10 requests in a session, when submitted, then
429 RATE_LIMITEDis returned.
Definition of Done: Standard DoD plus moderation boundary test cases green.
PORTAL-US-018 — View insurance coverage and EOB
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Patient views active insurance coverage and explanation of benefits |
| Epic link | PORTAL-EPIC-08 |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:patient-portal-service, type:backend, type:api, slice:S2 |
| Components | billing-bff, claims-client |
| FR references | FR-PORTAL-008 |
| Legacy FR refs | FR-PORT-008 |
| Dependencies | claims-service CLAIMS-EPIC-01 |
User story: As a patient, when I want to understand my insurance coverage, I want to view my active coverage details and past explanation of benefits so that I can verify what costs my insurer covers.
Acceptance criteria (Gherkin):
- Given Coverage resources in claims-service, when I call
GET /v1/portal/billing/coverage, then active coverage resources are returned. - Given EOB resources, when I call
GET /v1/portal/billing/eob, then EOBs are returned paginated. - Given claims-service unavailable, when called, then cached billing data is returned with a degraded banner.
Definition of Done: Standard DoD.