Skip to content

Document Lifecycle

A PAC document goes through two layered status machines:

  1. Legal status (legal_status column) — driven by PAC and DGI.
  2. Business status (invoice_status / credit_status) — driven by Cifras (payments, voids, etc.).

This page covers both.

stateDiagram-v2
    [*] --> Submitting: POST /invoices
    Submitting --> PAC_AUTHORIZED: 200 OK with cufe
    Submitting --> PAC_REJECTED: 4xx with errors
    PAC_AUTHORIZED --> DGI_AUTHORIZED: webhook / poll
    PAC_AUTHORIZED --> DGI_REJECTED: webhook / poll
    DGI_AUTHORIZED --> [*]
    DGI_REJECTED --> [*]
    PAC_REJECTED --> [*]
Status Set by Meaning
(none) Never submitted, or in PROFORM mode.
PAC_AUTHORIZED Alanube The PAC accepted the document and forwarded it to the DGI. PDF is now available.
DGI_AUTHORIZED DGI (via Alanube webhook/poll) DGI has fully authorized. This is the terminal "valid" state.
PAC_REJECTED Alanube Validation failed at the PAC. Document was never sent to DGI. Terminal.
DGI_REJECTED DGI DGI rejected the document. Terminal.

Terminal statuses are DGI_AUTHORIZED, DGI_REJECTED, PAC_REJECTED. Once a document hits a terminal state, it does not change again — except by being voided (see Voiding).

Business status — invoices

app/graphql/invoices/models/invoice_status.py

Status Trigger
UNPAID Default on creation.
PARTIALLY_PAID Some but not all payments applied.
PAID Sum of payments ≥ total.
OVERPAID Sum of payments > total.
PAST_DUE Past due date with balance remaining.
PARTIALLY_CANCELED Credit notes applied for less than full balance.
CANCELED Credit notes fully cancel the invoice.
VOID Voided in PAC via void_invoice.

Business and legal status are independent. An invoice can be DGI_AUTHORIZED + PAID, or DGI_AUTHORIZED + VOID, or PAC_REJECTED + UNPAID (the latter is effectively dead).

Business status — credits

app/graphql/credits/models/credit_status.py

Status Trigger
UNAPPLIED Default on creation.
APPLIED Credit applied against an invoice.
CANCELED Marked canceled internally.
VOID Voided in PAC via void_credit.

State transitions across actors

sequenceDiagram
    participant U as User
    participant C as Cifras
    participant P as PAC
    participant D as DGI

    U->>C: generate_invoice(mode=DGI)
    Note over C: legal_status = NULL
invoice_status = UNPAID C->>P: POST /invoices P-->>C: 200 { document_id, cufe, status: PAC_AUTHORIZED } Note over C: legal_status = PAC_AUTHORIZED P->>D: forward signed XML D-->>P: authorization protocol P->>C: webhook { legalStatus: DGI_AUTHORIZED } Note over C: legal_status = DGI_AUTHORIZED Note over C: PDF downloaded and attached U->>C: register_payment(...) Note over C: invoice_status = PAID

Why two trips to terminal status?

A submission that succeeds at the PAC isn't necessarily approved by the DGI. The PAC does structural validation (schema, signatures, RUC format). The DGI does fiscal validation (does this RUC exist, is it active, is the contributor allowed to issue this kind of doc, etc.).

So PAC_AUTHORIZED is best thought of as "syntactically valid, awaiting DGI verdict." DGI_AUTHORIZED is "fully legal."

In practice the gap is seconds to minutes. We have both webhooks and polling because Alanube's webhook delivery isn't 100% reliable — see Polling Fallback.