Skip to content

fix(read-only-view): serve completed contract PDF via provider (fixes fleet-wide /pdf 500) - #60

Open
christopherneilson wants to merge 3 commits into
rendis:mainfrom
TetherEducation:claude/fix-readonly-view-completed-pdf
Open

fix(read-only-view): serve completed contract PDF via provider (fixes fleet-wide /pdf 500)#60
christopherneilson wants to merge 3 commits into
rendis:mainfrom
TetherEducation:claude/fix-readonly-view-completed-pdf

Conversation

@christopherneilson

Copy link
Copy Markdown

Impact (prod, ongoing)

Every completed contract's read-only PDF view returns HTTP 500. In the CRM this is the "Unable to load this PDF" screen, so schools/staff cannot view any signed contract. It fires many times per hour across all schools and has been broken since the read-only view shipped (~#888–#900). Signing itself works; only viewing the completed PDF fails.

Root cause

GET /public/view/{token}/pdfReadOnlyViewService.getCompletedReadOnlyViewPDF reads the sealed PDF from storage by a key derived from doc.CompletedPDFURL:

storageKey := completedPDFStorageKey(doc.CompletedPDFURL)
if storageKey == "" {
    return nil, "", errors.New("signed PDF not available for this document")
}

But the Documenso adapter records a URL there (result.CompletedPDFURL = &envResp.CompletedDocumentURL), and completedPDFStorageKey() deliberately returns "" for any http(s):// value — it expects a storage key. No step persists the sealed PDF to storage, so the key is always empty for provider-completed docs. That error string isn't in isPublicUserError, so it falls through to the default 500.

Confirmed in prod logs (tether-digital-signature-prod):

msg="public signing error"  error="signed PDF not available for this document"
GET .../doc-assembly/public/view/{token}/pdf  -> 500

The sibling GET /public/sign/{token}/download works precisely because it already falls back to downloading the sealed PDF from the provider when there is no storage key.

Fix

getCompletedReadOnlyViewPDF now mirrors that working /download path: when there is no storage key it downloads the sealed PDF from the signing provider (via an injected provider + attempt repo — wired through a new SetCompletedPDFProvider). Also maps "signed PDF not available for this document" to a clean 4xx instead of a 500.

Longer term the sealed PDF should be persisted to GCS on completion, so this serves from storage without a per-view provider round-trip (noted in a code comment).

Verification

  • go build ./core/...
  • go test ./core/internal/core/service/document/... ./core/internal/adapters/primary/http/controller/...
  • Wrapper (tools-doc-assembly) builds against this change ✓

@rendis — please merge + cut a release

This is prod-down for contract viewing across every school, so it needs a merge + release so tools-doc-assembly can bump the pin and deploy.

Process flag: doc-assembly lives under a personal account (github.com/rendis/…), so everyone else has read-only and literally cannot land a fix here — this prod-critical bug is currently single-owner-gated on you. Strongly recommend moving the repo to the TetherEducation org so urgent fixes and rollbacks aren't blocked on one person.

🤖 Generated with Claude Code

christopherneilson and others added 3 commits July 13, 2026 13:32
getCompletedReadOnlyViewPDF read the sealed PDF from storage by a key
derived from CompletedPDFURL, but the Documenso adapter records a URL
there (completedDocumentUrl) and completedPDFStorageKey() returns "" for
URLs, so every completed document returned
errors.New("signed PDF not available for this document") -> not in
isPublicUserError -> HTTP 500. This broke the read-only contract PDF
view fleet-wide since the feature shipped.

Fall back to downloading the sealed PDF from the signing provider (the
same path /public/sign/{token}/download already uses) when there is no
storage key, and map the not-available error to a 4xx instead of a 500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@christopherneilson

Copy link
Copy Markdown
Author

Follow-up pushed at c85f05b:

  • adds a regression test for completed documents whose CompletedPDFURL is a provider URL rather than a storage key;
  • safely handles an empty provider response instead of dereferencing it;
  • separates the storage and provider branches so the fallback stays within the repository's complexity limits.

Validated locally with make test, make build, and go build -tags=integration ./.... Repository-wide lint now reports only unrelated pre-existing gofmt findings in untouched template files.

@christopherneilson

Copy link
Copy Markdown
Author

Ownership update: this PR head now comes from TetherEducation/doc-assembly, following a repository transfer from the personal fork. The reviewed head remains c85f05b; the transfer changes ownership only and preserves the PR history.

@christopherneilson

Copy link
Copy Markdown
Author

Status note (2026-07-19): this fix has been running in production since 2026-07-17 via the org-fork pin (TetherEducation/doc-assembly@claude/fix-readonly-view-completed-pdf, wired through tools-doc-assembly#21/#22), and is verified end-to-end — the fleet-wide read-only /pdf 500 (TetherEducation/tools-doc-assembly#17) is resolved. It is now also part of the combined branch feat/embedded-signing-plus-pdf-fix (caa75b78, this + #59) which tools-doc-assembly pins going forward. PR stays open as the upstream source of truth; merging it upstream when you're back lets us drop the replace.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant