AP, AR, and E-Invoicing Architecture Research
CODITECT Financial Suite — Multi-National, Multi-Tenant SaaS
Date: 2026-02-20 Task: FPA.A.7.14.2 Scope: Accounts Payable, Accounts Receivable, E-Invoicing, Payment File Formats
Executive Summary
This document provides comprehensive architecture guidance for building Accounts Payable (AP), Accounts Receivable (AR), and E-Invoicing modules for the CODITECT Financial Suite — a multi-national, multi-tenant SaaS platform serving accounting professionals globally. The existing GL engine prototype (with multi-currency IAS 21, multi-tenant isolation, and Brazil SPED compliance) provides the foundation.
Key findings:
- AP Subledger integrates with GL via control accounts with a dedicated vendor transaction table per tenant, using PostgreSQL Row-Level Security for isolation
- Invoice OCR/AI leaders (ABBYY 99.5%, Veryfi 98.7%) deliver near-touchless processing; hybrid AI+OCR architectures are the production standard
- 3-Way Matching requires tolerance-based automation with exception routing — 90%+ straight-through processing is achievable
- E-Invoicing mandates now cover 40+ countries; the architecture must support both clearance (pre-validation) and post-audit models
- ISO 20022 became the exclusive SWIFT standard for cross-border payments in November 2025; all payment rails are converging
- Peppol BIS 3.0 / UBL is the global baseline for structured e-invoicing; country-specific formats layer on top
Part 1: Accounts Payable (AP)
1.1 AP Subledger Schema Design
Core Architecture Principle
The AP subledger holds granular vendor transaction detail; the GL holds summary control account balances. The cardinal rule: the sum of all open AP subledger balances must equal the GL AP control account balance at all times.
In a multi-tenant financial platform, each tenant (accounting firm client entity) has:
- Their own AP control account in the CoA
- Tenant-isolated vendor master records
- Tenant-isolated invoice and payment transactions
Recommended Schema (PostgreSQL, multi-tenant)
-- ============================================================
-- VENDOR MASTER
-- ============================================================
CREATE TABLE ap_vendors (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
vendor_code VARCHAR(50) NOT NULL,
legal_name VARCHAR(255) NOT NULL,
trade_name VARCHAR(255),
tax_id VARCHAR(100), -- EIN/TIN (US), CNPJ/CPF (BR), RFC (MX), VAT# (EU)
tax_id_type VARCHAR(30), -- EIN, SSN, CNPJ, CPF, RFC, VAT, GSTIN, etc.
vendor_type VARCHAR(30) NOT NULL, -- COMPANY, INDIVIDUAL, GOVERNMENT
currency_code CHAR(3) NOT NULL, -- ISO 4217 functional currency
payment_terms_code VARCHAR(30), -- NET30, NET60, IMMEDIATE, etc.
payment_method VARCHAR(30), -- ACH, WIRE, SEPA, PIX, CHECK
bank_account_id UUID REFERENCES ap_vendor_bank_accounts(id),
control_account_id UUID NOT NULL REFERENCES gl_accounts(id), -- AP control account
withholding_exempt BOOLEAN DEFAULT FALSE,
w9_received BOOLEAN DEFAULT FALSE,
w9_received_date DATE,
tin_verified BOOLEAN DEFAULT FALSE,
tin_verified_date DATE,
is_1099_vendor BOOLEAN DEFAULT FALSE,
vendor_status VARCHAR(20) DEFAULT 'ACTIVE', -- ACTIVE, INACTIVE, BLOCKED, PENDING
country_code CHAR(2) NOT NULL,
address_line1 VARCHAR(255),
address_line2 VARCHAR(255),
city VARCHAR(100),
state_province VARCHAR(100),
postal_code VARCHAR(20),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, vendor_code)
);
CREATE INDEX idx_ap_vendors_tenant ON ap_vendors(tenant_id);
CREATE INDEX idx_ap_vendors_tax_id ON ap_vendors(tenant_id, tax_id);
-- ============================================================
-- VENDOR BANK ACCOUNTS
-- ============================================================
CREATE TABLE ap_vendor_bank_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
vendor_id UUID NOT NULL REFERENCES ap_vendors(id),
account_type VARCHAR(20) NOT NULL, -- CHECKING, SAVINGS, WIRE, SEPA_IBAN, PIX_KEY
routing_number VARCHAR(20), -- US ABA routing
account_number_enc BYTEA, -- encrypted
iban VARCHAR(34), -- SEPA
bic_swift VARCHAR(11),
pix_key VARCHAR(255), -- PIX key (CPF, CNPJ, email, phone, random)
pix_key_type VARCHAR(20), -- CPF, CNPJ, EMAIL, PHONE, RANDOM
bank_name VARCHAR(100),
bank_country CHAR(2),
currency_code CHAR(3),
is_primary BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
verified_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- AP INVOICES (SUBLEDGER HEADER)
-- ============================================================
CREATE TABLE ap_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
vendor_id UUID NOT NULL REFERENCES ap_vendors(id),
invoice_number VARCHAR(100) NOT NULL,
vendor_invoice_number VARCHAR(100), -- vendor's own invoice number
invoice_type VARCHAR(30) NOT NULL, -- STANDARD, CREDIT_MEMO, DEBIT_MEMO, PREPAYMENT
invoice_status VARCHAR(30) NOT NULL DEFAULT 'DRAFT',
-- DRAFT, PENDING_APPROVAL, APPROVED, MATCHED, POSTED, PAYMENT_SCHEDULED,
-- PARTIALLY_PAID, PAID, VOIDED, ON_HOLD
-- Amounts (always store in both transaction and functional currency)
invoice_date DATE NOT NULL,
due_date DATE NOT NULL,
currency_code CHAR(3) NOT NULL,
subtotal_amount NUMERIC(20,6) NOT NULL,
tax_amount NUMERIC(20,6) DEFAULT 0,
total_amount NUMERIC(20,6) NOT NULL,
paid_amount NUMERIC(20,6) DEFAULT 0,
outstanding_amount NUMERIC(20,6) GENERATED ALWAYS AS (total_amount - paid_amount) STORED,
-- Functional currency (IAS 21)
functional_currency CHAR(3) NOT NULL,
exchange_rate NUMERIC(20,10) NOT NULL DEFAULT 1,
exchange_rate_date DATE,
total_functional NUMERIC(20,6) NOT NULL, -- total_amount * exchange_rate
-- GL Linkage
control_account_id UUID REFERENCES gl_accounts(id),
expense_account_id UUID REFERENCES gl_accounts(id),
gl_journal_id UUID REFERENCES gl_journal_entries(id), -- posted journal
-- 3-Way Match References
po_id UUID REFERENCES procurement_purchase_orders(id),
receipt_id UUID REFERENCES procurement_goods_receipts(id),
match_status VARCHAR(30), -- NOT_APPLICABLE, PENDING, MATCHED, EXCEPTION
-- Payment Terms
payment_terms_code VARCHAR(30),
discount_date DATE,
discount_percent NUMERIC(5,4), -- early payment discount
discount_amount NUMERIC(20,6),
-- Document Capture
source_type VARCHAR(30), -- MANUAL, OCR, EDI, EINVOICE_API, EMAIL
ocr_confidence_score NUMERIC(5,4), -- 0-1, from OCR pipeline
original_document_url TEXT, -- S3/GCS path
-- E-Invoice fields
einvoice_id VARCHAR(255), -- government-issued UUID (NF-e, CFDI, etc.)
einvoice_format VARCHAR(30), -- NFE, CFDI, FATTURAPA, UBL, CII, etc.
einvoice_xml_hash VARCHAR(64), -- SHA-256 of signed XML
-- Withholding
withholding_tax_amount NUMERIC(20,6) DEFAULT 0,
withholding_tax_rate NUMERIC(5,4),
withholding_account_id UUID REFERENCES gl_accounts(id),
created_by UUID,
approved_by UUID,
approved_at TIMESTAMPTZ,
posted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, vendor_id, vendor_invoice_number)
);
CREATE INDEX idx_ap_invoices_tenant_status ON ap_invoices(tenant_id, invoice_status);
CREATE INDEX idx_ap_invoices_due_date ON ap_invoices(tenant_id, due_date) WHERE invoice_status NOT IN ('PAID','VOIDED');
CREATE INDEX idx_ap_invoices_vendor ON ap_invoices(tenant_id, vendor_id);
-- ============================================================
-- AP INVOICE LINE ITEMS
-- ============================================================
CREATE TABLE ap_invoice_lines (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
invoice_id UUID NOT NULL REFERENCES ap_invoices(id),
line_number INTEGER NOT NULL,
po_line_id UUID, -- references PO line for 3-way match
receipt_line_id UUID, -- references GR line for 3-way match
description TEXT NOT NULL,
quantity NUMERIC(20,6),
unit_price NUMERIC(20,6),
line_amount NUMERIC(20,6) NOT NULL,
tax_code VARCHAR(30),
tax_rate NUMERIC(5,4),
tax_amount NUMERIC(20,6) DEFAULT 0,
expense_account_id UUID REFERENCES gl_accounts(id),
cost_center_id UUID,
project_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- AP PAYMENTS
-- ============================================================
CREATE TABLE ap_payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
payment_batch_id UUID REFERENCES ap_payment_batches(id),
vendor_id UUID NOT NULL REFERENCES ap_vendors(id),
payment_status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
-- PENDING, APPROVED, PROCESSING, SENT, CLEARED, RETURNED, VOIDED
payment_date DATE NOT NULL,
payment_method VARCHAR(30) NOT NULL, -- ACH, WIRE, SEPA_SCT, SEPA_SDD, PIX, BACS, CHECK
currency_code CHAR(3) NOT NULL,
payment_amount NUMERIC(20,6) NOT NULL,
exchange_rate NUMERIC(20,10) DEFAULT 1,
functional_amount NUMERIC(20,6) NOT NULL,
-- Bank reference numbers
bank_reference VARCHAR(100),
ach_trace_number VARCHAR(15), -- NACHA trace
sepa_end_to_end_id VARCHAR(35), -- SEPA E2E reference
pix_txid VARCHAR(77), -- PIX transaction ID
swift_uetr UUID, -- SWIFT gpi UETR
-- GL
payment_account_id UUID REFERENCES gl_accounts(id), -- bank/cash account
gl_journal_id UUID REFERENCES gl_journal_entries(id),
created_by UUID,
approved_by UUID,
approved_at TIMESTAMPTZ,
sent_at TIMESTAMPTZ,
cleared_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- AP PAYMENT APPLICATIONS (invoice ↔ payment M2M)
-- ============================================================
CREATE TABLE ap_payment_applications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
payment_id UUID NOT NULL REFERENCES ap_payments(id),
invoice_id UUID NOT NULL REFERENCES ap_invoices(id),
applied_amount NUMERIC(20,6) NOT NULL,
discount_taken NUMERIC(20,6) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(payment_id, invoice_id)
);
-- ============================================================
-- AP PAYMENT BATCHES (for batch payment runs)
-- ============================================================
CREATE TABLE ap_payment_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
batch_name VARCHAR(255) NOT NULL,
payment_method VARCHAR(30) NOT NULL,
batch_status VARCHAR(30) DEFAULT 'DRAFT',
-- DRAFT, PENDING_APPROVAL, APPROVED, GENERATING_FILE, FILE_READY,
-- SUBMITTED_TO_BANK, PARTIALLY_PROCESSED, COMPLETED, FAILED
pay_date DATE NOT NULL,
currency_code CHAR(3) NOT NULL,
total_amount NUMERIC(20,6),
payment_count INTEGER,
-- File generation
bank_file_format VARCHAR(30), -- NACHA, SEPA_PAIN001, BACS_STD18, PIX_API
bank_file_url TEXT, -- generated file S3/GCS path
bank_file_hash VARCHAR(64),
submitted_at TIMESTAMPTZ,
bank_ack_reference VARCHAR(100),
created_by UUID,
approved_by UUID,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
GL Integration Pattern
When an AP invoice is posted, the system generates a GL journal:
DR Expense Account [invoice line amounts]
DR Input VAT Recoverable [tax amount, if applicable]
CR AP Control Account [total invoice amount] -- subledger control
When an AP payment is made:
DR AP Control Account [payment amount]
CR Bank/Cash Account [payment amount]
Reconciliation invariant:
-- AP subledger must equal AP control account GL balance
SELECT
SUM(outstanding_amount) AS subledger_balance
FROM ap_invoices
WHERE tenant_id = $1
AND invoice_status NOT IN ('DRAFT', 'VOIDED')
-- Must equal:
SELECT balance FROM gl_account_balances
WHERE tenant_id = $1 AND account_id = $ap_control_account_id AND period = $period;
1.2 Invoice Capture Pipeline: OCR/AI Document Parsing
Accuracy Benchmarks (2025-2026)
| Provider | Accuracy | Speed | Strengths | Pricing Model |
|---|---|---|---|---|
| ABBYY FlexiCapture | 99.5% structured, 97%+ semi-structured | ~2-3s | Highest documented accuracy; 1.5B invoices/year | Per-page or enterprise license |
| Veryfi | 98.7% field-level | <3s (sub-3s benchmark leader) | Speed + accuracy combined; REST API first | Per-document API |
| Rossum | ~97-98% | 3-5s | Self-learning; minimal template setup | SaaS per-document |
| Nanonets | ~96-98% | 3-6s | Deep learning; excellent for unstructured | Per-page API |
| Mindee | ~95-97% | 2-4s | Developer-friendly; pre-built + custom models | Freemium + per-page |
| Traditional OCR only | 85-95% | 1-2s | Fast on clean structured docs | Varies |
Industry standard: AI+ML hybrid models achieve ~99% vs. OCR-only at 85-95%. By 2025, ~75% of AP departments use some form of AI/automation.
Recommended Capture Pipeline Architecture
[Invoice Sources]
│
├── Email ingestion (IMAP/SMTP listener)
├── Vendor portal upload (REST API)
├── EDI 810 feed
├── E-Invoice API (government portals)
└── Scan/PDF upload (UI)
│
▼
[Document Classification Service]
- Invoice vs. credit memo vs. statement
- Vendor identification (logo, address matching)
- Language detection
│
▼
[OCR/AI Extraction Engine] ← ABBYY / Veryfi / Rossum (configurable per tenant)
Extracted fields:
- vendor_name, vendor_tax_id
- invoice_number, invoice_date, due_date
- line_items[] (description, qty, unit_price, amount)
- subtotal, tax_amount, total_amount
- currency_code, payment_terms
- PO_number (for 3-way match linkage)
│
▼
[Validation & Enrichment Service]
- Vendor lookup (fuzzy match to vendor master)
- Duplicate invoice detection
- Currency/amount format normalization
- Confidence score thresholding
│
├── confidence > 0.95 → Auto-approve for matching
├── 0.80-0.95 → Route to human review queue
└── < 0.80 → Manual data entry required
│
▼
[3-Way Match Engine] (see §1.3)
Key Implementation Decisions
-
Abstraction layer: Build a
DocumentParserAdapterinterface so the OCR provider is swappable per tenant or per document type. Some tenants may have pre-existing ABBYY licenses; others will use Mindee's API. -
Training corpus: For each provider, maintain a tenant-specific training corpus. Rossum and Nanonets support self-supervised learning from corrections.
-
Fallback chain: OCR provider → LLM extraction fallback (GPT-4o / Claude with vision) → human queue.
-
Storage: Store original PDF alongside extracted JSON. The extracted data is mutable (corrections); the original is immutable (audit trail).
1.3 Three-Way Matching: PO ↔ Receipt ↔ Invoice
Matching Logic Architecture
[ap_invoice] ←→ [procurement_purchase_orders] ←→ [procurement_goods_receipts]
│ │ │
invoice_total po_total_value receipt_total_value
invoice_lines po_lines[] receipt_lines[]
Tolerance rules (configurable per tenant):
CREATE TABLE ap_matching_tolerance_rules (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
rule_name VARCHAR(100),
match_level VARCHAR(20), -- HEADER, LINE
tolerance_type VARCHAR(20), -- PERCENTAGE, FIXED_AMOUNT
tolerance_value NUMERIC(10,4), -- e.g., 0.02 = 2%, or 5.00 = $5
auto_approve BOOLEAN DEFAULT TRUE, -- auto-approve within tolerance
applies_to VARCHAR(30), -- PRICE, QUANTITY, TOTAL
currency_code CHAR(3) -- NULL = all currencies
);
Match result states:
| Status | Condition | Action |
|---|---|---|
FULLY_MATCHED | All lines match within tolerance | Auto-approve for payment |
WITHIN_TOLERANCE | Minor variance within configured threshold | Auto-approve with note |
PRICE_VARIANCE | Unit price differs > tolerance | Route to procurement approval |
QUANTITY_VARIANCE | Quantity received ≠ invoiced (outside tolerance) | Route to warehouse |
PO_NOT_FOUND | No PO reference | Route to AP manager |
RECEIPT_PENDING | PO found, no receipt yet | Hold invoice (receipt-required flag) |
DUPLICATE | Duplicate invoice detected | Block and notify |
Matching algorithm (line-level):
interface MatchResult {
status: MatchStatus;
matchedLines: LineMatch[];
totalVariance: Money;
variancePercent: number;
autoApproved: boolean;
exceptions: MatchException[];
}
function performThreeWayMatch(
invoice: APInvoice,
po: PurchaseOrder,
receipt: GoodsReceipt,
rules: ToleranceRule[]
): MatchResult {
const lineMatches = invoice.lines.map(invoiceLine => {
const poLine = findPOLine(po, invoiceLine);
const receiptLine = findReceiptLine(receipt, invoiceLine);
const priceDelta = Math.abs(invoiceLine.unitPrice - poLine.unitPrice);
const qtyDelta = Math.abs(invoiceLine.quantity - receiptLine.acceptedQty);
const rule = findApplicableRule(rules, 'PRICE');
return {
invoiceLineId: invoiceLine.id,
priceVariance: priceDelta,
qtyVariance: qtyDelta,
withinTolerance: isWithinTolerance(priceDelta, invoiceLine.unitPrice, rule),
status: computeLineStatus(priceDelta, qtyDelta, rule)
};
});
return aggregateMatchResult(lineMatches);
}
1.4 Payment Run Orchestration
Payment Run State Machine
DRAFT → PENDING_APPROVAL → APPROVED → GENERATING_FILE
→ FILE_READY → SUBMITTED_TO_BANK → PARTIALLY_PROCESSED
→ COMPLETED | FAILED
Bank File Formats
NACHA/ACH (US)
- File structure: File Header → Company/Batch Header → Entry Detail Records → Addenda Records → Batch Control → File Control
- Key fields: Routing Number (9 digits), Account Number, SEC Code (CCD=corporate-to-corporate, CTX=with addenda, PPD=personal)
- IAT entries for international ACH
- NACHA published ISO 20022-to-ACH mapping guide (pain.001 → NACHA translation)
SEPA Credit Transfer (EU) — pain.001
<CstmrCdtTrfInitn>
<GrpHdr>
<MsgId>BATCH-2026-001</MsgId>
<CreDtTm>2026-02-20T09:00:00</CreDtTm>
<NbOfTxs>45</NbOfTxs>
<CtrlSum>125000.00</CtrlSum>
</GrpHdr>
<PmtInf>
<PmtMtd>TRF</PmtMtd>
<PmtTpInf><SvcLvl><Cd>SEPA</Cd></SvcLvl></PmtTpInf>
<ReqdExctnDt>2026-02-21</ReqdExctnDt>
<Dbtr>...</Dbtr>
<CdtTrfTxInf>
<EndToEndId>PAYMENT-UUID</EndToEndId>
<Amt><InstdAmt Ccy="EUR">1500.00</InstdAmt></Amt>
<CdtrAcct><Id><IBAN>DE89370400440532013000</IBAN></Id></CdtrAcct>
</CdtTrfTxInf>
</PmtInf>
</CstmrCdtTrfInitn>
PIX (Brazil) — REST API via BACEN/PSP
- PIX uses REST API (not file-based): POST to
/v2/cob(charge) or/v2/pix(payment) - Key fields:
txid(UUID, 26-35 alphanumeric), chave (PIX key), valor (amount), devedor/favorecido - Pix Automatico launched June 2025 for recurring payments
- Pix por Aproximacao (NFC contactless) launched February 2025
- By 2024: 63 billion transactions; single-day record 250.5M on April 6, 2024
BACS (UK)
- Two-day settlement cycle; file formats: Standard 18 (credit), Direct Debit Instructions
- Moving toward Faster Payments Service (FPS) for instant settlement
Payment Approval Workflow
CREATE TABLE ap_payment_approval_rules (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
threshold_min NUMERIC(20,6), -- lower bound
threshold_max NUMERIC(20,6), -- upper bound (NULL = unlimited)
currency_code CHAR(3), -- NULL = all currencies
approver_role VARCHAR(50), -- CFO, AP_MANAGER, CONTROLLER
required_count INTEGER DEFAULT 1, -- dual approval for large amounts
payment_method VARCHAR(30) -- NULL = all methods
);
Dual-control pattern for large payments (>$50K):
- AP staff creates payment batch
- AP Manager approves batch
- CFO/Controller approves if > $50K threshold
- System generates bank file only after all approvals obtained
- Immutable audit log entry created at each state transition
1.5 Vendor Management & Tax Compliance
Vendor Onboarding Workflow
[Vendor Portal Invite] → [Self-Registration]
→ [Tax Form Collection]
├── US Vendors: W-9 (TIN certification)
├── Foreign Vendors: W-8BEN / W-8BEN-E
├── Brazil: Cartão CNPJ or CPF
├── EU: VAT Registration Certificate
└── India: GSTIN Certificate
→ [TIN Verification] (IRS TIN matching API or Avalara)
→ [Bank Account Verification] (microdeposit or Plaid/Stripe instant)
→ [Sanctions Screening] (OFAC, EU, UN lists)
→ [Vendor Approved]
1099/W-9 Architecture (US)
CREATE TABLE ap_vendor_tax_forms (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
vendor_id UUID NOT NULL REFERENCES ap_vendors(id),
form_type VARCHAR(20) NOT NULL, -- W9, W8BEN, W8BENE, W8ECI
tax_year INTEGER,
tin VARCHAR(11), -- encrypted SSN or EIN (stored encrypted)
tin_type VARCHAR(10), -- SSN, EIN
entity_type VARCHAR(50), -- Individual, Corporation, LLC, Partnership
exempt_payee_code VARCHAR(5),
fatca_exempt BOOLEAN DEFAULT FALSE,
received_date DATE,
expiry_date DATE,
document_url TEXT, -- encrypted S3 path
tin_verified BOOLEAN DEFAULT FALSE,
tin_verified_at TIMESTAMPTZ,
backup_withholding BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Track 1099 reportable payments per vendor per year
CREATE TABLE ap_vendor_1099_summary (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
vendor_id UUID NOT NULL,
tax_year INTEGER NOT NULL,
form_type VARCHAR(20), -- 1099-NEC, 1099-MISC
box_number INTEGER,
ytd_payments NUMERIC(20,6), -- year-to-date reportable payments
threshold_met BOOLEAN GENERATED ALWAYS AS (ytd_payments >= 600) STORED,
filed_at TIMESTAMPTZ,
UNIQUE(tenant_id, vendor_id, tax_year, form_type, box_number)
);
Integration: Use Avalara 1099 & W-9 API for TIN validation, e-file to IRS, and state filing.
1.6 AP Aging Analysis
Aging Computation Pattern
-- AP Aging Report (as-of-date, multi-currency)
CREATE OR REPLACE VIEW ap_aging_report AS
SELECT
v.tenant_id,
v.id AS vendor_id,
v.legal_name AS vendor_name,
v.currency_code,
inv.id AS invoice_id,
inv.invoice_number,
inv.invoice_date,
inv.due_date,
inv.outstanding_amount,
inv.total_functional AS outstanding_functional,
inv.functional_currency,
CURRENT_DATE - inv.due_date AS days_past_due,
CASE
WHEN inv.due_date >= CURRENT_DATE THEN 'CURRENT'
WHEN inv.due_date >= CURRENT_DATE - 30 THEN '1_30'
WHEN inv.due_date >= CURRENT_DATE - 60 THEN '31_60'
WHEN inv.due_date >= CURRENT_DATE - 90 THEN '61_90'
WHEN inv.due_date >= CURRENT_DATE - 120 THEN '91_120'
ELSE '120_PLUS'
END AS aging_bucket
FROM ap_invoices inv
JOIN ap_vendors v ON v.id = inv.vendor_id
WHERE inv.invoice_status NOT IN ('DRAFT', 'VOIDED', 'PAID')
AND inv.outstanding_amount > 0;
-- Aging summary by vendor and bucket
SELECT
vendor_id, vendor_name, currency_code, functional_currency,
SUM(CASE WHEN aging_bucket = 'CURRENT' THEN outstanding_functional END) AS current_amt,
SUM(CASE WHEN aging_bucket = '1_30' THEN outstanding_functional END) AS due_1_30,
SUM(CASE WHEN aging_bucket = '31_60' THEN outstanding_functional END) AS due_31_60,
SUM(CASE WHEN aging_bucket = '61_90' THEN outstanding_functional END) AS due_61_90,
SUM(CASE WHEN aging_bucket = '91_120' THEN outstanding_functional END) AS due_91_120,
SUM(CASE WHEN aging_bucket = '120_PLUS' THEN outstanding_functional END) AS due_120_plus,
SUM(outstanding_functional) AS total_outstanding
FROM ap_aging_report
GROUP BY vendor_id, vendor_name, currency_code, functional_currency;
Multi-currency aging note: Always report aging in both transaction currency AND functional currency. For consolidated aging, convert all transaction currencies to functional currency using the exchange rate at the aging report date (IAS 21 closing rate).
Part 2: Accounts Receivable (AR)
2.1 AR Subledger Schema
-- ============================================================
-- CUSTOMER MASTER
-- ============================================================
CREATE TABLE ar_customers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
customer_code VARCHAR(50) NOT NULL,
legal_name VARCHAR(255) NOT NULL,
tax_id VARCHAR(100),
tax_id_type VARCHAR(30),
customer_type VARCHAR(30), -- BUSINESS, INDIVIDUAL, GOVERNMENT
currency_code CHAR(3) NOT NULL, -- default billing currency
payment_terms_code VARCHAR(30),
credit_limit NUMERIC(20,6),
credit_limit_currency CHAR(3),
credit_status VARCHAR(20) DEFAULT 'GOOD', -- GOOD, WATCH, HOLD, BLOCKED
credit_score NUMERIC(5,2), -- internal scoring 0-100
credit_score_date DATE,
control_account_id UUID NOT NULL REFERENCES gl_accounts(id),
ar_control_account_id UUID REFERENCES gl_accounts(id),
country_code CHAR(2),
billing_address JSONB,
shipping_address JSONB,
einvoice_routing JSONB, -- e.g. {"peppol_id": "0088:1234567890", "format": "UBL"}
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, customer_code)
);
-- ============================================================
-- AR INVOICES
-- ============================================================
CREATE TABLE ar_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
customer_id UUID NOT NULL REFERENCES ar_customers(id),
invoice_number VARCHAR(100) NOT NULL,
invoice_type VARCHAR(30) NOT NULL, -- STANDARD, CREDIT_MEMO, DEBIT_MEMO, PROFORMA
invoice_status VARCHAR(30) DEFAULT 'DRAFT',
-- DRAFT, ISSUED, SENT, PARTIALLY_PAID, PAID, OVERDUE, DISPUTED, WRITTEN_OFF, VOIDED
invoice_date DATE NOT NULL,
due_date DATE NOT NULL,
service_period_from DATE,
service_period_to DATE,
currency_code CHAR(3) NOT NULL,
subtotal_amount NUMERIC(20,6) NOT NULL,
tax_amount NUMERIC(20,6) DEFAULT 0,
total_amount NUMERIC(20,6) NOT NULL,
paid_amount NUMERIC(20,6) DEFAULT 0,
outstanding_amount NUMERIC(20,6) GENERATED ALWAYS AS (total_amount - paid_amount) STORED,
functional_currency CHAR(3) NOT NULL,
exchange_rate NUMERIC(20,10) DEFAULT 1,
total_functional NUMERIC(20,6) NOT NULL,
-- GL
control_account_id UUID REFERENCES gl_accounts(id),
revenue_account_id UUID REFERENCES gl_accounts(id),
gl_journal_id UUID REFERENCES gl_journal_entries(id),
-- Payment terms
payment_terms_code VARCHAR(30),
early_pay_discount_pct NUMERIC(5,4),
early_pay_discount_days INTEGER,
-- E-Invoice
einvoice_id VARCHAR(255), -- government-issued reference
einvoice_status VARCHAR(30), -- PENDING, SUBMITTED, ACCEPTED, REJECTED
einvoice_format VARCHAR(30),
einvoice_sent_at TIMESTAMPTZ,
-- Collections
dunning_level INTEGER DEFAULT 0, -- 0=not dunned, 1=reminder, 2=first notice, etc.
last_dunned_at TIMESTAMPTZ,
next_dunning_date DATE,
collections_hold BOOLEAN DEFAULT FALSE,
dispute_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, invoice_number)
);
-- ============================================================
-- AR CASH RECEIPTS
-- ============================================================
CREATE TABLE ar_receipts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
customer_id UUID REFERENCES ar_customers(id),
receipt_date DATE NOT NULL,
receipt_type VARCHAR(30), -- CHECK, ACH, WIRE, CARD, PIX, DIRECT_DEBIT
receipt_status VARCHAR(30) DEFAULT 'UNMATCHED',
-- UNMATCHED, PARTIALLY_APPLIED, FULLY_APPLIED, OVERPAYMENT, RETURNED
currency_code CHAR(3) NOT NULL,
receipt_amount NUMERIC(20,6) NOT NULL,
applied_amount NUMERIC(20,6) DEFAULT 0,
unapplied_amount NUMERIC(20,6) GENERATED ALWAYS AS (receipt_amount - applied_amount) STORED,
functional_currency CHAR(3),
exchange_rate NUMERIC(20,10) DEFAULT 1,
functional_amount NUMERIC(20,6),
bank_reference VARCHAR(100),
remittance_info TEXT, -- free-form remittance advice
bank_account_id UUID,
gl_journal_id UUID REFERENCES gl_journal_entries(id),
-- Cash application
match_method VARCHAR(30), -- AUTOMATIC, MANUAL, AI_SUGGESTED
match_confidence NUMERIC(5,4), -- 0-1 confidence from ML engine
matched_by UUID,
matched_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================================
-- AR RECEIPT APPLICATIONS (receipt ↔ invoice M2M)
-- ============================================================
CREATE TABLE ar_receipt_applications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
receipt_id UUID NOT NULL REFERENCES ar_receipts(id),
invoice_id UUID NOT NULL REFERENCES ar_invoices(id),
applied_amount NUMERIC(20,6) NOT NULL,
discount_taken NUMERIC(20,6) DEFAULT 0,
exchange_gain_loss NUMERIC(20,6) DEFAULT 0, -- IAS 21 FX gain/loss on settlement
applied_by UUID,
applied_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(receipt_id, invoice_id)
);
IAS 21 FX Settlement Gain/Loss
When a foreign currency invoice is settled, the FX difference between invoice date rate and payment date rate must be posted to a P&L exchange gain/loss account:
DR AR Control Account (at payment rate)
DR/CR Exchange Gain/Loss Account
CR Bank Account (actual payment amount)
2.2 Invoicing Engine
Multi-Currency, Multi-Language Invoice Generation
interface InvoiceGenerationRequest {
tenantId: string;
customerId: string;
currencyCode: string; // billing currency
language: string; // BCP 47: 'en-US', 'pt-BR', 'de-DE'
taxMode: 'INCLUSIVE' | 'EXCLUSIVE';
taxBehavior: TaxBehaviorConfig;
lines: InvoiceLineInput[];
einvoiceConfig?: EInvoiceConfig; // trigger e-invoice generation
}
interface TaxBehaviorConfig {
countryCode: string;
taxRegistrationId: string; // VAT# or GST# of seller
customerTaxId?: string; // reverse charge scenarios
taxEngine: 'INTERNAL' | 'AVALARA' | 'SOVOS' | 'TAXJAR';
reverseCharge?: boolean; // EU cross-border B2B
}
Tax-inclusive vs. tax-exclusive: Store both subtotal_amount (ex-tax) and tax_amount separately regardless of presentation. This is critical for VAT return reporting where the tax authority needs both bases.
Multi-language invoice templates: Use a template engine (Handlebars/Jinja2) with locale-aware:
- Date formats (DD/MM/YYYY vs MM/DD/YYYY vs YYYY-MM-DD)
- Number formats (1.234,56 vs 1,234.56)
- Currency symbol placement
- RTL support for Arabic (Saudi ZATCA)
2.3 Collections Management: Dunning Sequences
Dunning Configuration Schema
CREATE TABLE ar_dunning_policies (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
policy_name VARCHAR(100) NOT NULL,
customer_segment VARCHAR(50), -- ALL, HIGH_VALUE, SMALL_BUSINESS, etc.
is_default BOOLEAN DEFAULT FALSE
);
CREATE TABLE ar_dunning_steps (
id UUID PRIMARY KEY,
policy_id UUID NOT NULL REFERENCES ar_dunning_policies(id),
step_number INTEGER NOT NULL,
days_past_due INTEGER NOT NULL, -- trigger: N days after due date
channel VARCHAR(30) NOT NULL, -- EMAIL, SMS, LETTER, PHONE, HOLD_ORDERS
template_id UUID,
escalate_to VARCHAR(50), -- NULL, ACCOUNT_MANAGER, COLLECTIONS_TEAM
auto_execute BOOLEAN DEFAULT TRUE,
UNIQUE(policy_id, step_number)
);
-- Example dunning sequence:
-- Step 1: Day 0 → Friendly reminder email (auto)
-- Step 2: Day 7 → Second reminder email (auto)
-- Step 3: Day 14 → Phone call escalation (manual)
-- Step 4: Day 30 → Formal demand letter (auto)
-- Step 5: Day 45 → Account on hold, order block (auto)
-- Step 6: Day 60 → External collections / legal notice (manual)
AR Aging (mirrors AP pattern)
SELECT
customer_id,
SUM(CASE WHEN days_past_due <= 0 THEN outstanding_functional END) AS current_amt,
SUM(CASE WHEN days_past_due BETWEEN 1 AND 30 THEN outstanding_functional END) AS due_1_30,
SUM(CASE WHEN days_past_due BETWEEN 31 AND 60 THEN outstanding_functional END) AS due_31_60,
SUM(CASE WHEN days_past_due BETWEEN 61 AND 90 THEN outstanding_functional END) AS due_61_90,
SUM(CASE WHEN days_past_due BETWEEN 91 AND 120 THEN outstanding_functional END) AS due_91_120,
SUM(CASE WHEN days_past_due > 120 THEN outstanding_functional END) AS due_120_plus
FROM ar_aging_view
GROUP BY customer_id;
2.4 Cash Application: AI-Powered Payment Matching
Matching Approach Hierarchy
- Exact match (invoice number in remittance = AR invoice number) → 99.9% confidence
- Amount + customer match (payment amount exactly equals open invoice) → ~95% confidence
- Fuzzy match (partial invoice number, slight amount difference) → ~85-90% confidence
- ML prediction (pattern learning from historical applications) → escalating accuracy over time
Modern systems achieve 90-95% straight-through processing (STP) — automated without human intervention.
interface CashApplicationMatch {
receiptId: string;
candidateInvoices: InvoiceMatch[];
recommendedApplications: ApplicationRecommendation[];
overallConfidence: number; // 0-1
requiresReview: boolean; // true if confidence < threshold
}
interface ApplicationRecommendation {
invoiceId: string;
appliedAmount: Money;
discountTaken: Money;
matchBasis: 'EXACT_INVOICE_NUMBER' | 'AMOUNT_MATCH' | 'FUZZY_MATCH' | 'ML_PREDICTED';
confidence: number;
}
Key scenarios to handle:
- Partial payments: Customer pays $900 on $1,000 invoice → mark invoice partially paid, leave $100 outstanding
- Overpayments: Customer pays $1,100 on $1,000 invoice → apply $1,000, create $100 credit memo
- Multi-invoice payments: One payment covering multiple invoices (most complex matching scenario)
- Cross-currency: Payment in USD applied to EUR invoice → record FX gain/loss per IAS 21
2.5 Credit Management
Customer Credit Scoring Model
CREATE TABLE ar_credit_assessments (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
customer_id UUID NOT NULL REFERENCES ar_customers(id),
assessment_date DATE NOT NULL,
credit_score NUMERIC(5,2), -- 0-100 internal score
recommended_limit NUMERIC(20,6),
recommended_currency CHAR(3),
scoring_factors JSONB, -- {
-- "payment_history_score": 85,
-- "avg_days_to_pay": 22,
-- "dispute_rate": 0.02,
-- "ytd_revenue": 250000,
-- "external_score": 720
-- }
external_score_provider VARCHAR(50), -- Dun & Bradstreet, Experian, etc.
external_score INTEGER,
approved_by UUID,
valid_until DATE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Automated limit rules:
- Auto-approve credit increases ≤ 20% for customers with score > 80, DSO < 30 days
- Flag for manual review: score drop > 10 points, DSO increasing trend, dispute rate > 5%
- Auto-block: score < 40, any invoice 90+ days past due
Part 3: E-Invoicing Engine (Multi-Country)
3.1 Mandatory E-Invoicing by Country
Global E-Invoicing Mandate Map (as of February 2026)
| Country | System | Model | Format | Mandate Status | Notes |
|---|---|---|---|---|---|
| Brazil | NF-e / NFS-e / MDF-e | Clearance (pre-auth) | XML + digital sig | Mandatory all sizes | NBS mandatory for NFS-e Jan 2026 |
| Mexico | CFDI 4.0 | Clearance (PAC) | XML (SAT schema) + CSD signature | Mandatory (CFDI 4.0 since Apr 2023) | PAC validation required; SAT preloads VAT returns |
| Italy | FatturaPA / SDI | Clearance (central) | FatturaPA XML v1.9 (Apr 2025) | Mandatory B2B, B2C, B2G | 5MB max per file; SDI routes to recipient |
| India | GST e-Invoice (IRP) | Clearance (IRP) | JSON (GST schema) | Mandatory > ₹5 crore (Aug 2023) | IRN + QR code on invoice |
| Poland | KSeF | Clearance (central) | FA_VAT XML | Large taxpayers: Feb 1 2026; All VAT: Apr 1 2026; Micro: Jan 1 2027 | PLN 200M+ threshold |
| Saudi Arabia | ZATCA FATOORA | Phase 1: Generation; Phase 2: Integration | UBL 2.1 XML / PDF/A-3 | Phase 2 waves ongoing; Wave 23: Jan-Mar 2026 | SAR 750K+ turnover; 23rd wave |
| Egypt | ETA e-Invoice | Clearance | JSON (ETA schema) | Mandatory all VAT-registered | >1.5B documents processed by mid-2025 |
| Nigeria | FIRS MBS | Pre-clearance | UBL/XML or JSON (BIS 3.0, 55 mandatory fields) | Large taxpayers (NGN 5B+): Aug 2025; All VAT: Jan 2026 | Peppol network conventions |
| EU (general) | Peppol / EN 16931 | Post-audit (moving to clearance) | UBL 2.1 / CII D16B | B2G mandatory most EU; B2B via ViDA from 2028 | Belgium B2B Jan 2026 |
E-Invoicing Models
CLEARANCE MODEL (real-time / pre-validation):
Seller → [sign XML] → Government Portal (validate) → Buyer
Examples: Brazil NF-e, Mexico CFDI, Italy SDI, India IRP, Saudi ZATCA
POST-AUDIT MODEL (after-the-fact reporting):
Seller → Buyer [directly] AND Seller → Tax Authority [periodic report]
Examples: EU traditional VAT reporting, UK MTD VAT
PEPPOL 4-CORNER MODEL:
Seller → Seller's Access Point → Peppol Network → Buyer's Access Point → Buyer
Examples: EU Peppol B2B/B2G, Nigeria (Peppol-based), Singapore
3.2 E-Invoicing Technical Architecture
Recommended Service Architecture
┌────────────────────────────────────────────────────┐
│ E-INVOICE ORCHESTRATION SERVICE │
│ │
│ [Invoice Data] → [Country Router] │
│ │ │
│ ├── Brazil → [NF-e Adapter] │
│ ├── Mexico → [CFDI 4.0 Adapter] │
│ ├── Italy → [FatturaPA Adapter] │
│ ├── India → [GST IRP Adapter] │
│ ├── Poland → [KSeF Adapter] │
│ ├── Saudi → [ZATCA Adapter] │
│ ├── Egypt → [ETA Adapter] │
│ ├── Nigeria → [FIRS MBS Adapter] │
│ └── EU/Global → [Peppol UBL Adapter] │
│ │
└────────────────────────────────────────────────────┘
Country Adapter Interface
interface EInvoiceAdapter {
countryCode: string;
formatName: string;
// Build the country-specific XML/JSON payload
buildPayload(invoice: ARInvoice, tenantConfig: TenantEInvoiceConfig): EInvoicePayload;
// Sign and submit to government portal
submit(payload: EInvoicePayload, credentials: GovernmentCredentials): Promise<SubmissionResult>;
// Check submission status (for async systems)
checkStatus(submissionId: string): Promise<EInvoiceStatus>;
// Validate prior to submission
validate(payload: EInvoicePayload): ValidationResult;
// Cancel/void a submitted invoice (where supported)
cancel(einvoiceId: string, reason: string): Promise<CancellationResult>;
}
interface SubmissionResult {
einvoiceId: string; // government-issued reference
accessKey?: string; // Brazil NF-e chave de acesso (44 digits)
uuid?: string; // Mexico CFDI UUID
irn?: string; // India IRN
ksefReferenceNumber?: string; // Poland KSeF
status: 'ACCEPTED' | 'REJECTED' | 'PENDING';
rejectionReasons?: string[];
signedDocumentUrl?: string; // PDF/XML with government signature
submittedAt: Date;
}
Database State Tracking
CREATE TABLE einvoice_submissions (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
invoice_id UUID NOT NULL, -- ar_invoices or ap_invoices
invoice_direction VARCHAR(3), -- OUT (issued) or IN (received)
country_code CHAR(2) NOT NULL,
format VARCHAR(30) NOT NULL,
submission_status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
-- PENDING, SUBMITTED, ACCEPTED, REJECTED, CANCELLED, RETRY_PENDING
-- Government-issued references
government_ref VARCHAR(255), -- NF-e chave, CFDI UUID, IRN, KSeF#, etc.
access_key VARCHAR(255), -- Brazil 44-digit chave de acesso
digital_signature TEXT, -- XML-DSig or PKCS#7
qr_code_data TEXT, -- for display on PDF invoice
-- Payload storage
payload_format VARCHAR(10), -- XML, JSON
payload_hash CHAR(64), -- SHA-256 of submitted payload
payload_url TEXT, -- S3/GCS path (immutable)
signed_doc_url TEXT, -- government-returned signed document
-- Submission attempts
attempt_count INTEGER DEFAULT 0,
last_attempt_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
error_message TEXT,
error_code VARCHAR(50),
submitted_at TIMESTAMPTZ,
accepted_at TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
3.3 Brazil NF-e / NFS-e Technical Details
NF-e (Nota Fiscal Eletrônica) — for goods/products:
- 44-digit access key (chave de acesso)
- XML signed with digital certificate (e-CNPJ A1/A3)
- Submitted to SEFAZ (state tax authority) via SOAP web service
- Response:
cStat 100= authorized;cStat 150= authorized out-of-time-window - DANFE (PDF representation) required for physical goods transit
NFS-e (Nota Fiscal de Serviços) — for services:
- Municipal-level regulation (each of 5,570 municipalities has different system)
- National NFS-e standard being unified via SPED/ABRASF
- NBS (Nomenclatura Brasileira de Serviços) mandatory from January 1, 2026
CT-e (Conhecimento de Transporte) — for freight/transport
MDF-e (Manifesto Eletrônico de Documentos Fiscais) — for transport manifest
3.4 Mexico CFDI 4.0 Technical Details
- Format: XML with XSD schema from SAT (Servicio de Administración Tributaria)
- Signing: Seller signs with CSD (Certificado de Sello Digital)
- PAC validation: Send to SAT-authorized PAC (Proveedor Autorizado de Certificación)
- PAC stamps: PAC adds
tfd:TimbreFiscalDigitalnode with UUID, PAC certificate, timestamp - Key fields:
NoCertificado,Certificado,Sello,UUID,FechaTimbrado,NoCertificadoSAT - CFDI 4.0 additions vs 3.3: Mandatory receiver RFC, export indicator, periodic payment supplements
- SAT integration: Since 2024, SAT preloads monthly VAT returns with CFDI data
3.5 Peppol Network: BIS 3.0
Architecture Overview
Peppol uses a 4-corner model:
Corner 1 (Sender) ──→ Corner 2 (Sender's Access Point)
│
Peppol
Network
│
Corner 4 (Receiver) ←── Corner 3 (Receiver's Access Point)
Format: UBL 2.1 XML (mandatory), with optional UN/CEFACT CII D16B Standard: EN 16931 (European e-invoicing standard) Key BIS 3.0 documents: Invoice (T10), Credit Note (T14), Order (T01)
Access Point providers: Basware, Pagero, TrueCommerce, Storecove, Svefaktura, Datalite
Peppol IDs (participant addressing):
0088:GLN — Global Location Number
0007:SE556677 — Swedish org number
9930:DE810 — German tax ID
0192:NO123 — Norwegian org number
Adoption by region:
- Scandinavia/Nordics: Mature, high adoption
- EU: Growing; B2G mandatory in most countries
- Singapore, Australia, New Zealand: Active Peppol networks
- US: OpenPeppol + PINT (Peppol International Invoice)
- Nigeria: Adopted Peppol conventions for FIRS MBS
- Global: OpenPeppol has 42+ member countries
UBL vs CII Comparison
| Dimension | UBL 2.1 | UN/CEFACT CII D16B |
|---|---|---|
| Governance | OASIS | UN/CEFACT |
| Peppol primary format | Yes (mandatory) | Optional supplement |
| Country usage | EU, Australia, NZ, Singapore | Germany (ZUGFeRD/Factur-X), France (Factur-X) |
| Structure | XML (verbose, element-centric) | XML (more compact) |
| Hybrid PDF | PDF/A-3 + embedded UBL | PDF/A-3 + embedded CII (Factur-X) |
| Complexity | High | High |
3.6 E-Invoicing SaaS Provider Comparison
| Provider | Coverage | Architecture | Best For | Integration |
|---|---|---|---|---|
| Avalara | 190+ countries (tax); e-invoice for key mandates | API-first; REST APIs well-documented; ERP connectors | High-volume US tax + selective e-invoice mandates | Pre-built NetSuite, SAP, D365 |
| Sovos | 60+ countries; deep clearance model coverage | Compliance-focused; handles Brazil, Italy, LatAm, EU | Complex multi-country clearance mandates | API + pre-built ERP |
| Pagero (SAP) | Global; Peppol certified AP | Peppol-native; acquired by SAP 2024 | Peppol B2B/B2G + global supply chain | SAP-native; API for others |
| Comarch | EU, Poland, LatAm | EDI+eInvoice platform | KSeF (Poland expertise), EU compliance | API + FTP/AS2 |
| Storecove | 50+ countries; Peppol AP | API-only (developer-friendly) | Startups/SaaS needing API-first Peppol | REST API |
Architectural recommendation for CODITECT: Build an abstraction layer (EInvoiceProviderAdapter) and use:
- Sovos or Avalara for Brazil/Mexico/LatAm and EU clearance mandates
- Storecove or Pagero for Peppol BIS 3.0 network delivery
- Direct government API for Brazil NF-e SEFAZ (already partially implemented in SPED prototype)
Part 4: Payment File Formats
4.1 NACHA/ACH (US)
File structure:
[1] File Header Record (Record type code 1)
[5] Company Batch Header (Record type code 5) — one per payment batch
[6] Entry Detail Records (Record type code 6) — one per payment
[7] Addenda Records (Record type code 7) — optional CTX/IAT addenda
[8] Batch Control Record (Record type code 8)
[9] File Control Record (Record type code 9)
Key Entry Detail fields:
- Record Type Code (1 char) =
6 - Transaction Code (2 chars):
22=checking debit,27=checking credit,32=savings credit - Routing Transit Number (9 chars)
- DFI Account Number (17 chars)
- Amount (10 chars, implied 2 decimal places)
- Individual ID Number (15 chars) — vendor invoice or reference
- Individual Name (22 chars)
- Trace Number (15 chars) — unique per entry
SEC codes for AP:
CCD— Corporate Credit or Debit (single AP payment)CTX— Corporate Trade Exchange (with addenda for remittance data, up to 9,999 addenda records)IAT— International ACH Transaction (cross-border)
ISO 20022 mapping: Nacha published pain.001 → NACHA translation guide. CHIPS migrated to ISO 20022 in April 2024; Fedwire migrates July 2025.
4.2 SEPA (EU) — ISO 20022 Native
SEPA is already ISO 20022 based. Key message types:
pain.001— Customer Credit Transfer Initiation (SEPA SCT — outgoing AP payments)pain.002— Payment Status Report (bank confirmation)pain.007— Payment Reversalpain.008— Direct Debit Initiation (SEPA SDD — AR collections)camt.052— Bank Account Report (intraday)camt.053— Bank Statement (end-of-day, for cash application)camt.054— Bank Debit/Credit Notification (for AR cash matching)
SEPA SCT timing: D+1 (next business day). SEPA Instant (SCT Inst): 10-second settlement, 24/7.
Key SEPA validation rules:
- IBAN check digit validation (ISO 7064 MOD 97-10)
- BIC (SWIFT code) validation
- Character set: Latin extended (no special chars outside SWIFT char set)
- Amount: max EUR 999,999,999.99 (SCT); max EUR 100,000 (SCT Inst, being raised)
4.3 BACS / Faster Payments (UK)
BACS Direct Credit (STD 18 format):
- Fixed-width 100-character record format (legacy)
- 3-day processing cycle (Day 1: submission, Day 3: settlement)
- Used for payroll and supplier payments
Faster Payments Service (FPS):
- Instant settlement (within 2 hours, typically seconds)
- Max £1M per transaction
- Used via Pay.UK API or bank APIs
UK migration: Transitioning to NPA (New Payments Architecture) using ISO 20022 messaging
4.4 Brazil Payment Rails
| Rail | Settlement | Use Case | Notes |
|---|---|---|---|
| PIX | Real-time (24/7) | Consumer + B2B + B2G | REST API via PSP; 63B transactions in 2024; NFC contactless from Feb 2025; Automatico (recurring) from Jun 2025 |
| TED | Same-day (banking hours) | High-value B2B | Being displaced by PIX; deprecated for most use cases |
| DOC | T+1 | Low-value | Discontinued 2024 |
| Boleto Bancário | T+1 to T+2 | Consumer/SMB billing | Widely used for AR; can be used in batches |
| SPEI | Real-time | Mexico equivalent (different) | Mexico's SPEI, not Brazil |
PIX API integration pattern:
POST /v2/cob — Create PIX charge (QR Code)
POST /v2/cobv — Create PIX charge with due date (Cobv)
GET /v2/cob/{txid} — Check charge status
POST /v2/pix — Outgoing PIX payment (AP)
GET /v2/pix/{e2eId} — Check PIX payment status
POST /v2/webhook/{chave} — Register webhook for status updates
4.5 India Payment Rails
| Rail | Settlement | Use Case | Max Amount |
|---|---|---|---|
| UPI | Real-time (24/7) | Consumer + SMB; 14B+ transactions/month May 2024 | ₹1L per transaction (₹5L for verified merchants) |
| IMPS | Real-time (24/7) | Banking transfers | ₹5L |
| NEFT | Near real-time (30-min cycles) | B2B transfers | No limit |
| RTGS | Real-time (within 30 min) | High-value B2B (>₹2L) | No limit; 69% of payment value |
UPI accounts for 85% of India payment volumes and 49% of global real-time transactions (IMF, 2025).
UPI 3.0 features (2024): Conversational Voice Payments, enhanced credit on UPI, delegated payments.
4.6 ISO 20022 Migration Status (Critical for Platform Design)
| Payment System | Migrated? | Date | Notes |
|---|---|---|---|
| SEPA | Yes (native) | 2008 | ISO 20022 since inception |
| SWIFT cross-border | Yes (exclusive) | November 2025 | MT formats deprecated Nov 2025 |
| CHIPS (US) | Yes | April 2024 | Full ISO 20022 |
| Fedwire (US) | Planned | July 2025 | Currently migrating |
| CHAPS (UK) | Yes | May 2023 | |
| TARGET2 (EU) | Yes | March 2023 | |
| Canada LVTS/Lynx | Yes | 2021 |
Platform implication: All payment file generation should support ISO 20022 pain.001 as the canonical internal format, with translators to legacy formats (NACHA) where still required. The pain.001 → NACHA translator is available from Nacha's mapping guide.
4.7 Cross-Border Payments
SWIFT gpi
- End-to-End tracking: Every SWIFT gpi payment has a Unique End-to-end Transaction Reference (UETR, RFC 4122 UUID)
- Speed: ~50% of SWIFT gpi payments credited within 30 minutes; nearly all within 24 hours
- Volume: $300B+ sent via SWIFT gpi daily
- ISO 20022 native: SWIFT mandatory ISO 20022 for cross-border from November 2025
- Wise partnership: SWIFT + Wise collaboration announced to route SWIFT payments over Wise rails where beneficial
Fintech Cross-Border APIs
| Provider | Key Feature | Settlement Time | Integration |
|---|---|---|---|
| Wise (TransferWise) API | Local bank accounts in 40+ countries; ~0.5% FX fee | 1-2 business days | REST API; Batch payments API |
| Airwallex | Multi-currency wallets; local collection accounts | Real-time to 1 day | REST API; ERP integrations |
| Nium | Payout API to 100+ countries; 35+ currencies | Minutes to 1 day | REST API; embeddable payments |
| SWIFT gpi | Tracking + faster correspondent banking | 30 min to 24 hours | MT/ISO 20022 via bank |
Recommendation for CODITECT: For AP cross-border payments, offer:
- SWIFT gpi for high-value, bank-to-bank (via the tenant's primary bank)
- Wise Business API or Airwallex for lower-cost FX conversions
- PIX for Brazil, UPI/NEFT/RTGS for India (directly via local PSP)
Part 5: Architectural Recommendations
5.1 Overall Module Architecture
┌─────────────────────────────────────────────────────────────────┐
│ CODITECT FINANCIAL SUITE │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │ AP MODULE │ │ AR MODULE │ │ E-INVOICE ENGINE │ │
│ │ │ │ │ │ │ │
│ │ Vendor Mgmt │ │ Customer │ │ Country Router │ │
│ │ Invoice Cap │ │ Invoicing │ │ Format Adapters │ │
│ │ 3-Way Match │ │ Cash Apply │ │ Signing Service │ │
│ │ Payment Run │ │ Collections │ │ Gov Portal Connectors │ │
│ │ 1099 Mgmt │ │ Credit Mgmt │ │ Peppol AP │ │
│ └──────┬──────┘ └──────┬──────┘ └────────────┬────────────┘ │
│ │ │ │ │
│ └────────────────┴───────────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ GL ENGINE │ │
│ │ (existing proto) │ │
│ │ Multi-currency │ │
│ │ Multi-tenant RLS │ │
│ │ SPED compliant │ │
│ └───────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ PAYMENT ORCHESTRATION LAYER │ │
│ │ NACHA │ SEPA pain.001 │ PIX API │ SWIFT gpi │ │
│ │ BACS │ UPI/NEFT/RTGS │ Wise API │ Airwallex │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ TAX & COMPLIANCE LAYER │ │
│ │ Avalara/Sovos (tax calc) │ Pagero/Storecove (Peppol) │ │
│ │ Country-specific rules │ OFAC/Sanctions screening │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
5.2 Multi-Tenant Isolation Strategy (aligns with existing GL prototype)
The existing GL engine uses PostgreSQL Row-Level Security (RLS). AP/AR tables should follow the same pattern:
-- Enable RLS on all AP/AR tables
ALTER TABLE ap_invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE ap_vendors ENABLE ROW LEVEL SECURITY;
ALTER TABLE ar_invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE ar_customers ENABLE ROW LEVEL SECURITY;
-- Policy: current_setting('app.tenant_id') enforced at session level
CREATE POLICY tenant_isolation ON ap_invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);
-- Application sets tenant context at connection/session start:
-- SET app.tenant_id = 'tenant-uuid-here';
For accounting firms managing multiple client entities: a single tenant_id corresponds to one client entity. The accounting firm itself has a firm_id which maps to a set of tenant_id values it manages.
5.3 Control Account Reconciliation Service
Run as a scheduled job (every N minutes in near-real-time, or on every posting):
interface ControlAccountReconciliation {
tenantId: string;
accountId: string;
period: AccountingPeriod;
glBalance: Money; // from gl_account_balances
subledgerBalance: Money; // from ap_invoices or ar_invoices
difference: Money; // must be zero
reconciliationStatus: 'BALANCED' | 'OUT_OF_BALANCE';
outOfBalanceAmount?: Money;
rootCause?: string; // if out of balance
}
A non-zero difference must immediately trigger an alert. Common causes: posting timing gaps, manual journal entries to AP/AR control accounts (which should be blocked), currency rounding differences.
5.4 Priority Build Sequence
Given the existing GL prototype, recommended build order:
- AP Subledger core (vendor master, invoice, payment tables + GL integration)
- Invoice capture pipeline (OCR adapter → validation → matching)
- 3-way matching engine (tolerance rules + exception workflow)
- Payment run orchestration (NACHA, SEPA pain.001, PIX for Brazil)
- AR subledger core (customer master, AR invoice, receipt tables)
- Cash application (matching algorithms + ML confidence scoring)
- Dunning engine (policy-driven automated collections)
- E-invoice engine (Brazil NF-e first, given SPED existing work; then CFDI, UBL/Peppol)
- Multi-country e-invoice adapters (Italy, India, Poland, Saudi, Egypt, Nigeria)
- Vendor portal + 1099/W-9 (US compliance layer)
Gaps Identified (Areas Requiring Further Research)
- Digital certificate management for e-invoicing: Brazil (e-CNPJ A1/A3), Mexico (CSD/FIEL) — certificate lifecycle, storage (HSM vs software), rotation
- SPED fiscal integration depth: Full SPED Fiscal (EFD-ICMS/IPI) and SPED Contribuições (EFD-PIS/COFINS) schema requirements for Brazil going beyond NF-e
- Bank API connectivity: Specific bank API specifications (Open Banking UK, Brazilian Open Finance, US FDX) for real-time bank statement import to support cash application
- Payment fraud detection: Real-time anomaly detection patterns for payment run fraud (account hijacking, BEC fraud) — referenced 79% fraud rate in 2024
- Multi-entity consolidation: Inter-company AP/AR elimination rules for accounting firms managing parent/subsidiary relationships
- ZATCA Phase 2 integration details: Specific API endpoints and cryptographic stamping (CSID) process for Saudi Arabia full integration mode
- KSeF implementation details: Polish KSeF API specifications, token-based authorization, FA_VAT schema specifics
Sources Validated
Official Documentation
- Nacha ISO 20022 Resource Center — ACH file format and ISO 20022 mapping
- Peppol BIS Billing 3.0 (May 2025) — Official Peppol UBL specification
- ZATCA e-Invoicing — Saudi Arabia FATOORA
- IRS 1099/W-9 Forms
Research & Analysis Sources
- Parseur AI Invoice Processing Benchmarks 2026 — OCR/AI accuracy comparison
- Veryfi Invoice OCR Benchmark 2025 — Sub-3-second processing benchmarks
- Klippa Best OCR Tools 2025 — AP OCR comparison
- Tipalti 3-Way Match Guide — 3-way matching methodology
- Stampli 3-Way Invoice Matching — Automation patterns
- J.P. Morgan ISO 20022 Migration — Migration status and timeline
- ACI Worldwide ISO 20022 Global Expansion
- SDK.finance Payment Processing Architecture 2026
- Transfi Brazil Payment Rails
- Global Banking & Finance PIX at Five Years
- EDICOM Mexico CFDI
- EDICOM Italy SDI
- Comarch E-Invoicing Brazil 2025
- Comarch E-Invoicing Mexico 2025
- VATupdate Poland KSeF Mandate
- OpenText 2025 Guide to Global E-Invoicing Mandates
- EY Nigeria FIRS e-Invoicing
- Deloitte Nigeria FIRS Mandate
- Innovate Tax Africa e-Invoicing 2025
- Avalara vs Sovos Comparison — E-invoicing SaaS platform analysis
- HighRadius Cash Application Automation
- Versapay Cash Application — ML matching approaches
- SWIFT gpi Overview
- Thunes SWIFT GPI Cross-Border
- European Payments Council UPI Article
- Tipalti W-9 for Vendors
- Crunchy Data Multi-Tenant PostgreSQL
- AWS Multi-Tenant PostgreSQL SaaS