Facility Service — User Stories
Service: facility-service Story prefix: FAC-US Last updated: 2026-04-17
Stories
FAC-US-001 — Create hierarchy node validated against profile
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Admin creates a ward node validated against active profile |
| Epic link | FAC-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:facility-service, type:backend, slice:S0 |
| Components | hierarchy |
| FR references | FR-FAC-HIER-001 |
| Legacy FR refs | FR-HIER-NODE-001, FR-HIER-NODE-002 |
| Dependencies | — |
User story: As a tenant facility admin, when I need to model a new ward, I want to create a HierarchyNode that is validated against my tenant's active profile, so that I cannot accidentally create an invalid structure.
Acceptance criteria (Gherkin):
- Given my tenant has
AFG_MOPHprofile active, when I POST a node withtype=wardand a valid department parent, then 201 is returned andfacility.hierarchy_node.created.v1is published. - Given my profile does not allow
type=virtual-clinic, when I POST such a node, then 422HIERARCHY_NODE_TYPE_NOT_ALLOWEDis returned. - Given a root node already exists in my tenant, when I POST a second
mophroot, then 409HIERARCHY_MULTIPLE_ROOTSis returned.
Technical notes:
- Endpoint
POST /api/v1/hierarchy/nodes, handlerCreateHierarchyNodeHandler. - Validation runs against
profile.allowedNodeTypes. - Authorisation via
access-policy.evaluate(action: hierarchy.node:create). - Emits
facility.hierarchy_node.created.v1.
Definition of Done: Unit + integration tests, OpenAPI, event schema, Pact consumer (tenant admin UI), telemetry span, 80%+ coverage.
FAC-US-002 — Prevent cycles on contains edges
| Field | Value |
|---|---|
| Issue type | Story |
| Summary | Reject edge creation that would form a cycle |
| Epic link | FAC-EPIC-01 |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:facility-service, type:backend, slice:S0 |
| FR references | FR-FAC-HIER-005 |
| Legacy FR refs | FR-HIER-EDGE-003, BR-HIER-003 |
User story: As a tenant admin, when I link nodes, I want cycles on contains edges blocked so the hierarchy remains a DAG.
Acceptance criteria:
- Given
A contains B contains C, when I POST edge{parent:C, child:A, relationshipType:contains}, then 409HIERARCHY_CYCLE. - Given
A manages B, when I POST edge{parent:B, child:A, relationshipType:manages}, then 201 (cycles allowed on non-contains). - Given I POST a self-loop, then 400.
Technical notes:
- Recursive CTE in Postgres; p95 ≤ 50ms up to 1k descendants.
- Alert fires when cycle rejections > 50/min/tenant.
Definition of Done: as above + perf test.
FAC-US-003 — Profile-based seeding on tenant creation
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-01 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-HIER-007 |
| Legacy FR refs | FR-HIER-PROF-004 |
User story: As the platform, when a tenant is created, I want to seed a default profile and root node so the tenant is immediately usable.
Acceptance criteria:
- Given a
tenant.tenant.created.v1event withcountry=AF, when consumed, thenAFG_MOPHis set as default and a root MoPH node is created. - Given the country is unknown, then
CLINIC_SIMPLEis chosen.
Technical notes: OnTenantCreatedPolicy; inbox-dedup keyed on eventId.
FAC-US-004 — Subtree query with depth limit
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-01 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-TRV-001 |
| Legacy FR refs | FR-HIER-TRV-001, NFR-HIER-002 |
User story: As a downstream service, I want to fetch the subtree of any node up to a depth, so I can resolve scope without over-fetching.
Acceptance criteria:
- Given a subtree of 1,000 nodes,
GET /hierarchy/nodes/:id/subtree?maxDepth=3returns in ≤ 100 ms p95. - When
maxDepth > profile.constraints.maxDepth, return 422.
FAC-US-005 — Ancestor chain lookup (Licensing hot path)
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-01 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-TRV-002 |
| Legacy FR refs | FR-HIER-TRV-004 |
User story: As the Licensing Service, I want the ordered root→node ancestor chain so I can resolve effective licenses.
Acceptance criteria:
- Given cached context, response p99 ≤ 20 ms.
- When cache miss, DB fallback p95 ≤ 80 ms.
FAC-US-006 — Clone platform profile for tenant customisation
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-01 |
| Priority | Should |
| Story points | 3 |
| FR references | FR-FAC-PROF-002 |
| Legacy FR refs | FR-HIER-PROF-002 |
User story: As a tenant admin, I want to clone a platform profile and edit it for my needs.
Acceptance criteria:
- Given platform profile
PRIVATE_HOSPITAL, when I POST/hierarchy/profiles/:id/clone, then a new profile exists under my tenant with identical constraints. - Given I edit the cloned profile, existing nodes are not invalidated (warn only).
FAC-US-007 — Soft-deactivate hierarchy node
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-01 |
| Priority | Must |
| Story points | 2 |
| FR references | FR-FAC-HIER-004 |
| Legacy FR refs | FR-HIER-NODE-004, BR-HIER-004 |
User story: As an admin, I want to soft-deactivate a node (no cascade) so history is preserved.
Acceptance criteria: child nodes remain active; deactivated node hidden from default list; event emitted.
FAC-US-008 — Create location anchored to node
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-02 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-LOC-001 |
| Legacy FR refs | FR-FAC-001, FR-FAC-002 |
User story: As an admin, I want to create a Location anchored to exactly one HierarchyNode so no orphan locations exist.
Acceptance criteria:
- Given a missing
hierarchyNodeId, then 400LOCATION_NODE_REQUIRED. - Given an invalid/inactive node, then 404/409.
FAC-US-009 — Set location operational hours
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-02 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-LOC-002 |
| Legacy FR refs | FR-FAC-001 (hours) |
User story: As an admin, I want to set 7-day hours per location with time zone so scheduling can build slots.
Acceptance criteria: PUT replaces entire week; closed days allowed; emits facility.location.updated.v1.
FAC-US-010 — Deactivate location with occupied-bed safeguard
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-02 |
| Priority | Must |
| Story points | 5 |
| FR references | FR-FAC-LOC-004 |
| Legacy FR refs | BR-FAC-002 |
User story: As an admin, I should not be able to deactivate a location while beds are occupied unless I explicitly force it.
Acceptance criteria:
- Given at least one OCCUPIED bed,
PUT /locations/:id/status?active=falsereturns 409LOCATION_HAS_OCCUPIED_BEDS. - Given
force=truewith admin scope, deactivates and emitsfacility.audit.location.force_deactivated.
FAC-US-011 — Time zone aware location rendering
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-02 |
| Priority | Should |
| Story points | 2 |
| FR references | FR-FAC-LOC-005 |
| Legacy FR refs | FR-FAC-008 |
User story: As a consumer, I want to receive location timezone and locale so I render times correctly.
Acceptance criteria: timezone IANA valid; defaults Asia/Kabul for AFG, Asia/Dubai for UAE.
FAC-US-012 — Create bed and transition through lifecycle
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-03 |
| Priority | Must |
| Story points | 5 |
| FR references | FR-FAC-BED-001, FR-FAC-BED-002 |
| Legacy FR refs | FR-FAC-003, FR-FAC-006 |
User story: As a ward user, I want to manage beds through AVAILABLE → OCCUPIED → CLEANING → AVAILABLE so occupancy is accurate.
Acceptance criteria: state-machine enforced; invalid transitions 409 BED_INVALID_TRANSITION; event emitted on each change.
FAC-US-013 — Prevent double-assignment (BR-FAC-001)
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-03 |
| Priority | Must |
| Story points | 5 |
| FR references | FR-FAC-BED-003 |
| Legacy FR refs | BR-FAC-001 |
User story: As clinical safety, a bed cannot be assigned to two patients simultaneously.
Acceptance criteria: concurrent PUT with two patient IDs → one 200, one 409 BED_ALREADY_OCCUPIED.
FAC-US-014 — Housekeeping sub-state
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-03 |
| Priority | Should |
| Story points | 3 |
| FR references | FR-FAC-BED-004 |
| Legacy FR refs | FR-FAC-003 |
User story: As housekeeping staff, I want a sub-state (clean / in_progress / dirty) visible to ward dashboards.
Acceptance criteria: sub-state only valid while status ∈ {CLEANING, AVAILABLE}; audit trail.
FAC-US-015 — Create resource catalog item
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-04 |
| Priority | Should |
| Story points | 3 |
| FR references | FR-FAC-RES-001 |
| Legacy FR refs | FR-FAC-004 |
User story: As an admin, I want to register resources (rooms, equipment) for scheduling.
Acceptance criteria: create/list/update/deactivate; tenant-wide if no locationId.
FAC-US-016 — Publish resource events to scheduling-service
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-04 |
| Priority | Should |
| Story points | 2 |
| FR references | FR-FAC-RES-002 |
| Legacy FR refs | (FR-FAC-004 integration) |
User story: As scheduling-service, I want to consume facility.resource.* events to build slots.
Acceptance criteria: Pact contract with scheduling-service passes.
FAC-US-017 — Assign provider to node
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-05 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-MBR-001 |
| Legacy FR refs | FR-HIER-MBR-001 |
User story: As provider-directory, when I onboard a practitioner, I want to assign them to one or more nodes.
Acceptance criteria: only internal service scope can call; primary rule enforced.
FAC-US-018 — Provider context lookup (hot path)
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-05 |
| Priority | Must |
| Story points | 5 |
| FR references | FR-FAC-CTX-001 |
| Legacy FR refs | FR-HIER-MBR-004, NFR-HIER-001 |
User story: As any clinical service, I want GET /internal/providers/:id/context with p99 ≤ 20ms.
Acceptance criteria: cache hit rate ≥ 95%; fallback ≤ 80ms; event-driven invalidation ≤ 30s.
FAC-US-019 — Cache invalidation within 30s of mutation
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-05 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-CTX-002 |
| Legacy FR refs | BR-HIER-007 |
User story: As a downstream, the cache must reflect node/edge changes within 30s.
Acceptance criteria: integration test asserts ≤ 30s drift.
FAC-US-020 — FHIR Location projection
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-06 |
| Priority | Must |
| Story points | 5 |
| FR references | FR-FAC-FHIR-001 |
| Legacy FR refs | SPEC §6, §10 |
User story: As an interop consumer, I want /fhir/R4/Location to represent each internal Location + parent HierarchyNode as FHIR Location with partOf.
Acceptance criteria: round-trip search by hierarchy-node query param; contract test with interop-service.
FAC-US-021 — FHIR Organization projection
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-06 |
| Priority | Must |
| Story points | 3 |
| FR references | FR-FAC-FHIR-002 |
| Legacy FR refs | SPEC §6 |
User story: As interop, I want org-level nodes exposed as FHIR Organization.
Acceptance criteria: hospital / clinic / moph nodes map; code + name searchable.
FAC-US-022 — Import facility definition (dry-run + apply)
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-07 |
| Priority | Should |
| Story points | 8 |
| FR references | FR-FAC-IO-001 |
| Legacy FR refs | FR-FAC-005 |
User story: As an onboarding admin, I want to import a full facility snapshot with dry-run before applying.
Acceptance criteria: dry-run returns errors list without mutation; apply is transactional; rollback on failure.
FAC-US-023 — Export facility snapshot
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-07 |
| Priority | Should |
| Story points | 3 |
| FR references | FR-FAC-IO-002 |
| Legacy FR refs | FR-FAC-005 |
User story: As an admin, I want to export the full hierarchy + locations + beds as JSON for DR and migration.
Acceptance criteria: export matches import schema; checksum verified.
FAC-US-024 — SLO dashboards and alerts
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-08 |
| Priority | Must |
| Story points | 5 |
| FR references | NFR-FAC-001..005 |
| Legacy FR refs | NFR-HIER-001..005, NFR-FAC-001 |
User story: As SRE, I want dashboards for the 4 key SLIs and alerts wired to on-call.
Acceptance criteria: all alerts fire in staging drills; runbooks linked.
FAC-US-025 — Tenant-isolation integration spec
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-08 |
| Priority | Must |
| Story points | 3 |
| FR references | NFR-FAC-TEN |
| Legacy FR refs | NFR-HIER-004 |
User story: As platform security, I need the mandatory tenant-isolation test green.
Acceptance criteria: 100 tenants, property-based; no cross-tenant read/write ever succeeds.
FAC-US-026 — Chaos drill: NATS + Redis + Postgres failover
| Field | Value |
|---|---|
| Epic link | FAC-EPIC-08 |
| Priority | Should |
| Story points | 5 |
| FR references | NFR-FAC-CHAOS |
| Legacy FR refs | — (new) |
User story: As SRE, I want to prove the service survives dependency outages without data loss.
Acceptance criteria: chaos scenarios pass; runbooks updated post-drill.