Skip to content

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 typeNATURAL (individual) or JURIDICO (legal entity).
  • Receptor typeCONTRIBUYENTE, GOBIERNO, or CONSUMIDOR_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:

  1. Per-invoice override (set via the mutation input).
  2. The client's default retention_code.
  3. 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 CPBS
  • Totales with payment methods and optional retention
  • id_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_idresponse.document.id
  • invoice_numberresponse.document.cufe
  • legal_status'PAC_AUTHORIZED'
  • pac_input ← the full JSON payload
  • pac_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.