Document Service — User Stories
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template
Stories
DOC-US-001 — Create a Document Template (Draft)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-01 — Template Library Management |
| Summary | As a Tenant Admin, I want to create a new document template in draft status so that clinical staff can review it before it is published |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | template-crud |
| FR references | FR-DOC-001, FR-DOC-002, FR-DOC-003 |
| Legacy FR refs | FR-DMS-001, FR-DMS-002, FR-DMS-003 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | — |
User story:
As a Tenant Admin, I want to create a new document template with a name, category, and initial definition, so that clinical staff can design and review the template before it goes live.
Acceptance criteria:
Scenario: Create a new draft template
Given I am authenticated as a Tenant Admin for tenant "ten_afg_moph_001"
And I have a valid template definition JSON with name "Outpatient Prescription" and category "prescription"
When I POST /v1/document-templates with the template payload
Then the response status is 201
And the response body contains a "templateId" with prefix "dtpl_"
And the template "status" is "draft"
And the template "origin" is "tenant"
And a "document.template.created.v1" event is emitted to NATS
Scenario: Template name must be unique within tenant and category
Given I am authenticated as a Tenant Admin for tenant "ten_afg_moph_001"
And a template named "Outpatient Prescription" in category "prescription" already exists
When I POST /v1/document-templates with the same name and category
Then the response status is 409
And the error code is "TEMPLATE_NAME_CONFLICT"
Scenario: Platform templates cannot be created via the tenant API
Given I am authenticated as a Tenant Admin
When I POST /v1/document-templates with "origin": "platform"
Then the response status is 422
And the error code is "PLATFORM_ORIGIN_FORBIDDEN"
Technical notes:
- Template
iduses ULID with prefixdtpl_ createdByis the authenticated user's sub claim- All template writes go through outbox pattern for event delivery guarantee
definitionstored as JSONB; layout and bindings validated at publish, not at create
Definition of Done:
- POST /v1/document-templates returns 201 with correct shape
- Template persisted with
status=draft,origin=tenant -
document.template.created.v1event in NATS outbox - Conflict returns 409 with
TEMPLATE_NAME_CONFLICT - Unit tests for CreateTemplateUseCase
- Integration test verifying DB row and outbox entry
DOC-US-002 — Publish a Document Template
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-01 — Template Library Management |
| Summary | As a Tenant Admin, I want to publish a draft template version so that clinicians can use it to generate documents |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | template-versioning |
| FR references | FR-DOC-004, FR-DOC-005, FR-DOC-006 |
| Legacy FR refs | FR-DMS-004, FR-DMS-005, FR-DMS-006 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | DOC-US-001 |
User story:
As a Tenant Admin, I want to publish a draft template version with semantic version number and an effectiveFrom date, so that clinicians can start using it for document generation.
Acceptance criteria:
Scenario: Publish a valid draft template version
Given I am authenticated as a Tenant Admin
And a draft template version "tv_001" exists with a valid definition (all required binding fields present)
When I POST /v1/document-templates/{templateId}/versions/{versionId}/publish
Then the response status is 200
And the version "status" changes to "published"
And the version is immutable (subsequent edits return 409)
And a "document.template.published.v1" event is emitted
Scenario: Cannot publish a template version with invalid bindings
Given a draft template version with a binding referencing a non-existent FHIR path
When I attempt to publish the version
Then the response status is 422
And the error code is "TEMPLATE_BINDING_INVALID"
And the response details the offending binding paths
Scenario: effectiveTo scheduling deprecates a published version
Given a published version with "effectiveTo": "2026-06-01T00:00:00Z"
When the system clock passes that date
Then the version status transitions to "deprecated"
And a "document.template.deprecated.v1" event is emitted
Technical notes:
- Publish triggers binding path validation against FHIR R4 schema registry
- Semantic version must follow
MAJOR.MINOR.PATCHformat effectiveToscheduling is handled by a background job (not real-time)- Published versions are write-protected at the DB level (trigger or application guard)
Definition of Done:
- POST publish endpoint returns 200 and transitions status
- Binding validation runs and returns 422 with details on failure
-
document.template.published.v1event emitted - Immutability enforced: PATCH on published version returns 409
- Integration test covering full draft → publish lifecycle
DOC-US-003 — Version a Template (Semantic Versioning)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-01 — Template Library Management |
| Summary | As a Tenant Admin, I want to create a new draft version of an existing published template so that I can make changes without affecting documents already generated |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | template-versioning |
| FR references | FR-DOC-005 |
| Legacy FR refs | FR-DMS-005 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | DOC-US-002 |
User story:
As a Tenant Admin, I want to create a new draft version from an existing published template version, so that I can iterate on the design while existing documents remain linked to the previous published version.
Acceptance criteria:
Scenario: Create a new draft version from a published version
Given a published template version "1.0.0" for template "dtpl_001"
When I POST /v1/document-templates/{templateId}/versions with baseVersionId "tv_001" and semver "1.1.0"
Then the response status is 201
And the new version has status "draft"
And the new version's definition is a copy of version "1.0.0"
And version "1.0.0" remains published and unchanged
Scenario: Semver must be monotonically increasing
Given published version "1.2.0" exists
When I attempt to create a new version with semver "1.1.5"
Then the response status is 422
And the error code is "SEMVER_REGRESSION"
Technical notes:
baseVersionIdis optional; if omitted, the latest published version is used- Version number comparison uses semver library (strict mode)
Definition of Done:
- POST /versions creates draft copy
- Semver regression guard returns 422 with
SEMVER_REGRESSION - Old published version unaffected (verified via integration test)
DOC-US-004 — Generate a Document Synchronously (PDF)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-02 — Server-Side PDF Generation |
| Summary | As a Clinician, I want to generate a PDF document from a template in real time so that I can hand it to the patient during the consultation |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | pdf-renderer, fhir-binding, idempotency |
| FR references | FR-DOC-007, FR-DOC-008, FR-DOC-009, FR-DOC-011, FR-DOC-012 |
| Legacy FR refs | FR-DMS-007, FR-DMS-008, FR-DMS-009, FR-DMS-011, FR-DMS-012 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | DOC-US-002, interop-service |
User story:
As a Clinician, I want to POST a generate request with a templateVersionId and patientId and receive a presigned PDF download URL within 5 seconds, so that I can download and print the document without PHI ever reaching the browser unencrypted.
Acceptance criteria:
Scenario: Successful synchronous PDF generation
Given I am authenticated as a Clinician with role "clinician"
And template version "tv_001" is published and has status "active"
And patient "pat_001" exists in the FHIR gateway
When I POST /v1/documents/generate with templateVersionId "tv_001", patientId "pat_001", locale "ps-AF"
And I include "Idempotency-Key: <uuidv4>" header
Then the response status is 200
And the response body contains "downloadUrl" (presigned, TTL 15 min)
And the response body contains "artifactId" with prefix "dart_"
And the response body contains "fhirDocumentReferenceId"
And the PDF is stored AES-256 encrypted in object storage
And the response p95 latency is < 5 seconds at 10 rps
Scenario: Idempotent repeat request returns same artifact
Given I previously generated a document with Idempotency-Key "idem-123"
When I POST /v1/documents/generate again with the same Idempotency-Key "idem-123"
Then the response status is 200
And the response body is identical to the first response
And no new PDF is generated
Scenario: PHI does not leave the server boundary
Given a generation request is processing
When the FHIR bindings are resolved
Then all PHI resolution happens within the document-service process
And the generated PDF bytes are stored directly to object storage
And the client receives only a presigned URL, never raw PDF bytes in the API response body
Technical notes:
clientMutationIdscoped to(tenantId, clientMutationId)for idempotency- Input snapshot hash computed from
(templateVersionId, patientId, encounterId, locale)for audit - FHIR resolution calls
GET /fhir/Patient/{id}on interop-service - Chromium-based renderer (Puppeteer) runs in isolated worker process
Definition of Done:
- POST /v1/documents/generate returns 200 with presigned URL
- p95 < 5 s under 10 rps load test
- Idempotency: duplicate
Idempotency-Keyreturns cached response - PDF stored AES-256 in MinIO; verified via integration test
-
document.render.completed.v1event emitted - FHIR DocumentReference created in interop-service via event
DOC-US-005 — Generate a Document Asynchronously (Batch / Heavy)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-02 — Server-Side PDF Generation |
| Summary | As a Clinician, I want to submit a render job for a complex or batch document and poll its status, so that the UI remains responsive while the PDF is being generated |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | render-jobs |
| FR references | FR-DOC-013, FR-DOC-014 |
| Legacy FR refs | FR-DMS-013, FR-DMS-014 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | DOC-US-004 |
User story:
As a Clinician, I want to submit a render job and be notified when the PDF is ready via a pollable status endpoint, so that complex documents do not block my workflow.
Acceptance criteria:
Scenario: Submit async render job
Given I am authenticated as a Clinician
When I POST /v1/documents/render-jobs with a valid generation payload
Then the response status is 202
And the response body contains "renderJobId" with prefix "rjob_"
And the job status is "queued"
Scenario: Poll render job until completion
Given a render job "rjob_001" exists
When I GET /v1/documents/render-jobs/{renderJobId}
And the job has completed processing
Then the response status is 200
And the job "status" is "completed"
And the response body contains "downloadUrl"
Scenario: Render job fails gracefully
Given FHIR gateway is unavailable during rendering
When the render worker attempts to resolve FHIR bindings
Then the job "status" transitions to "failed"
And the job "errorCode" is "FHIR_RESOLUTION_ERROR"
And a "document.render.failed.v1" event is emitted
Technical notes:
- Render worker is a separate deployment unit that consumes from NATS JetStream render queue
- Auto-scales on queue depth; alert at 500 queued jobs
- Failed jobs are retried up to 3 times with exponential backoff before transitioning to
failed
Definition of Done:
- POST /v1/documents/render-jobs returns 202 with
renderJobId - GET status endpoint reflects job lifecycle transitions
- Failed job emits
document.render.failed.v1 - Integration test: job queued → worker processes → completed status
DOC-US-006 — Offline Render Queue (Retry on Reconnect)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-02 — Server-Side PDF Generation |
| Summary | As a Clinician on an intermittent connection, I want my generate request to be queued and retried automatically when connectivity is restored |
| Status | To Do |
| Priority | Should |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S0, offline |
| Components | idempotency, render-jobs |
| FR references | FR-DOC-012 |
| Legacy FR refs | FR-DMS-012 (PLAT-DMS SPEC.md §3.1) |
| Dependencies | DOC-US-004 |
User story:
As a Clinician at a facility with intermittent connectivity, I want to submit a document generation request that is queued locally and replayed when the network reconnects, so that I do not lose work during connectivity gaps.
Acceptance criteria:
Scenario: Offline generate request is replayed on reconnect
Given the device is offline
And I submit a generate request with "clientMutationId": "offline-001"
When the device reconnects and the client replays the request
Then the server processes the request exactly once (idempotency key deduplicates)
And the document is generated successfully
And no duplicate artifact is created
Scenario: Replay does not produce duplicate when server already processed
Given the server already processed request with "clientMutationId": "offline-001"
When the client replays the same request after reconnect
Then the response is 200 with the original artifact details (no re-render)
Technical notes:
- Client is responsible for local queue management; document-service is idempotency-aware only
(tenantId, clientMutationId)uniqueness enforced at DB level (unique index)
Definition of Done:
- Duplicate
clientMutationIdreturns 200 with cached response (no re-render) - DB unique index on
(tenant_id, client_mutation_id)confirmed in migration - Integration test simulates replay scenario
DOC-US-007 — Upload a Scanned Document
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-03 — Document Upload and Ingestion |
| Summary | As a Clinical Staff member, I want to upload a scanned PDF or image of a paper record so that it is stored securely and linked to the patient's FHIR record |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | upload-ingestion, virus-scanner, object-storage |
| FR references | FR-DOC-023 |
| Legacy FR refs | FR-DMS-023 (PLAT-DMS SPEC.md §3.2), FR-DM-001 |
| Dependencies | interop-service (DocumentReference creation) |
User story:
As a Clinical Staff member, I want to upload a scanned PDF or image of a paper record via a two-step flow (initiate → finalize), so that the file is virus-scanned and registered as a FHIR DocumentReference automatically.
Acceptance criteria:
Scenario: Successful two-step upload flow
Given I am authenticated as a Clinical Staff member with role "clinical_staff"
When I POST /v1/documents/uploads/initiate with filename "referral_letter.pdf", mimeType "application/pdf", patientId "pat_001"
Then the response status is 200
And the response contains "uploadUrl" (presigned PUT URL) and "uploadId"
When I PUT the file bytes to the "uploadUrl"
And I POST /v1/documents/uploads/{uploadId}/finalize
Then the response status is 200
And the response contains "artifactId" and "fhirDocumentReferenceId"
And a "document.upload.completed.v1" event is emitted
Scenario: ClamAV scan runs before finalization
Given a clean file was uploaded to the staged URL
When I POST finalize
Then ClamAV scans the file before moving it to permanent storage
And the file is moved to "/{tenantId}/documents/{artifactId}" only if scan result is "CLEAN"
Scenario: Upload is blocked when ClamAV is unavailable
Given ClamAV service is down
When I POST /v1/documents/uploads/initiate
Then the response status is 503
And the error code is "VIRUS_SCANNER_UNAVAILABLE"
Technical notes:
- ClamAV readiness is part of the
/health/readycheck; uploads gate on readiness - Staged uploads live in
/{tenantId}/uploads/staged/bucket prefix with 24h TTL - Supported mime types:
application/pdf,image/jpeg,image/png; max size 50 MB
Definition of Done:
- Two-step upload flow (initiate + finalize) implemented
- ClamAV scan runs on finalize; clean file moves to permanent path
- 503 returned when ClamAV health check fails
- FHIR DocumentReference created via interop-service event
-
document.upload.completed.v1event in outbox - Integration test covers clean upload flow end-to-end
DOC-US-008 — Quarantine an Infected Upload
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-03 — Document Upload and Ingestion |
| Summary | As the Platform Security Team, I want infected uploaded files to be quarantined automatically and a security event emitted so that the threat is contained and auditable |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S0, security |
| Components | virus-scanner, object-storage |
| FR references | FR-DOC-023 |
| Legacy FR refs | FR-DMS-023 (PLAT-DMS SPEC.md §3.2) |
| Dependencies | DOC-US-007 |
User story:
As the Platform Security Team, I want infected files to be quarantined in an isolated bucket and a
document.upload.quarantined.v1security event emitted, so that the threat is logged, the file never reaches permanent storage, and the incident is auditable.
Acceptance criteria:
Scenario: Infected file is quarantined on finalization
Given a file containing EICAR test signature was uploaded to the staged URL
When I POST /v1/documents/uploads/{uploadId}/finalize
Then the response status is 422
And the error code is "VIRUS_DETECTED"
And the file is moved to the quarantine bucket "/{tenantId}/quarantine/"
And the file is NOT present in "/{tenantId}/documents/"
And a "document.upload.quarantined.v1" event is emitted with severity "security"
Scenario: Quarantined files cannot be accessed by tenants
Given a quarantined file exists in "/{tenantId}/quarantine/"
When any authenticated user attempts to retrieve it
Then the response status is 404 (quarantine bucket is private)
Technical notes:
- Quarantine bucket has no presigned URL generation permission
- Security events are routed to NATS subject
document.upload.quarantined.v1withseverity: "security"in the CloudEventsextensionattributes - EICAR test file used in readiness tests and CI
Definition of Done:
- EICAR test file triggers quarantine flow
-
document.upload.quarantined.v1event emitted - Quarantine bucket inaccessible to tenant API
- Integration test: EICAR file → finalize → 422 + event
- Readiness gate: EICAR quarantine verified before launch sign-off
DOC-US-009 — List and Search Patient Documents
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-04 — Artifact Management and Audit |
| Summary | As a Clinician, I want to search a patient's document history by category and date range so that I can quickly locate the relevant clinical record |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:document, domain:clinical_documents, slice:S1 |
| Components | artifact-search |
| FR references | FR-DOC-016, FR-DOC-018 |
| Legacy FR refs | FR-DMS-016, FR-DMS-018 (PLAT-DMS SPEC.md §3.2) |
| Dependencies | DOC-US-004, DOC-US-007 |
User story:
As a Clinician, I want to search a patient's documents by category (e.g., prescription, lab_requisition) and date range, so that I can quickly find relevant records without scrolling through unrelated documents.
Acceptance criteria:
Scenario: List all documents for a patient
Given patient "pat_001" has 5 documents across 3 categories
When I GET /v1/documents?patientId=pat_001&pageSize=20
Then the response status is 200
And the response body contains a "data" array of up to 20 artifacts
And each artifact has "artifactId", "category", "createdAt", "status"
And the response includes "pagination" metadata
Scenario: Filter by category and date range
Given patient "pat_001" has documents in categories "prescription" and "lab_requisition"
When I GET /v1/documents?patientId=pat_001&category=prescription&dateFrom=2026-01-01&dateTo=2026-04-01
Then only "prescription" documents within the date range are returned
Scenario: Cross-tenant access is blocked
Given I am authenticated as a Clinician in tenant "ten_afg_moph_001"
When I GET /v1/documents?patientId=pat_from_other_tenant
Then the response status is 200
And the response "data" array is empty (RLS prevents cross-tenant rows)
Technical notes:
- Backed by FHIR DocumentReference search via
GET /fhir/DocumentReference?subject=Patient/{id}on interop-service - Tenant isolation enforced by PostgreSQL RLS (
tenant_id = current_setting('app.current_tenant_id')) - Cursor-based pagination; default page size 20, max 100
Definition of Done:
- GET /v1/documents with query params returns filtered results
- Pagination metadata present in all responses
- Cross-tenant query returns empty array (not 403; RLS transparent)
- Integration test: multi-tenant isolation scenario
DOC-US-010 — Download a Document via Presigned URL
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-04 — Artifact Management and Audit |
| Summary | As a Clinician, I want to download a document via a short-lived presigned URL so that PHI is never exposed in long-lived links |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S1 |
| Components | presigned-urls, audit |
| FR references | FR-DOC-019, FR-DOC-020, FR-DOC-021, FR-DOC-022 |
| Legacy FR refs | FR-DMS-019, FR-DMS-020, FR-DMS-021, FR-DMS-022 (PLAT-DMS SPEC.md §3.2) |
| Dependencies | DOC-US-009 |
User story:
As a Clinician, I want to GET a presigned download URL for a specific artifact that expires in 15 minutes, so that I can download the document securely and the link cannot be shared indefinitely.
Acceptance criteria:
Scenario: Get a valid presigned URL for an artifact
Given artifact "dart_001" exists and belongs to my tenant
When I GET /v1/documents/{artifactId}/download-url
Then the response status is 200
And the response body contains "downloadUrl" with a presigned S3/MinIO URL
And the URL TTL is 900 seconds (15 minutes)
And a "document.artifact.accessed.v1" audit event is emitted
Scenario: Expired presigned URL returns 403
Given a presigned URL was generated 16 minutes ago
When a client attempts to use that URL
Then the object storage returns 403 Forbidden
Scenario: Download of another tenant's artifact is blocked
Given artifact "dart_999" belongs to tenant "ten_other"
When I GET /v1/documents/dart_999/download-url as a user of tenant "ten_moph"
Then the response status is 404
Technical notes:
- Presigned URL TTL configured via
PRESIGNED_URL_TTL_SECONDS(default 900) - Every download URL request emits
document.artifact.accessed.v1for HIPAA accounting of disclosures - TTL is enforced by MinIO/S3 natively; no server-side expiry needed
Definition of Done:
- GET download-url returns 200 with presigned URL
- TTL default is 900 seconds; configurable via env var
-
document.artifact.accessed.v1event emitted on every request - Cross-tenant access returns 404
- Integration test verifies URL TTL enforcement
DOC-US-011 — Access Platform Reference Template Catalog
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-05 — Platform Reference Form Catalog |
| Summary | As a Tenant Admin, I want to browse the platform's curated template catalog so that I can see what reference forms are available for my tenant from day one |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | platform-templates |
| FR references | FR-DOC-029, FR-DOC-030 |
| Legacy FR refs | FR-DMS-029, FR-DMS-030 (PLAT-DMS SPEC.md §3.3) |
| Dependencies | DOC-US-001 |
User story:
As a Tenant Admin, I want to view the full catalog of platform reference templates with their stable
platformFormKeyvalues, so that I know which standard forms are available and can plan which ones to use or fork for my facility.
Acceptance criteria:
Scenario: List platform reference templates
Given I am authenticated as a Tenant Admin
When I GET /v1/document-templates?origin=platform&status=published
Then the response status is 200
And the response "data" array contains at least the "General Test Requisition" template
And each template has a non-null "platformFormKey" field
And each template has "origin": "platform"
Scenario: Platform templates are visible to all tenants read-only
Given I am authenticated as a Tenant Admin for any tenant
When I GET /v1/document-templates?origin=platform
Then I can read the template definitions
And I cannot modify them (PATCH returns 403 with "PLATFORM_TEMPLATE_IMMUTABLE")
Technical notes:
- Platform templates seeded via
scripts/migration/seed-platform-templates.tsduring platform bootstrap platformFormKeyvalues are stable identifiers used in contract tests (e.g.,platform.dms.general-lab.general-test-requisition)
Definition of Done:
- Seed script creates General Test Requisition with correct
platformFormKey - GET ?origin=platform returns platform templates to all authenticated tenants
- PATCH on platform template returns 403 with
PLATFORM_TEMPLATE_IMMUTABLE -
platformFormKeypresent on all platform template rows
DOC-US-012 — Fork a Platform Template
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-05 — Platform Reference Form Catalog |
| Summary | As a Tenant Admin, I want to fork a platform reference template into my tenant so that I can customise it for my facility's needs |
| Status | To Do |
| Priority | Must |
| Story points | 5 |
| Labels | service:document, domain:clinical_documents, slice:S0 |
| Components | fork-workflow |
| FR references | FR-DOC-031, FR-DOC-032 |
| Legacy FR refs | FR-DMS-031, FR-DMS-032 (PLAT-DMS SPEC.md §3.3) |
| Dependencies | DOC-US-011 |
User story:
As a Tenant Admin, I want to fork a platform reference template into a tenant-owned copy, so that I can customise the layout and branding for my facility while retaining traceability to the original platform form.
Acceptance criteria:
Scenario: Fork a platform template
Given platform template "platform.dms.general-lab.general-test-requisition" exists
When I POST /v1/document-templates/{platformTemplateId}/fork
Then the response status is 201
And a new template is created with "origin": "tenant"
And the new template has "forkedFromId" pointing to the platform template
And the new template has "forkedFromPlatformFormKey": "platform.dms.general-lab.general-test-requisition"
And the new template starts in "draft" status
Scenario: Forked template is independently editable
Given I have forked a platform template
When I PATCH my forked template's draft version definition
Then the changes are saved to the tenant-owned copy only
And the original platform template is unchanged
Scenario: Platform template version upgrade does not auto-update forks
Given my fork was based on platform version "1.0.0"
When the platform ships version "1.1.0"
Then my fork remains at its own definition
And I am notified via a "document.template.platform_upgrade_available.v1" event (informational)
Technical notes:
- Fork creates a new
DocumentTemplaterow withorigin=tenantand a newDocumentTemplateVersionrow copying the definition from the platform version forkedFromIdenables audit trail back to platform source
Definition of Done:
- POST /fork returns 201 with new tenant template
-
forkedFromIdandforkedFromPlatformFormKeypopulated - Forked template editable; platform original unchanged
- Integration test: fork → edit → publish tenant copy
DOC-US-013 — Generate a PDF in Pashto or Dari (RTL)
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-06 — RTL/Multilingual PDF Rendering and Branding |
| Summary | As a Clinician at an Afghan facility, I want to generate a prescription PDF in Pashto so that the patient receives a document they can read |
| Status | To Do |
| Priority | Must |
| Story points | 8 |
| Labels | service:document, domain:clinical_documents, slice:S1, rtl, i18n |
| Components | pdf-renderer, rtl-support |
| FR references | FR-DOC-026 |
| Legacy FR refs | FR-DMS-026 (PLAT-DMS SPEC.md §3.2), FR-L10N-006 |
| Dependencies | DOC-US-004, config-service |
User story:
As a Clinician at an Afghan facility, I want to specify
locale: "ps-AF"in a generate request and receive a PDF with correct RTL layout, Noto Nastaliq Urdu embedded fonts, and accurate Arabic numerics, so that Pashto-speaking patients receive documents they can read.
Acceptance criteria:
Scenario: Generate RTL PDF in Pashto
Given template version "tv_001" supports locale "ps-AF"
When I POST /v1/documents/generate with locale "ps-AF"
Then the response is 200 with a "downloadUrl"
And the generated PDF renders text right-to-left
And the PDF embeds Noto Nastaliq Urdu font
And numeric fields use Eastern Arabic numerals where specified in the template
Scenario: Generate PDF in Dari (fa-AF)
Given template version "tv_001" supports locale "fa-AF"
When I POST /v1/documents/generate with locale "fa-AF"
Then the generated PDF renders text right-to-left with Dari script
And the PDF passes the golden hash comparison for the "fa-AF" locale fixture
Scenario: Unsupported locale falls back to "en"
Given a template version that only declares locales ["en", "ps-AF"]
When I POST /v1/documents/generate with locale "zh-CN"
Then the PDF is generated in "en" locale
And the response includes a "localeWarning": "LOCALE_FALLBACK_TO_EN" field
Technical notes:
- Font embedding: Noto Nastaliq Urdu (ps-AF, ur), Noto Sans Arabic (fa-AF), Noto Sans Latin (en)
- Layout direction resolved from config-service design token
layout.directionfor the request locale - Unicode bidi algorithm applied for mixed Arabic/Latin numeral fields
- Golden PDF hash tests run per reference form per locale in CI
Definition of Done:
- ps-AF generation produces RTL PDF with embedded Noto Nastaliq Urdu font
- fa-AF generation passes golden hash comparison fixture
- Unsupported locale falls back to "en" with localeWarning
- Golden PDF contract tests (pnpm test:pdf-contracts) green for ps-AF and fa-AF locales
- Performance: RTL render does not exceed 7 s p95 (allow +2 s over standard)
DOC-US-014 — Apply Tenant Branding to Generated PDFs
| Field | Value |
|---|---|
| Issue type | Story |
| Epic link | DOC-EPIC-06 — RTL/Multilingual PDF Rendering and Branding |
| Summary | As a Tenant Admin, I want generated PDFs to display my facility's logo and header so that documents look official and are recognisable to patients |
| Status | To Do |
| Priority | Must |
| Story points | 3 |
| Labels | service:document, domain:clinical_documents, slice:S1, branding |
| Components | pdf-renderer, branding |
| FR references | FR-DOC-026 |
| Legacy FR refs | FR-DMS-026 (PLAT-DMS SPEC.md §3.2) |
| Dependencies | DOC-US-013, config-service |
User story:
As a Tenant Admin, I want generated PDFs to automatically include my facility's logo, facility name, and header configured in the config-service design tokens, so that all clinical documents look official and carry the facility's identity.
Acceptance criteria:
Scenario: PDF generation applies tenant branding from config-service
Given tenant "ten_afg_moph_001" has branding tokens: logo URL, facilityName, headerColor
When I POST /v1/documents/generate
Then the generated PDF includes the facility logo in the header
And the facility name appears in the header text
And the header color matches the configured branding token
Scenario: Missing branding tokens fall back to platform defaults
Given a tenant has no branding tokens configured
When I generate a PDF
Then the PDF uses the platform default Ghasi eHealth branding
And no error is thrown
Scenario: Branding token fetch failure does not block generation
Given config-service is temporarily unavailable
When I generate a PDF
Then the PDF is generated with platform default branding
And a "BRANDING_FETCH_FAILED_FALLBACK" warning is logged
And the generation succeeds (non-blocking degradation)
Technical notes:
- Branding tokens fetched from config-service
GET /v1/config/design-tokens?scope=pdf_branding&tenantId=...at render time - Logo URL must be a data URI or a presigned internal URL; external HTTP URLs are blocked (SSRF prevention)
- Branding fetch is wrapped in a circuit breaker (timeout 500 ms, fallback to platform defaults)
Definition of Done:
- Tenant branding tokens from config-service appear in generated PDF header
- Fallback to platform defaults when tokens missing or config-service unavailable
- Circuit breaker on branding fetch (500 ms timeout)
- SSRF prevention: external logo URLs blocked (only data URIs or internal presigned URLs)
- Integration test: branding tokens applied end-to-end