PDF Attachment¶
When a document is authorized, Alanube hosts a signed PDF. Cifras downloads it and stores it as a File row attached to the invoice/credit.
Three paths¶
flowchart LR
A[Webhook handler
PacWebhookService] --> Z
B[Polling task
pac_status_poll_task] --> Z
C[Manual mutation
attach_pac_pdf] --> Z
Z[(File row
linked to entity)]
All three eventually call:
_get_pdf_url_from_pac(document_id, pac_company_id, endpoint)—GET /pan/v1/{endpoint}/{document_id}?documents=pdf. Endpoint isinvoicesorcredit-notes._download_and_attach_pdf(entity_id, entity_type, pdf_url, cufe)— downloads the bytes, wraps them in anUploadFile, and callsFileService.create_file(...).
The file is stored with:
filename = f"{cufe}.pdf"entity_id = invoice.idorcredit.identity_type = SourceType.INVOICESorSourceType.CREDITSstatus = FileStatus.COMPLETED
The shared service¶
app/graphql/common/services/pac_pdf_attachment_service.py
This service exists specifically to power the manual path, but it deliberately replicates the webhook handler's logic. If you find yourself adding behavior to one, mirror it in the other.
Manual attachment¶
mutation {
attachPacPdf(
sourceId: "<invoice-or-credit-uuid>"
sourceType: INVOICE # or CREDIT_NOTE
)
}
When to use it:
- Webhook never arrived and polling timed out.
- A document was authorized before this code path existed (legacy import).
- Alanube re-issued a PDF and you want a fresh copy.
Failure modes¶
| Failure | Behavior |
|---|---|
document_id is null on the entity |
ValueError. The document never reached PAC. |
pac_company_id missing on context |
ValueError. Tenant misconfigured. |
PAC returns no pdf field |
Method returns False, no exception. PDF wasn't ready yet. |
| Download HTTP error | Logged with logger.exception, method returns False. |
FileService.create_file fails |
Bubbles up. Investigate file storage health. |
The webhook handler swallows these (returns False) so it doesn't 5xx back to Alanube and trigger their retries — Alanube's retries are not helpful here, since we already have the document.
Where the PDF actually lives¶
FileService.create_file is the abstraction over file storage. Underneath it's S3-compatible (see s3fs in deps). The File model holds the storage key and exposes a download URL via the GraphQL API.
Duplicate PDFs¶
As noted in Webhooks, the same document can pick up multiple PDF attachments if both the webhook and the poller succeed, or if the webhook fires twice. Until that's fixed, code that surfaces the PDF should pick the most recent file by created_at.