Skip to content

M5-10 PDF Verification Report Generation

Milestone: M5 — Disclosure and Verification SOW Reference: FR3, INT7 Requirement Clarity: ✅ Clear — concrete rules added 2026-08-26 Dev Status: 🟡 Planned, not built — two-phase plan agreed 2026-09-15 (stream-only first, S3 storage second); see update below before starting Phase 1

Overview

Let a Staffing Company download and save a PDF report of a verification outcome, for compliance/audit purposes — covering credentials within the whole Velocity ecosystem, not just those issued/verified via this platform.

2026-08-26, from Velocity's CV Capture spec — concrete rules, not previously specified anywhere: - One credential and its verification checks per page — not one combined table for all disclosed credentials. - Printing must be disabled on the rendered/viewed report. - If generation fails, show a clear, visible error. Never render a download link/button with an empty href — a dead-looking-live control is explicitly called out as a failure state to avoid. - The PDF's content must match the on-screen verification results exactly (same checks, same pass/fail state) — this is a testable acceptance criterion, not just a suggestion (see M5-13).

Backend Tasks

  • Add a PDF generation library (pdfkit, puppeteer, or similar) — none currently present
  • Build a PDF report template: one credential + its 6 checks (see M5-09) + timestamp + issuer/credential metadata per page
  • Wire the existing VerificationReport.pdfS3Key column up to actual storage (S3) and generation
  • Per the SOW, note VerifyMyCreds already contains this capability — confirm whether that code can be reused rather than built fresh

Frontend Tasks

  • Add a "Download PDF" action on the verification result view
  • Handle the generation-failure case explicitly — visible error state, disabled/absent button rather than a broken link

Dependencies

Acceptance Criteria

  • PDF report generated
  • PDF is downloadable
  • PDF includes audit evidence (per SOW Appendix 3, FR3 acceptance criteria)

Existing Reference Material

Blockers & Risks

  • Risk: confirm the VerifyMyCreds PDF-generation code-reuse question here too — same underlying uncertainty as M5-08.

Update 2026-09-15 — VerifyMyCreds code reuse resolved: not possible

Same finding as M5-00's DEP3 update. VerifyMyCreds' own public Terms of Service grant only "a limited, non-exclusive, non-transferable, non-sublicensable license to access and use the Service", and explicitly list "attempting to derive the source code for the Site and Service" as prohibited. It's a closed hosted SaaS product — there is no PDF-generation code to reuse, regardless of what the SOW assumed. This needs to be built from scratch, same as the rest of M5's verification UI (which already was, once the same dependency turned out not to hold).

Still not started — no PDF library present, pdfS3Key still unused. The disclosure detail page already has a disabled placeholder button (disclosure-requests/[id].tsx, tooltip: "PDF export is not yet available — CSV covers this for now") — so the UI hook point exists, generation itself doesn't.

Update 2026-09-15 — confirmed no Velocity PDF endpoint exists; two-phase build plan agreed

Checked both live Velocity API specs (CIH and Registrar — the full OpenAPI JSON pulled for other investigations this milestone) for anything PDF/report-related: nothing exists. This isn't a Velocity integration at all — combined with the DEP3 finding above, it's 100% an in-house rendering task. This also resolves disclosure.controller.ts:200-213's own open question ("PDF ownership... is an open decision") — confirmed in-house, no external service to call.

Also found while planning storage: the M5-00 guide's earlier assumption that the Attachment entity is "reusable as-is for storing verification PDFs" doesn't hold up — checked, and there is no S3 upload code anywhere in this backend at all, despite AWS_S3_BUCKET/credentials already being configured in env. Attachment.s3Key is unused dead schema exactly like VerificationReport.pdfS3Key — nothing has ever actually uploaded a file to S3 in this codebase. Storage isn't "wire up the existing pattern," it's a small service built from scratch.

Agreed plan — split into two phases, so the content/layout gets validated before the storage complexity is added:

Phase 1 — stream-only PDF (build first)

  • Add pdfkit to app/backend/package.json (pure Node, no headless-browser dependency — better fit for this simple templated layout than puppeteer)
  • Replace the NotImplementedException stub in reportPdf() (disclosure.controller.ts:200-213) with real generation
  • Build directly from the same data getDetail() already returns — one page per disclosed credential: candidate name, credential type, disclosed fields (employer/role/dates/location, from the subject data captured 2026-09-15), all 6 checks with pass/fail, verification timestamp. Same source as the on-screen UI, so the PDF can't drift from what's displayed (the acceptance criterion above).
  • Render in-memory, stream the bytes straight back as the HTTP response (Content-Type: application/pdf, Content-Disposition: attachment) — no S3, no Attachment rows, nothing persisted
  • Frontend: replace the disabled placeholder "Download PDF" button with a real action
  • Failure case handled explicitly per the spec's own rule — clear visible error, never a dead-looking link

Phase 2 — secure S3 storage (after Phase 1 sign-off, not started until the rendered output is confirmed correct)

  • Build a small StorageService (@aws-sdk/client-s3) — upload + presigned, time-limited download URLs, not public bucket access, for genuinely secure retrieval
  • Wire VerificationReport.pdfS3Key to the real upload
  • Decide then (easier with real output in hand): re-render fresh every download vs. serve the stored copy vs. re-render-and-replace on demand

Not started yet — this is the plan, execution begins on Phase 1 next.