Architecture¶
Layers, top to bottom¶
flowchart TD
GraphQL[GraphQL Mutations & Queries
app/graphql/**/mutations/]
DomainSvc[Domain Services
InvoicePacService, CreditPacService
app/graphql/**/services/]
Common[Common Services
PacUtilsService, PacPdfAttachmentService
app/graphql/common/services/]
Factory[Factories
PacInvoiceFactory, PacCreditNoteFactory
app/wrappers/pac/factories/]
LowLevel[Low-level Client
PacService
app/wrappers/pac/pac_service.py]
HTTP[aiohttp ClientSession]
Alanube[Alanube REST API]
GraphQL --> DomainSvc
DomainSvc --> Factory
DomainSvc --> Common
DomainSvc --> LowLevel
Common --> LowLevel
Factory --> LowLevel
LowLevel --> HTTP
HTTP --> Alanube
The rule: only PacService and a few sibling clients in app/wrappers/pac/ make outbound HTTP calls to Alanube. Everything else either calls them or transforms data for them.
Outbound flow: issuing an invoice¶
sequenceDiagram
autonumber
participant Client as GraphQL Client
participant Mut as invoice_mutations
participant Svc as InvoicePacService
participant Fac as PacInvoiceFactory
participant Pac as PacService
participant Alanube as Alanube
participant Worker as taskiq worker
Client->>Mut: generate_invoice(order_id, mode=DGI)
Mut->>Svc: generate_pac_invoice(event)
Svc->>Svc: resolve_ruc_types()
Svc->>Fac: create_invoice_data(...)
Fac-->>Svc: PacInvoice payload
Svc->>Pac: generate_invoice(payload)
Pac->>Alanube: POST /pan/v1/invoices
Alanube-->>Pac: { id, legalStatus, cufe }
Pac-->>Svc: PacInvoiceResponse
Svc->>Svc: persist document_id, cufe, pac_input, pac_response
Svc-->>Mut: Invoice
Mut->>Worker: enqueue pac_status_poll_task
Mut-->>Client: Invoice
Inbound flow: webhook delivers a status¶
sequenceDiagram
autonumber
participant Alanube
participant Router as webhooks/router.py
participant Wh as PacWebhookService
participant Pac as PacService
participant Files as FileService
Alanube->>Router: POST /webhooks/pac/{company_id}
X-Pac-Signature: ...
Router->>Router: verify signature
Router->>Router: lookup tenant by pac_company_id
Router->>Wh: process_document_status(payload)
Wh->>Wh: find invoice/credit by document_id or cufe
alt PAC_AUTHORIZED or DGI_AUTHORIZED
Wh->>Pac: GET /pan/v1/invoices/{id}?documents=pdf
Pac-->>Wh: { pdf: }
Wh->>Files: download + create_file()
end
Wh->>Wh: update legal_status
Router-->>Alanube: 200 OK
The three paths a PDF can land on a document¶
flowchart LR
A[Webhook arrives
preferred] --> Z[(File attached
to entity)]
B[Polling task
fallback] --> Z
C[Manual mutation
attach_pac_pdf
operator action] --> Z
All three paths share the same final step: call _get_pdf_url_from_pac() → download → FileService.create_file(). This redundancy is intentional — Alanube's webhooks are best-effort.
Multi-tenancy¶
PAC is strictly per-tenant:
- Each tenant has its own
pac_company_idissued by Alanube (tenant_model.py). - Webhook URLs include the company ID in the path (
/webhooks/pac/{company_id}) so we can resolve back to the tenant. - Every outbound call to Alanube includes
?idCompany={pac_company_id}from the request context.
If context.pac_company_id is missing, the call fails fast with ValueError("PAC company ID is not configured for this tenant").
Dependency injection¶
We use aioinject. Services declare their dependencies in __init__ and are wired up in app/core/providers/. For PAC code, the relevant providers are typically:
Settings— forpac_base_url,pac_api_key, etc.aiohttp.ClientSession— shared HTTP client.ContextWrapper— request context, includingpac_company_idand current tenant.FileService— to attach PDFs.