Skip to content

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:

  1. _get_pdf_url_from_pac(document_id, pac_company_id, endpoint)GET /pan/v1/{endpoint}/{document_id}?documents=pdf. Endpoint is invoices or credit-notes.
  2. _download_and_attach_pdf(entity_id, entity_type, pdf_url, cufe) — downloads the bytes, wraps them in an UploadFile, and calls FileService.create_file(...).

The file is stored with:

  • filename = f"{cufe}.pdf"
  • entity_id = invoice.id or credit.id
  • entity_type = SourceType.INVOICES or SourceType.CREDITS
  • status = 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

pac_mutations.py

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.