Reference
This page is the detailed engineering reference for the e-invoicing feature. For a shorter introduction, see the E-Invoicing. For Invopop API specifics, see Invopop Integration.
Jira Epic: PLU-778
Project Status (March 2026)
The core end-to-end flow is implemented and working in staging. The feature is not yet production-ready. The main gaps before go-live are: a fix for VAT rounding discrepancies, a PMS UI for workspace configuration, and wiring the mark_as_issued HTTP adapter stub.
Total tickets in epic
64 (+ 3 subtasks outside epic)
Done
39 (+ PLU-4494, PLU-4495, PLU-4496 as subtasks)
Dismissed
10
Backlog
15
In Progress
0
Architecture
E-invoicing uses the standard CDC/RPC/Kafka patterns already in the codebase, with LVDAM owning invoice data and state, and Abacus owning the e-invoicing decision logic, GOBL conversion, and Invopop integration. The e-invoicing-specific additions are the Issuing and Rejected document states, entity locking during submission, per-workspace Invopop configuration, and the Invopop webhook pipeline. For the full end-to-end flow walkthrough, entity locking details, and a suggested code reading order, see the Architecture page.
Document State Machine
Standard flow: Draft → Issued. E-invoicing introduces intermediate states:
Draft ──(e-invoicing)──► Issuing ──(accepted)──► Issued
│
└──(rejected)──► Rejected ──(re-submit)──► IssuingIssuing
Locked. All modification commands are blocked. Awaiting tax authority response
Rejected
Can be re-submitted (transitions back to Issuing with a new invoice number). Cannot be deleted or exported
GOBL Conversion
What's Supported
Regular invoices with full totals and tax breakdowns
Negative line items using
lineswith negative prices (notdiscounts/charges)Country-specific configuration via
get_regime_config/1(returns Verifactu addons and identity extensions for Spain)ID document mapping covering all LVDAM ID types:
Spanish IDs (DNI via
id_card, NIE viaspanish_resident_permit) map totax_idwith country"ES"Foreign passports map to
identitieswithes-verifactu-identity-type: "03"Foreign national IDs map to
identitieswithes-verifactu-identity-type: "04"Other documents (driver's license, student ID) map to
identitieswithes-verifactu-identity-type: "06"
Company recipients with
tax_number: Spanish companies usetax_id, non-Spanish useidentitieswithes-verifactu-identity-type: "04"
Credit Notes
The infrastructure for credit note references exists (referenced document IDs are tracked on the Document entity). GOBL conversion logic for credit notes has been prototyped but is not wired into the production submission pipeline. See the backlog section for the remaining tickets.
Corrective Invoices
Spanish corrective invoices (facturas rectificativas) are handled through the standard business process: issue a credit note (R4) to cancel the original, then issue a new invoice (F1) as the corrected version. No special GOBL document type is needed.
VAT and Rounding
LVDAM calculates VAT per line item from gross amounts (gross to net), while GOBL aggregates net amounts by rate and calculates tax on the total. This causes discrepancies of 1–10 cents. Bypass mode ($tags: ["bypass"]) is used as a temporary mitigation. The plan is to fix LVDAM's calculations to match GOBL (PLU-4358 + PLU-4627) and remove bypass mode (PLU-4666). For a detailed breakdown of the rounding methods and the remediation plan, see VAT Rounding.
Abacus E-Invoicing Flow
Issue Command (PLU-4233)
The Issue command handler checks e-invoicing eligibility using the document type and workspace configuration:
E-invoicing eligible: Calls LVDAM
issuingRPC, receivesdisplay_document_number, setssend_to_tax_authority?: true. The entity emits anIssuingevent only (notIssued).Not eligible: Calls LVDAM
issueRPC. The entity emits anIssuedevent (standard flow).
When issuing_at is set on the entity, it is locked. All modification commands are blocked except MarkAsAcceptedByTaxAuthority and MarkAsRejectedByTaxAuthority.
Blocked commands during Issuing: Credit, Refund, Reverse, Delete, UpdateRecipient, AddTransaction, RemoveTransaction, UpdateTransaction, SendToTaxAuthority.
OnIssuing Handler (PLU-4234)
Listens for Issuing events with send_to_tax_authority?: true and dispatches the SendToTaxAuthority command. This handler is the bridge between the document lifecycle and the e-invoicing integration.
Acceptance Flow (PLU-4236)
The MarkAsAcceptedByTaxAuthority handler:
Calls LVDAM
mark_as_issuedRPC (currently a stub — needs a LVDAM endpoint and adapter wiring)Entity sets
issued_at, clearsissuing_at, and sets acceptance fieldsEmits both
AcceptedByTaxAuthorityandIssuedeventsPattern matching on
document.issued_atdifferentiates e-invoicing from legacy flow
Rejection Flow (PLU-4237)
The MarkAsRejectedByTaxAuthority handler:
Calls LVDAM
mark_as_rejectedRPC (PUT /invoicing/rpc/documents/:uuid/reject)Entity clears
issuing_at, setsrejected_by_tax_authority_atandrejection_reasonEmits
RejectedByTaxAuthorityevent
Note: Clearing
issuing_atis critical. Without it,issuing?returns true indefinitely, which blocks all subsequent commands on the document.
Booking Document Denormalizers
PLU-4235: Handles
Issuingevent. Setsissuing_started_aton the bookings documents read model.PLU-4238: Handles
RejectedByTaxAuthorityevent. Clearsissuing_started_at, setsrejected_by_tax_authority_atandrejected_by_tax_authority_reason.
LVDAM RPC Endpoints
PUT /invoicing/rpc/documents/:uuid/issuing
PUT /invoicing/rpc/documents/:uuid/issuingTransitions a draft or rejected document to issuing state. Reserves an invoice number atomically. Skips all tax validations (Invopop handles validation).
Success (draft/rejected to issuing)
200
{ uuid, display_invoice_number, status: "issuing" }
Not in draft/rejected state
422
{ errors: [{field, message}] }
Invalid document type
422
{ errors: [{field: "base", message: "invoiceInvalidDocumentType"}] }
Document not found
410
{ errors: [...] }
Empty line items
422
Raises ArgumentError (prevents number consumption)
Note: This endpoint is not idempotent. It returns 422 for already-issuing documents. This is flagged as a follow-up.
PUT /invoicing/rpc/documents/:uuid/reject
PUT /invoicing/rpc/documents/:uuid/rejectTransitions an issuing document to rejected state. This endpoint is idempotent (already-rejected documents return 200).
Success (issuing to rejected)
200
{ uuid, status: "rejected", ... }
Already rejected (idempotent)
200
{ uuid, status: "rejected", ... }
Not in issuing/rejected state
422
{ errors: [{field, message}] }
Document not found
410
{ errors: [...] }
PUT /invoicing/rpc/documents/:uuid/issue (existing)
PUT /invoicing/rpc/documents/:uuid/issue (existing)Standard issue endpoint. When called by Abacus after tax authority acceptance, transitions issuing to issued.
Rejected State Behaviour (LVDAM)
Status stored in JSONB
payload['status']Rejected documents cannot be deleted or exported
Rejected documents can transition back to
issuing(re-submission with a new invoice number)rejectedtodrafttransition is blockedexportablescope excludesdraft,issuing, andrejectedget_draft_or_rejected_invoicefinder supports both initial submission and re-submissionTransitionToIssuingStateaccepts bothdraftandrejecteddocumentsTransitionToRejectedStateis idempotent
Workspace Configuration
E-invoicing is configured per workspace using the WorkspaceEInvoicingConfiguration entity (Abacus entity store).
Each configured workspace stores:
An encrypted Invopop API token (AES-256 via
Shared.Infrastructure.Vault)A workflow ID (which Invopop processing pipeline to use)
A company ID (the supplier's UUID in Invopop)
When a document is issued, the system checks whether the workspace has e-invoicing configured. If not, the document follows the standard flow.
Note: There is no PMS UI to configure these settings yet (PLU-4223). Configuration is currently done manually in staging only.
Eventual Consistency
Problem
Multiple asynchronous touchpoints create timing issues:
LVDAM to Abacus CDC events can be delayed
Document data in Abacus can be stale when event handlers fire
PDF generation can show stale data for newly issued invoices
Mitigation
A sync_document function was implemented (PLU-4228) to refetch documents from LVDAM via RPC before critical operations. However, it was rolled back because the synchronous handler could block the Kafka queue on bad data. Re-enabling requires the Kafka inbox pattern (PLU-4309), which moves Kafka messages into Oban for independent processing.
The current e-invoicing Issue flow uses best-effort CDC sync. Failures are logged but do not block the flow.
Error Recovery
When a workflow fails (e.g. rejected at verifactu.generate or verifactu.send), entries can be corrected via PATCH and resubmitted:
Constraint: Once Verifactu has accepted a record, changing the invoice code and resubmitting fails with error 3002 ("No existe el registro de facturacion"). Verifactu maintains a hash chain linking records. To correct an accepted invoice, issue a credit note instead.
Current limitation: The Abacus adapter only uses PUT (create-only). If we need to support resubmission of corrected entries, PATCH support needs to be added to the adapter.
Known Issues and Gaps
Blocking Go-Live
Rounding (PLU-4358 + PLU-4627): VAT calculation differences between LVDAM and GOBL. The decision is to fix our calculations to match GOBL (net to gross, aggregated per rate) and remove bypass mode. Without this fix, invoices will be intermittently rejected.
PMS workspace config UI (PLU-4223): No UI to configure Invopop credentials per workspace. Currently staging-only, manual.
Abacus HTTP adapter stub:
mark_as_issuedin the HTTP adapter still raises "not implemented". A corresponding LVDAM endpoint is needed to complete the acceptance flow.
Abacus-LVDAM Alignment Gap
mark_as_issuedadapter is a stub that raises. Needs a corresponding LVDAM endpoint and adapter wiring
Rejection Handling Strategy (Open Decision)
Two strategies are under discussion:
Strategy A (Original): Rejected document is edited in place and resubmitted directly.
Strategy B (Proposed): On rejection, mark the invoice as rejected, immediately create a draft corrective invoice, fix the rejected invoice to get it to issued, then credit note it. Block the corrective from being issued until the rejected invoice reaches issued. Start with manual fix and credit note; build UI only if rejections are frequent.
This decision affects the Abacus-side implementation significantly and has not been finalised.
Other Issues
Issuing endpoint not idempotent: Returns 422 for already-issuing documents (unlike
issueandreject)No production configuration exists: Invopop workspace configuration is staging-only
UUID v7 required: Invopop Silo rejects standard v4 UUIDs; all entry/job IDs must be UUID v7
Adapter uses PUT only: No PATCH support for error recovery or resubmission of corrected entries
Kafka inbox pattern blocks two tickets: PLU-4289 (credit note validations) and PLU-4309 (sync_document) need the Kafka inbox pattern first
Backlog
Critical for Go-Live
PLU-4358
Fix rounding to match GOBL (remove bypass mode)
None (LVDAM-side)
Medium
PLU-4627
Fix double rounding in prorated billing services
Related to PLU-4358
Medium
PLU-4223
PMS integration for Invopop (workspace config UI)
None
Spike first
(No ticket)
Wire Abacus HTTP adapter to real LVDAM endpoints
PLU-4494/4496 merged
Small-Medium
Important for UX
PLU-3969
Parent story: Introduce Issuing and Rejected States (covers PMS UI and full lifecycle; most sub-work done)
PLU-4149
Display and Filter Issuing/Rejected docs in Invoice List View
PLU-4224
Expose rejected reason via GQL
PLU-4225
Filter issuing/rejected via GQL
PLU-4226
PMS documents view: issuing/rejected filters
PLU-4227
Show rejected reason in PMS
PLU-4084
Display error when supplier is not registered
PLU-4220
Remove invoice number on rejection
Credit Notes via E-Invoicing
(No ticket)
Credit note GOBL conversion (code exists from PLU-4179 investigation, needs wiring)
PLU-4293
LVDAM calls Abacus to issue credit notes
PLU-4288
Backfill referenced document data
PLU-4289
Re-enable credit note type validations (blocked by Kafka inbox)
Blocked
PLU-4309
Re-enable sync_document function
Kafka inbox pattern
PLU-4289
Credit note validations
Kafka inbox pattern
Implementation Patterns
Command Design (Abacus)
External timestamps (enforce: true)
Syncing from CDC or external system
Credit, Reverse, Refund
System timestamps (no field)
Abacus is source of truth
SendToTaxAuthority
Mixed source (enforce: false)
Either CDC or Abacus-initiated
Issue
LVDAM RPC Operations
New RPC business logic uses Dry::Operation with:
Nested
Dry::Validation::ContractNamed steps (validate, find, transition, persist)
on_failurehook for loggingController pattern-matches on
result.success?Validation failures return a 4-tuple:
Failure([result, :validation_error, message, errors_hash])Operations rescue known exceptions and map to named
Failurecodes
Last updated