Invoice Submission¶
This is the happy path: a DGI-mode invoice goes from a GraphQL mutation to a fully-authorized document with PDF attached.
Entry points¶
Three GraphQL mutations can produce a PAC invoice:
| Mutation | File | When to use |
|---|---|---|
generate_invoice(order_id, mode) |
invoice_mutations.py |
Convert an existing order into an invoice. |
create_invoice(invoice) |
invoice_mutations.py |
Create a standalone invoice (no order). |
update_invoice(invoice) |
invoice_mutations.py |
Edit a draft. Validates retention if newly enabled. |
All three end up calling InvoicePacService.generate_pac_invoice when mode in {DGI, DGI_IMPORT}.
The submission flow¶
flowchart TD
Start[generate_invoice mutation] --> Mode{mode = DGI?}
Mode -- No, PROFORM --> Save[Save invoice
no PAC call]
Mode -- Yes --> Resolve[InvoicePacService
resolve RUC types]
Resolve --> RucCheck[RucVerificationService
verify_ruc]
RucCheck --> Numeration[PacUtilsService.get_numeration]
Numeration --> Office[Resolve office_id
to pac_office_id]
Office --> Retention[Resolve codigo_retencion
invoice override or client default]
Retention --> Factory[PacInvoiceFactory
create_invoice_data]
Factory --> Submit[PacService.generate_invoice]
Submit --> Alanube[POST /pan/v1/invoices]
Alanube -->|200| Persist[Persist:
document_id, invoice_number/cufe
pac_input, pac_response
legal_status = PAC_AUTHORIZED]
Alanube -->|4xx| Reject[legal_status = PAC_REJECTED
raise PacError]
Persist --> Enqueue[Enqueue pac_status_poll_task]
Enqueue --> Done[Return Invoice]
Step-by-step¶
1. RUC type resolution¶
InvoicePacService._resolve_ruc_types looks up the RUC of the receiver and determines:
- Contributor type —
NATURAL(individual) orJURIDICO(legal entity). - Receptor type —
CONTRIBUYENTE,GOBIERNO, orCONSUMIDOR_FINAL.
This is done by calling RucVerificationService.verify_ruc which hits GET /pan/v1/check-digit.
When adhoc_client_ruc is set on the invoice, that RUC is verified instead of the registered client's RUC. See RUC Verification.
2. Numeration¶
PacUtilsService.get_numeration generates a 10-digit numeration string used for the document. The exact formula combines:
- ASCII sum of the tenant ID
- A random 3-digit number
- A monotonic per-tenant sequence counter
The result is left-padded to 10 digits. It is not the CUFE — the CUFE comes back from Alanube. This number is just the document number on Alanube's side.
3. Office resolution¶
The invoice's office_id is mapped to the Office.pac_office_id. The PAC payload references the office Alanube created, not Cifras's internal UUID. Without a valid office mapping, submission fails.
4. Retention¶
If the receiver has invoice_retention=True, the document needs a retention code (codigo_retencion). Lookup order:
- Per-invoice override (set via the mutation input).
- The client's default
retention_code. - Otherwise raise
MissingRetentionCodeError.
5. CPBS (only for government receivers)¶
If receptor_fe_type == GOBIERNO, every line item must have a cpbs_code (Codificación Panameña de Bienes y Servicios). If any item is missing it, the factory raises an error before submission.
The CPBS catalog can be fetched via the goods_and_services GraphQL query, which is cached for 24 hours.
6. Build the payload¶
PacInvoiceFactory.create_invoice_data returns a PacInvoice dataclass containing:
DatosGenerales(header: issue date, billing point, security code, document type, etc.)InformacionReceptor(buyer info)list[ItemPac]with prices, ITBMS rates, optional CPBSTotaleswith payment methods and optional retentionid_office
7. Submit¶
PacService.generate_invoice does:
POST {pac_base_url}/pan/v1/invoices?idCompany={pac_company_id}
Authorization: Bearer {pac_api_key}
Content-Type: application/json
<PacInvoice as JSON>
The call is wrapped with tenacity retry: 2 attempts, exponential backoff (2s–10s) on aiohttp.ClientResponseError.
8. Persist¶
On success, the service writes back to the invoice row:
document_id←response.document.idinvoice_number←response.document.cufelegal_status←'PAC_AUTHORIZED'pac_input← the full JSON payloadpac_response← the full JSON response
pac_input and pac_response are intentionally stored verbatim. They are the audit trail when something looks wrong months later.
9. Async PDF retrieval¶
The mutation enqueues pac_status_poll_task. See Polling Fallback.
In parallel, Alanube will (eventually) push a webhook — see Webhooks. Whichever path arrives first wins; both are idempotent.
Error paths¶
| Failure | Where | What happens |
|---|---|---|
| RUC verification fails | RucVerificationService |
Raises; mutation returns error. Nothing persisted. |
| Missing retention code | InvoicePacService |
MissingRetentionCodeError. Nothing persisted. |
| Missing CPBS for gov | PacInvoiceFactory |
ValueError("AP3063_MESSAGE"). Nothing persisted. |
POST /invoices 4xx |
PacService |
PacError raised. We may still persist pac_input for debugging — check the call site. |
POST /invoices 5xx / network |
PacService |
Retried twice. Then PacError. |
Idempotency¶
Submitting twice is not idempotent on Alanube's side — you'll get two documents. The mutations therefore guard against re-submission by checking legal_status before calling PAC. If a record already has legal_status set, mutations refuse to re-submit.