Skip to main content

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
-- ============================================================
-- 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)

ProviderAccuracySpeedStrengthsPricing Model
ABBYY FlexiCapture99.5% structured, 97%+ semi-structured~2-3sHighest documented accuracy; 1.5B invoices/yearPer-page or enterprise license
Veryfi98.7% field-level<3s (sub-3s benchmark leader)Speed + accuracy combined; REST API firstPer-document API
Rossum~97-98%3-5sSelf-learning; minimal template setupSaaS per-document
Nanonets~96-98%3-6sDeep learning; excellent for unstructuredPer-page API
Mindee~95-97%2-4sDeveloper-friendly; pre-built + custom modelsFreemium + per-page
Traditional OCR only85-95%1-2sFast on clean structured docsVaries

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.

[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

  1. Abstraction layer: Build a DocumentParserAdapter interface 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.

  2. Training corpus: For each provider, maintain a tenant-specific training corpus. Rossum and Nanonets support self-supervised learning from corrections.

  3. Fallback chain: OCR provider → LLM extraction fallback (GPT-4o / Claude with vision) → human queue.

  4. 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:

StatusConditionAction
FULLY_MATCHEDAll lines match within toleranceAuto-approve for payment
WITHIN_TOLERANCEMinor variance within configured thresholdAuto-approve with note
PRICE_VARIANCEUnit price differs > toleranceRoute to procurement approval
QUANTITY_VARIANCEQuantity received ≠ invoiced (outside tolerance)Route to warehouse
PO_NOT_FOUNDNo PO referenceRoute to AP manager
RECEIPT_PENDINGPO found, no receipt yetHold invoice (receipt-required flag)
DUPLICATEDuplicate invoice detectedBlock 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):

  1. AP staff creates payment batch
  2. AP Manager approves batch
  3. CFO/Controller approves if > $50K threshold
  4. System generates bank file only after all approvals obtained
  5. 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

  1. Exact match (invoice number in remittance = AR invoice number) → 99.9% confidence
  2. Amount + customer match (payment amount exactly equals open invoice) → ~95% confidence
  3. Fuzzy match (partial invoice number, slight amount difference) → ~85-90% confidence
  4. 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)

CountrySystemModelFormatMandate StatusNotes
BrazilNF-e / NFS-e / MDF-eClearance (pre-auth)XML + digital sigMandatory all sizesNBS mandatory for NFS-e Jan 2026
MexicoCFDI 4.0Clearance (PAC)XML (SAT schema) + CSD signatureMandatory (CFDI 4.0 since Apr 2023)PAC validation required; SAT preloads VAT returns
ItalyFatturaPA / SDIClearance (central)FatturaPA XML v1.9 (Apr 2025)Mandatory B2B, B2C, B2G5MB max per file; SDI routes to recipient
IndiaGST e-Invoice (IRP)Clearance (IRP)JSON (GST schema)Mandatory > ₹5 crore (Aug 2023)IRN + QR code on invoice
PolandKSeFClearance (central)FA_VAT XMLLarge taxpayers: Feb 1 2026; All VAT: Apr 1 2026; Micro: Jan 1 2027PLN 200M+ threshold
Saudi ArabiaZATCA FATOORAPhase 1: Generation; Phase 2: IntegrationUBL 2.1 XML / PDF/A-3Phase 2 waves ongoing; Wave 23: Jan-Mar 2026SAR 750K+ turnover; 23rd wave
EgyptETA e-InvoiceClearanceJSON (ETA schema)Mandatory all VAT-registered>1.5B documents processed by mid-2025
NigeriaFIRS MBSPre-clearanceUBL/XML or JSON (BIS 3.0, 55 mandatory fields)Large taxpayers (NGN 5B+): Aug 2025; All VAT: Jan 2026Peppol network conventions
EU (general)Peppol / EN 16931Post-audit (moving to clearance)UBL 2.1 / CII D16BB2G mandatory most EU; B2B via ViDA from 2028Belgium 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

┌────────────────────────────────────────────────────┐
│ 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:TimbreFiscalDigital node 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

DimensionUBL 2.1UN/CEFACT CII D16B
GovernanceOASISUN/CEFACT
Peppol primary formatYes (mandatory)Optional supplement
Country usageEU, Australia, NZ, SingaporeGermany (ZUGFeRD/Factur-X), France (Factur-X)
StructureXML (verbose, element-centric)XML (more compact)
Hybrid PDFPDF/A-3 + embedded UBLPDF/A-3 + embedded CII (Factur-X)
ComplexityHighHigh

3.6 E-Invoicing SaaS Provider Comparison

