Document Lifecycle¶
A PAC document goes through two layered status machines:
- Legal status (
legal_statuscolumn) — driven by PAC and DGI. - Business status (
invoice_status/credit_status) — driven by Cifras (payments, voids, etc.).
This page covers both.
Legal status¶
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.