Skip to content

Sign-off PDF

The sign-off PDF is the final, immutable artefact that iFlow produces for a clinical case. This page describes how the Approve-and-Sign flow assembles it atomically and what guarantees it provides.

Endpoints

The signoff HTMX routes live under /<report_id>/signoff/:

Method Path Purpose
GET /confirm-modal Open confirmation modal; kick off async preview render
POST /prepare-pdf Trigger (re)render of the preview PDF
GET /pdf-status Polled by modal spinner while render is in flight
GET /pdf Stream the currently rendered (preview) PDF
POST /commit Atomic finalize — Approve-and-Sign
GET /completed-modal Post-finalize completion popup with download link

Atomic Approve-and-Sign

The core method is approve_and_sign() on the Report service. It executes all of the following in a single database transaction:

  1. Validate the report is in approved or pending_review (falls through to approve if the latter).
  2. Take the preview PDF path (already rendered by prepare_signoff_pdf) and treat it as the signed PDF path.
  3. Set on the Report:
    • status = signed
    • signed_by = <user_id>
    • signed_at = now()
    • signed_pdf_path = orders/{slug}/signed-report.pdf
  4. Call MinerServiceClient.advance_order_on_sign(order_id) which transitions the order from sign_off to completed in Miner.
  5. Commit.

On any failure, the transaction rolls back: the report does not become signed and no PDF is left behind. One exception: if Miner returns 409 because the order is already past sign_off, the signoff routine treats that as success and proceeds.

prepare_signoff_pdf()

The preview render used by the signoff confirmation modal is produced synchronously by prepare_signoff_pdf():

  • Computes the signed path via signed_report_path(display_id)orders/{slug}/signed-report.pdf.
  • Renders the DOCX to PDF using the template selected for the report.
  • Stamps the signer's e-signature onto the final page if the template contains a sign-off placeholder table (see below).
  • Uploads the result to the project bucket via admin credentials (not the caller's credentials), so a clinical user without bucket write access can still produce the signed PDF.
  • Stores a small cache record under Report.properties["signoff_preview"] with {"path": ..., "status": "ready" | "failed" | "missing_profile", ...}.
  • Refuses to run if the report is already signed — the path is immutable.

Every time the user opens the confirmation modal, any stale signoff_preview is cleared first so the render is always produced from the latest report state and signer profile data. This is how updating your profile signature and reopening the modal actually takes effect.

This means the confirmation modal shows the same PDF that will become the signed artifact. Curators have one last chance to cancel before the atomic commit locks it in.

E-signature stamping

If the selected DOCX template ends with a sign-off placeholder table (the last table on the final or penultimate page), prepare_signoff_pdf() overlays it with the signer's identity and signature image before upload. The stamp contains:

  • Approved on: <weekday>, <day> <month> <year>, <time> (UTC)
  • Approved by: <display name>[, <job title>]
  • The signer's uploaded signature PNG below the text

Templates without such a placeholder table (e.g. non-clinical internal reports) flow through the same path unchanged — the stamper detects the missing placeholder and returns the PDF untouched.

Signer profile requirements

When the template has a sign-off placeholder, the signer's iFlow profile must have both:

  • Display name — shown under "Approved by". Synced from Zitadel, so usually present for any real user.
  • Signature image — a PNG uploaded at /account/profile in the Admin Console. Size-limited to 1 MB.

Optionally, a job title on the profile is appended after the display name on the stamp.

If either required field is missing, the status becomes missing_profile and the confirmation modal shows the specific missing-data error ("Signer has no signature uploaded" or "Signer has no display name in profile"). The "Generate Final Report" button stays disabled — no retry button is offered: the signer closes the modal, updates their profile, reopens the modal, and the fresh render takes over.

This operation cannot be undone

approve_and_sign() writes an immutable PDF, marks the report signed, and advances the order to completed — all in one transaction. The only way to change a signed report is an amendment.

Immutability of the signed PDF

Once approve_and_sign() commits:

  • signed_pdf_path on the report is set and never modified.
  • The uploaded PDF at orders/{slug}/signed-report.pdf is written once. Subsequent render attempts that try to overwrite it are blocked by the _check_write() guard inside prepare_signoff_pdf().
  • An amendment produces a different PDF at a versioned path (e.g. orders/{slug}/signed-report-amended-1.pdf) so the original signed artifact is always recoverable.

All-or-nothing transaction

If any step of the commit fails (PDF upload, DB write, order transition), the whole transaction rolls back. You will never end up with a half-signed report.

Why admin credentials

Uploading the signed PDF uses admin credentials rather than the signing user's credentials so that sign-off does not depend on the physician's personal bucket access. The physician's identity is captured on the report (signed_by), not on the bucket write.

What the user sees

The user experience during sign-off is the modal choreography driven by the endpoints above:

  1. Click Approve and Sign/confirm-modal opens. The background render task starts fresh (any stale preview state is cleared first).
  2. Spinner polls /pdf-status while the DOCX render, e-signature stamp, and GCS upload run.
  3. When the preview is ready, the spinner is replaced by a link to the rendered PDF and the Generate Final Report button unlocks.
  4. Alternative: if the template requires a signature but the profile is incomplete, the spinner is replaced by a missing_profile warning with the specific missing field, and Generate Final Report stays disabled.
  5. User confirms by clicking Generate Final Report/commit runs atomically; spinner shows during the commit.
  6. /completed-modal replaces the spinner with a success popup and a Download signed PDF link.