ProviderCoverageArchitectureBest ForIntegration
Avalara190+ countries (tax); e-invoice for key mandatesAPI-first; REST APIs well-documented; ERP connectorsHigh-volume US tax + selective e-invoice mandatesPre-built NetSuite, SAP, D365
Sovos60+ countries; deep clearance model coverageCompliance-focused; handles Brazil, Italy, LatAm, EUComplex multi-country clearance mandatesAPI + pre-built ERP
Pagero (SAP)Global; Peppol certified APPeppol-native; acquired by SAP 2024Peppol B2B/B2G + global supply chainSAP-native; API for others
ComarchEU, Poland, LatAmEDI+eInvoice platformKSeF (Poland expertise), EU complianceAPI + FTP/AS2
Storecove50+ countries; Peppol APAPI-only (developer-friendly)Startups/SaaS needing API-first PeppolREST 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 Reversal
  • pain.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

RailSettlementUse CaseNotes
PIXReal-time (24/7)Consumer + B2B + B2GREST API via PSP; 63B transactions in 2024; NFC contactless from Feb 2025; Automatico (recurring) from Jun 2025
TEDSame-day (banking hours)High-value B2BBeing displaced by PIX; deprecated for most use cases
DOCT+1Low-valueDiscontinued 2024
Boleto BancárioT+1 to T+2Consumer/SMB billingWidely used for AR; can be used in batches
SPEIReal-timeMexico 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

RailSettlementUse CaseMax Amount
UPIReal-time (24/7)Consumer + SMB; 14B+ transactions/month May 2024₹1L per transaction (₹5L for verified merchants)
IMPSReal-time (24/7)Banking transfers₹5L
NEFTNear real-time (30-min cycles)B2B transfersNo limit
RTGSReal-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 SystemMigrated?DateNotes
SEPAYes (native)2008ISO 20022 since inception
SWIFT cross-borderYes (exclusive)November 2025MT formats deprecated Nov 2025
CHIPS (US)YesApril 2024Full ISO 20022
Fedwire (US)PlannedJuly 2025Currently migrating
CHAPS (UK)YesMay 2023
TARGET2 (EU)YesMarch 2023
Canada LVTS/LynxYes2021

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

ProviderKey FeatureSettlement TimeIntegration
Wise (TransferWise) APILocal bank accounts in 40+ countries; ~0.5% FX fee1-2 business daysREST API; Batch payments API
AirwallexMulti-currency wallets; local collection accountsReal-time to 1 dayREST API; ERP integrations
NiumPayout API to 100+ countries; 35+ currenciesMinutes to 1 dayREST API; embeddable payments
SWIFT gpiTracking + faster correspondent banking30 min to 24 hoursMT/ISO 20022 via bank

Recommendation for CODITECT: For AP cross-border payments, offer:

  1. SWIFT gpi for high-value, bank-to-bank (via the tenant's primary bank)
  2. Wise Business API or Airwallex for lower-cost FX conversions
  3. 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:

  1. AP Subledger core (vendor master, invoice, payment tables + GL integration)
  2. Invoice capture pipeline (OCR adapter → validation → matching)
  3. 3-way matching engine (tolerance rules + exception workflow)
  4. Payment run orchestration (NACHA, SEPA pain.001, PIX for Brazil)
  5. AR subledger core (customer master, AR invoice, receipt tables)
  6. Cash application (matching algorithms + ML confidence scoring)
  7. Dunning engine (policy-driven automated collections)
  8. E-invoice engine (Brazil NF-e first, given SPED existing work; then CFDI, UBL/Peppol)
  9. Multi-country e-invoice adapters (Italy, India, Poland, Saudi, Egypt, Nigeria)
  10. Vendor portal + 1099/W-9 (US compliance layer)

Gaps Identified (Areas Requiring Further Research)

  1. Digital certificate management for e-invoicing: Brazil (e-CNPJ A1/A3), Mexico (CSD/FIEL) — certificate lifecycle, storage (HSM vs software), rotation
  2. 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
  3. 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
  4. Payment fraud detection: Real-time anomaly detection patterns for payment run fraud (account hijacking, BEC fraud) — referenced 79% fraud rate in 2024
  5. Multi-entity consolidation: Inter-company AP/AR elimination rules for accounting firms managing parent/subsidiary relationships
  6. ZATCA Phase 2 integration details: Specific API endpoints and cryptographic stamping (CSID) process for Saudi Arabia full integration mode
  7. KSeF implementation details: Polish KSeF API specifications, token-based authorization, FA_VAT schema specifics

Sources Validated

Official Documentation

Research & Analysis Sources