CANONICAL SOURCE OF TRUTH. Read this before editing ANY diagram or code.
Last updated: 2026-03-03
| Table |
Purpose |
Row Count |
company |
Chart legal entities — one row per operating company. Carries ERP system + connection, AP tool + connection, PDF handling, country-specific e-invoicing config, and buyer identifier columns (tax_id, buyer_biz_reg, buyer_duns, buyer_gln) for buyer fallback matching |
~20 |
supplier |
One row per supplier per Chart company. The resolution target for AP routing. Replaces the previous supplier_lookup table |
~10K |
supplier_identifier |
Flat key-value store of all supplier identifiers (VAT, DUNS, GLN, REG, etc.). The lookup hot path |
~30K |
StagingInvoice |
First landing table for all inbound invoices (AR + AP) |
Transient |
InvoiceRegister |
Promoted after validation; tracks full lifecycle via status column. Two-record model for clearance countries (Record 1: clearance, Record 2: delivery). Linked by parent_invoice_id. |
Growing |
AuditLog |
Append-only audit trail with payload hashes |
Growing |
- DLQ is a STATUS (
status = 'DLQ') in InvoiceRegister, NOT a separate table.
- There is no "Invoice" table. The correct name is
InvoiceRegister.
- There is no separate "erp_instance" table. ERP connection details are on the
company table (one row per company, with acceptable denormalisation for shared ERP instances like D365).
- There is no "Country" table. Country rules (clearance model, tax ID format, default output format) are static business rules in code/config, derived from the
country field on company.
- There is no
supplier_lookup table. Replaced by supplier + supplier_identifier (two tables for multi-identifier matching). Lookup results are logged to AuditLog (SUPPLIER_LOOKUP_RESULT event).
- Two-Push Model for Clearance Countries: Malaysia (and future clearance countries) involves TWO separate ERP pushes. Phase 1: ERP pushes a clearance request → StagingInvoice #1 → InvoiceRegister #1 (clearance) → EDICOM → Government. Phase 2: after writeback and PDF regen, ERP pushes the cleared invoice → StagingInvoice #2 → InvoiceRegister #2 (delivery) → EDICOM → Customer. Each push goes through the full staging → validate → promote → send pipeline. InvoiceRegister #2 links to #1 via
parent_invoice_id.
- Delivery code path is unified: Phase 2 for clearance countries is identical to non-clearance. Same stages, same pipeline, no country-specific branching. Each phase has the same number of stages as a non-clearance invoice.
Cross-cutting concern — updated at EVERY step of the lifecycle, not just callbacks.
¶ AR States (Standard — DE / DK / FR / BE)
STAGED_RECEIVED → STAGED_VALIDATED → VALIDATED → READY_TO_SEND → SENDING → SENT → DELIVERED
↘ STAGED_VALIDATION_FAILED (terminal)
SENDING ↔ RETRY (exponential backoff)
RETRY → DLQ (max retries exceeded)
PHASE 1 — Clearance (ERP push #1, JSON only, no PDF):
StagingInvoice #1: STAGED_RECEIVED → STAGED_VALIDATED → PROMOTED
InvoiceRegister #1: RECEIVED → VALIDATED → READY_TO_SEND → SENDING → SENT → AWAITING_CLEARANCE → CLEARED
On CLEARED: middleware writes back ID+QR to ERP.
ERP regenerates PDF with ID+QR. This happens outside the middleware.
PHASE 2 — Delivery (ERP push #2, JSON + PDF with ID+QR):
StagingInvoice #2: STAGED_RECEIVED → STAGED_VALIDATED → PROMOTED
InvoiceRegister #2: RECEIVED → VALIDATED → READY_TO_SEND → SENDING → SENT → DELIVERED
(parent_invoice_id links to InvoiceRegister #1)
Phase 2 is IDENTICAL to a non-clearance invoice — same stages, same pipeline.
Error states (either phase): CLEARANCE_REJECTED, RETRY, DLQ
AP_STAGED_RECEIVED → AP_STAGED_VALIDATED → AP_RECEIVED → AP_ROUTING → AP_SENDING → AP_SENT → AP_CONFIRMED
↘ AP_STAGED_VALIDATION_FAILED (terminal)
AP_SENDING ↔ AP_RETRY (exponential backoff)
AP_RETRY → AP_DLQ (max retries exceeded)
Self-billing (Malaysia):
AP_SELF_BILLING_REQUIRED → AP_SELF_BILLING_NOTIFIED → AP_SELF_BILLING_CLEARING → AP_SELF_BILLING_CLEARED
1. ERP Systems → Push UCF JSON + PDF → AR Invoice Controller
State: STAGED_RECEIVED (in StagingInvoice)
2. AR Staging Validation Worker → schema, business rules, country rules
State: STAGED_VALIDATED or STAGED_VALIDATION_FAILED (terminal)
3. Promote to InvoiceRegister (record_type = DELIVERY, parent_invoice_id = NULL)
State: VALIDATED
4. Intercompany check + format routing:
External customer → Step 5 (EDICOM — always)
Intercompany + MANDATED_NETWORK=YES → Step 5 (EDICOM — legal requirement)
Intercompany + LTA=YES → Step 5 (EDICOM — certified archival)
Intercompany + neither → Step 5b (direct delivery)
5. Send to EDICOM via REST API (external + intercompany where mandated)
State: READY_TO_SEND → SENDING
6a. EDICOM accepts → State: SENT
6b. EDICOM transient error → State: RETRY → backoff → SENDING
Max retries → DLQ
7. EDICOM delivery callback → State: DELIVERED
-- OR --
5b. Direct delivery (intercompany, no mandated network, no LTA)
Middleware delivers directly to receiving Chart entity (email / SFTP / REST)
State: READY_TO_SEND → SENDING → SENT → DELIVERED
No EDICOM callback — middleware confirms delivery itself.
Currently applies to: Germany and Denmark intercompany (unless LTA — ACT-062 pending)
8. On REJECTED/DLQ:
→ Error/DLQ Handling → FreshService ticket (P1/P2/P3)
9. DLQ: manual triage via Blazor Dashboard
PHASE 1 — Clearance (ERP push #1):
1. ERP Systems → Push UCF JSON (no PDF) → AR Invoice Controller
StagingInvoice #1: STAGED_RECEIVED
2. AR Staging Validation Worker → schema, business rules, country rules
StagingInvoice #1: STAGED_VALIDATED or STAGED_VALIDATION_FAILED (terminal)
3. Promote to InvoiceRegister #1 (record_type = CLEARANCE, parent_invoice_id = NULL)
InvoiceRegister #1: RECEIVED → VALIDATED
4. Build JSON-only payload (no PDF for clearance)
InvoiceRegister #1: READY_TO_SEND
5. Send to EDICOM → MyInvois via REST API
InvoiceRegister #1: SENDING → SENT
6. Awaiting government response
InvoiceRegister #1: AWAITING_CLEARANCE
7. EDICOM clearance callback → Callback Handler
Approved: InvoiceRegister #1: CLEARED (terminal)
Rejected: InvoiceRegister #1: CLEARANCE_REJECTED → Error/DLQ
8. On CLEARED:
Callback Handler → ERP Writeback Service (passes Gov ID + QR) → ERP
ERP regenerates PDF with embedded ID + QR
(This happens OUTSIDE the middleware pipeline)
PHASE 2 — Delivery (ERP push #2):
9. ERP pushes updated UCF JSON + PDF (with ID+QR) → AR Invoice Controller
StagingInvoice #2: STAGED_RECEIVED
10. AR Staging Validation Worker validates
StagingInvoice #2: STAGED_VALIDATED
11. Promote to InvoiceRegister #2 (record_type = DELIVERY, parent_invoice_id = Record 1 ID)
InvoiceRegister #2: RECEIVED → VALIDATED
12–15. Standard delivery flow (IDENTICAL to non-clearance):
READY_TO_SEND → SENDING → SENT → DELIVERED
1. EDICOM → AP Invoice Controller (POST /api/ap/invoice)
State: AP_STAGED_RECEIVED (in StagingInvoice)
2. AP Staging Validation Worker → schema, business rules
State: AP_STAGED_VALIDATED or AP_STAGED_VALIDATION_FAILED (terminal)
3. Promote to InvoiceRegister table
State: AP_RECEIVED
4. AP Routing Worker → supplier lookup (3-step multi-identifier) → route determination by company
State: AP_ROUTING → AP_SENDING
5. Deliver to target:
- SFTP → COR360 (JDE sites)
- SFTP → OpenText (SAP sites)
- EMAIL → Basware (AX sites)
- REST API → ERP (Epicor / JDE direct)
State: AP_SENT → AP_CONFIRMED
Step 0: Resolve Buyer Company
buyer_tax_id → company.tax_id → primary match
fallback: buyer_biz_reg, buyer_duns, buyer_gln → secondary match
No match → CompanyNotFoundError (P2 Freshservice ticket)
Step 1: Multi-Identifier Lookup (The Hook)
Extract ALL supplier identifiers from invoice (VAT, DUNS, GLN, REG, etc.)
Normalise each one (shared function — same as sync)
Single IN-query against supplier_identifier → JOIN supplier
Filter by resolved company_code + status = ACTIVE
Step 2: Result Filtering (The Filter)
0 rows → EXCEPTION (P3 Freshservice ticket)
1 row → MATCHED → return erp_vendor_id + company_code
N rows, same supplier_id → MATCHED (strong confirmation)
N rows, different supplier_ids, 1 pref → MATCHED (PREFERRED_TIEBREAK)
N rows, different supplier_ids, no pref → AMBIGUOUS (exception queue)
Every attempt → logged to AuditLog (SUPPLIER_LOOKUP_RESULT event)
Exception queue = filtered view on AuditLog (EXCEPTION/AMBIGUOUS with no matching SUPPLIER_LOOKUP_RESOLVED)
¶ API Controllers (Request Handlers)
| Component |
Endpoint |
Purpose |
Input |
Output |
| AR Invoice Controller |
POST /api/ar/invoice |
Receive AR invoices from ERPs |
UCF JSON + PDF (base64 or file reference + SHA-256 hash) from ERP |
StagingInvoice record created; HTTP 202 Accepted with stagingId |
| AR Invoice Controller |
POST /api/ar/invoice/clearance-complete |
Receive cleared invoice for Phase 2 delivery (MY) |
UCF JSON + PDF (with Gov ID+QR embedded) from ERP |
StagingInvoice #2 record created; HTTP 202 Accepted |
| AP Invoice Controller |
POST /api/ap/invoice |
Receive AP invoices from EDICOM |
UCF JSON from EDICOM (inbound via APIM) |
StagingInvoice record created; HTTP 202 Accepted with stagingId |
| EDICOM Callback Controller |
POST /api/edicom/callback |
Receive delivery status callbacks from EDICOM |
{transactionId, status, deliveredAt} or {rejectionCode, reason} |
InvoiceRegister status updated (DELIVERED / DLQ) |
| EDICOM Callback Controller |
POST /api/edicom/callback/clearance |
Receive clearance callbacks from EDICOM (MY) |
{governmentId, qrCode (base64), validatedAt} or {rejectionCode, reason} |
InvoiceRegister #1 → CLEARED; triggers ERP writeback |
| Health Controller |
GET /health |
Health check for monitoring |
Monitoring probe |
Health status JSON (DB, EDICOM, disk) |
| Component |
Schedule |
Purpose |
Input |
Output |
| AR Staging Validation Worker |
Every few seconds |
Validate AR staging records and promote to InvoiceRegister |
StagingInvoice records (status = STAGED_RECEIVED) |
Pass: InvoiceRegister record created (RECEIVED), staging → PROMOTED. Fail: staging → STAGED_VALIDATION_FAILED, ErrorLog + Freshservice ticket |
| AP Staging Validation Worker |
Every few seconds |
Validate AP staging records and promote to InvoiceRegister |
StagingInvoice records (status = STAGED_RECEIVED) |
Pass: InvoiceRegister record created (AP_RECEIVED), staging → PROMOTED. Fail: staging → AP_STAGED_VALIDATION_FAILED, ErrorLog + Freshservice ticket |
| AR Submission Worker |
Every few seconds |
Build payload and send validated AR invoices to EDICOM |
InvoiceRegister records (status = READY_TO_SEND) |
EDICOM REST API call; status → SENDING → SENT. Error: → RETRY (backoff) or DLQ |
| AP Routing Worker |
Every few seconds |
Run supplier lookup (3-step multi-identifier), route AP invoices to correct AP tool |
InvoiceRegister records (status = AP_RECEIVED) |
Supplier lookup → route determination → delivery to COR360 (SFTP) / OpenText (SFTP) / Basware (EMAIL) / ERP (REST). Status → AP_SENT → AP_CONFIRMED |
| Supplier Refresh Job |
Nightly |
Refresh supplier + supplier_identifier from ERP vendor masters. JDE: F0101/F0101G/F0401 + Parent AN8 fallback. Non-JDE: flat import via API/file/view |
ERP vendor master data (per-ERP extraction) |
Upsert supplier + supplier_identifier tables. Re-evaluate unresolved exceptions |
| Staging Archival Job |
Nightly |
Archive old staging records to free storage |
StagingInvoice records > 90 days old |
Large payload data cleared, records retained for audit |
| DailyStats Aggregation Job |
Nightly |
Pre-aggregate stats for Power BI reporting |
InvoiceRegister data (counts, timings, success rates) |
DailyStats table rows (by date, country, direction) |
| Component |
Purpose |
Input |
Output |
| Validation Service |
Schema validation, business rules, country rules, duplicate check |
StagingInvoice record (raw UCF JSON) |
Validation result: pass (with normalised data) or fail (with error list) |
| EDICOM Client Service |
Send invoices to EDICOM REST API with retry + circuit breaker |
UCF JSON payload (+ embedded PDF for delivery) |
EDICOM transaction ID + ACK. Polly retry (30s→2m→10m→1h→4h, max 5). Circuit breaker: 5 errors in 30s → OPEN 60s |
| Supplier Lookup Service |
3-step multi-identifier lookup: resolve buyer company → identifier pool IN-query → filter results |
All supplier identifiers + buyer_tax_id from AP invoice |
{company_code, ap_tool, erp_vendor_id} or error (P2/P3 Freshservice ticket). Logged to AuditLog (SUPPLIER_LOOKUP_RESULT event) |
| AP Delivery Service |
Deliver AP invoices to target systems |
Formatted invoice + target route from routing worker |
SFTP file drop (COR360/OpenText), email (Basware), or REST API call (ERP direct). Fire-and-forget for SFTP |
| PDF Service |
Pick up PDFs from ERP file shares, verify hash, store to blob storage |
PDF file reference + SHA-256 hash from ERP push |
Verified PDF stored in blob storage; pdf_blob_path + pdf_blob_hash written to StagingInvoice. Hash mismatch → STAGED_VALIDATION_FAILED |
| Clearance Writeback Service |
Write Gov ID + QR code back to ERP after clearance (MY) |
Gov ID, QR code (base64), invoice reference from callback |
REST call to JDE Orchestrator (POST /api/jde/orchestrator/clearance-writeback). ERP confirms → triggers PDF regen |
| Freshservice Service |
Create IT support tickets for failures via REST API |
Error event (type, severity, invoice details) |
Freshservice ticket: P1 (gov rejection), P2 (DLQ), P3 (validation/lookup failure) |
| Audit Service |
Append-only audit trail for every state change and API call |
Any state transition, API call, or data change |
AuditLog record (immutable — no UPDATE/DELETE). Includes payload hashes for legal defensibility |
| Component |
Purpose |
Input |
Output |
| Operations Dashboard |
Real-time IT operations view |
User queries (search, filter, drill-down) |
KPIs (today's volume, pending, failed, success rate), status charts by country/ERP, failure table with retry button, invoice detail view, audit log viewer, DLQ management |
| Target |
Route |
Sites |
Transport |
| COR360 |
Middleware → SFTP file drop → COR360 → JDE |
MY, GOFA |
SFTP/22 (fire-and-forget) |
| OpenText |
Middleware → SFTP file drop → OpenText → SAP |
HDE, HTO, HTO DK |
SFTP/22 (fire-and-forget) |
| Basware |
Middleware → Email (PDF + UCF) → Basware → AX |
HSV, HBC |
EMAIL (to Basware mailbox) |
| ERP Direct |
Middleware → REST API → ERP |
FLOW, VCT, HMP FR, CPI FR |
HTTPS/443 (with confirmation) |
| Component |
Purpose |
Notes |
| State Machine |
Track invoice lifecycle via status column in InvoiceRegister |
Updated at EVERY step — not a discrete processing box. Cross-cutting concern |
| Error / DLQ Handling |
Process invoices in error states |
DLQ is a STATUS (status = 'DLQ') in InvoiceRegister, NOT a separate table. Manual triage via Dashboard |
| Idempotency |
Prevent duplicate processing across all API hops |
Key: {company_code}_{invoiceNo}_{invoiceType}_{version}. Create-or-ignore on every hop |
| Circuit Breaker |
Prevent flooding EDICOM during outages |
5 errors in 30s → OPEN for 60s. Invoices queue locally. Half-open: 1 test request |
| Step |
Status |
Stored In |
| ERP pushes UCF + PDF |
STAGED_RECEIVED |
StagingInvoice |
| Validation pass |
STAGED_VALIDATED |
StagingInvoice |
| Validation fail |
STAGED_VALIDATION_FAILED (terminal) |
StagingInvoice |
| Promote to InvoiceRegister |
PROMOTED / RECEIVED |
StagingInvoice → PROMOTED; InvoiceRegister created → RECEIVED |
| PDF picked up, hash verified |
READY_TO_SEND |
InvoiceRegister |
| EDICOM API call |
SENDING |
InvoiceRegister |
| EDICOM ACK |
SENT |
InvoiceRegister |
| Delivery callback |
DELIVERED (terminal) |
InvoiceRegister |
| Transient error |
RETRY → SENDING |
InvoiceRegister |
| Max retries exceeded |
DLQ |
InvoiceRegister |
| Step |
Status |
Stored In |
| ERP pushes clearance request (no PDF) |
STAGED_RECEIVED |
StagingInvoice |
| Validation pass |
STAGED_VALIDATED |
StagingInvoice |
| Promote to InvoiceRegister |
PROMOTED / RECEIVED |
StagingInvoice → PROMOTED; InvoiceRegister #1 created → RECEIVED |
| Hash verified (no PDF for clearance) |
READY_TO_SEND |
InvoiceRegister |
| EDICOM API call |
SENDING |
InvoiceRegister |
| EDICOM ACK, submits to MyInvois |
SENT |
InvoiceRegister |
| Awaiting LHDN response |
AWAITING_CLEARANCE |
InvoiceRegister |
| Gov approves (ID+QR via callback) |
CLEARED (terminal) |
InvoiceRegister |
| Gov rejects |
CLEARANCE_REJECTED (terminal) |
InvoiceRegister |
Middleware writes back ID+QR to ERP. ERP regenerates PDF. Outside middleware pipeline.
| Step |
Status |
Stored In |
| ERP pushes cleared invoice (JSON + PDF) |
STAGED_RECEIVED |
StagingInvoice |
| Validation pass |
STAGED_VALIDATED |
StagingInvoice |
| Promote (parent_invoice_id = Record 1) |
PROMOTED / RECEIVED |
StagingInvoice → PROMOTED; InvoiceRegister #2 created → RECEIVED |
| PDF picked up, hash verified |
READY_TO_SEND |
InvoiceRegister |
| EDICOM API call |
SENDING |
InvoiceRegister |
| EDICOM ACK |
SENT |
InvoiceRegister |
| Delivery callback |
DELIVERED (terminal) |
InvoiceRegister |
Phase 2 is IDENTICAL to non-Malaysia — same pipeline, same stages.
Full per-ERP integration details (AR/AP flows, PDF handling, writeback, file shares): ERP Integration
| Company |
ERP |
Country |
AP Tool |
AP Delivery |
PDF Tool |
| Chart MY |
JDE |
Malaysia |
COR360 |
SFTP |
BI Publisher |
| GOFA |
JDE |
Germany |
COR360 |
SFTP |
BI Publisher |
| FLOW |
JDE |
Germany |
ERP Direct |
REST API |
BI Publisher |
| VCT |
JDE |
Germany |
ERP Direct |
REST API |
BI Publisher |
| HMP FR |
IFS (no mod — migrating to JDE) |
France |
ABBYY (existing) |
existing process |
N/A — no IFS integration |
| HMP BE |
IFS (no mod — migrating to JDE) |
Belgium |
COR360 (existing) |
existing process |
N/A — no IFS integration |
| HDE |
SAP S/4H |
Germany |
OpenText |
SFTP |
Adobe Forms |
| HTO |
SAP S/4H |
Germany |
OpenText |
SFTP |
Adobe Forms |
| HTO DK |
SAP S/4H |
Denmark |
OpenText |
SFTP |
Adobe Forms |
| HAX DK |
AX / D365 |
Denmark |
TBD |
TBD |
SSRS |
| HSV |
AX |
France |
Basware |
EMAIL |
SSRS |
| HBC |
AX |
France |
Basware |
EMAIL |
SSRS |
| CPI FR |
Epicor |
France |
Ancora/DocStar |
REST API |
SSRS |
Per-country rules, formats, transport methods, and requirements: Country Rules
| Model |
Countries |
Government Role |
Government System |
AR Format |
Process |
| Private Exchange |
DE, DK, BE |
None |
— |
XRechnung (CII/UBL) / Peppol BIS |
ERP → EDICOM → Customer. No government involvement. Structured invoice sent directly via Peppol network. |
| Exchange + Reporting |
FR, ES |
Receives copy |
Chorus Pro (FR) |
Factur-X (PDF/A-3 + CII XML) |
ERP → EDICOM → Customer + Government copy. Invoice delivered to customer while a copy is simultaneously reported to the government in real-time. |
| Clearance Central |
PL, IT |
Clears via platform |
SDI (IT), KSeF (PL) |
Country-specific |
ERP → EDICOM → Government platform → Customer. Invoice submitted to the government's central platform which clears it and makes it available to the customer. |
| Clearance Single |
MY, MX, CO, CL |
Pre-approval required |
MyInvois / LHDN (MY) |
MyInvois JSON/XML |
ERP → EDICOM → Government (approve) → EDICOM → Customer. Government validates and approves, then EDICOM delivers the cleared invoice to the customer. |
| Clearance Dual |
GR, HU |
Dual transaction |
Country-specific |
Country-specific |
ERP → EDICOM → Government (approve + report). Two steps: government clears the invoice, then the dataset is separately reported to the tax administration. |
| System |
Fill |
Stroke |
Use |
| ERP Systems |
#f5f5f5 |
#666666 |
On-prem ERPs, file shares |
| Middleware |
#dae8fc |
#6c8ebf |
All middleware components |
| Middleware container |
#E8F0FE |
#6c8ebf |
Outer container box |
| EDICOM |
#fff2cc |
#d6b656 |
EDICOM platform + client service |
| Government |
#f8cecc |
#b85450 |
Government portals (MyInvois, PPF, Peppol) |
| Customer |
#d5e8d4 |
#82b366 |
Customer (AR delivery target) |
| Supplier |
#e1d5e7 |
#9673a6 |
Supplier (AP invoice source) |
| AP Tools |
#d5e8d4 |
#82b366 |
COR360, OpenText (same as Customer green) |
| Infrastructure |
#E6E6E6 |
#999999 |
FreshService, APIM, Power BI, App Insights |
| Notes |
#ffffcc |
#cccc00 |
Annotation callouts |
| Rules reference |
#E6F3FF |
#4A90D9 |
Applicable rules boxes |
| State Type |
Fill |
Stroke |
| Processing |
#dae8fc |
#6c8ebf (blue) |
| Success |
#d5e8d4 |
#82b366 (green) |
| Error |
#f8cecc |
#b85450 (red) |
| Retry/Warning |
#fff2cc |
#d6b656 (yellow) |
AR (Outbound):
ERPs (JDE/SAP/AX/Epicor) ──Push UCF+PDF──→ Middleware (Azure) ──REST──→ EDICOM ──→ Government / Customer
↑ |
└── Callbacks (status, clearance ID+QR) ──┘
AP (Inbound):
Supplier ──→ EDICOM ──REST──→ Middleware (Azure) ──→ COR360 (SFTP) / OpenText (SFTP) / Basware (EMAIL) / ERP Direct (REST)
Supplier Data:
ERP vendor masters ──nightly Supplier Refresh Job──→ Middleware (supplier + supplier_identifier tables)
Key points:
- ERPs push directly to middleware via REST API.
- Supplier/company lookup refresh source is TBD (see DEC-009). Snowflake is NOT used.
- Single EDICOM interface from middleware (1 connection, not 11).
- EDICOM handles format transformation to country-specific schemas.
Full EDICOM integration details (UCF format, API spec, callbacks, security): EDICOM Integration
| # |
Interface |
Protocol |
Auth |
Direction |
Data Format |
| 1 |
AR submission |
REST API (HTTPS) |
Basic Auth (base64) |
Middleware → EDICOM |
UCF JSON + embedded PDF (base64) |
| 2 |
Delivery callback |
Webhook (HTTPS) |
HMAC-SHA256 + IP whitelist |
EDICOM → Middleware (via APIM) |
JSON {transactionId, status, deliveredAt} |
| 3 |
Clearance callback (MY) |
Webhook (HTTPS) |
HMAC-SHA256 + IP whitelist |
EDICOM → Middleware (via APIM) |
JSON {governmentId, qrCode, validatedAt} |
| 4 |
AP inbound |
REST API (HTTPS) |
HMAC-SHA256 |
EDICOM → Middleware (via APIM) |
UCF JSON + embedded PDF (base64) |
| 5 |
Clearance writeback |
REST API (HTTPS) |
Bearer token |
Middleware → ERP |
JSON {governmentId, qrCode} |
| 6 |
AP to COR360/OpenText |
SFTP file drop |
SSH key (Key Vault) |
Middleware → AP tool |
JSON + PDF files |
| 6b |
AP to Basware |
Email (SMTP) |
Authenticated SMTP |
Middleware → Basware mailbox |
PDF + UCF as attachments |
| 7 |
AP to ERP direct |
REST API (HTTPS) |
Bearer token |
Middleware → ERP |
UCF JSON + PDF |
| 8 |
Supplier refresh |
TBD (see DEC-009) |
TBD |
External source → Middleware |
Data set (nightly) |
Network: IPsec tunnel (AES-256, DH20, PSK) for EDICOM ↔ Middleware. Inbound callbacks route through APIM.
AP file naming: {taxid}_{supplierid}_{timestamp}.xml
- IPsec tunnel: AES-256, DH20, PSK — site-to-site to EDICOM
- APIM: Azure API Management — subscription keys, rate limiting, TLS termination
- ERP connections: HTTPS/443 (TLS 1.2+), Bearer tokens via APIM, Sectigo certs
- AP tool connections: SFTP/22, SSH key auth (Key Vault)
- Callback auth: HMAC-SHA256 + IP whitelist
- Supplier data source: TBD (see DEC-009)
- .NET 9 — Minimal API
- SQL Server — single database
- Azure VM — East US2
- Blazor — Dashboard
- Polly — retry + circuit breaker
- Supplier data source — TBD (see DEC-009)
Full decisions log with rationale and options: Decisions Log
| # |
Decision |
Choice |
Rationale |
| DEC-001 |
AR integration pattern |
Middleware Hub |
Single EDICOM interface (1 vs 11), supplier portability, centralised monitoring |
| DEC-002 |
PDF generation |
ERP generates |
Required for clearance (ID+QR), native branding, ERP has templates |
| DEC-003 |
AP for JDE sites |
Direct to COR360 |
COR360 already deployed, simple SFTP path, no middleware overhead |
| DEC-004 |
AP for non-JDE sites |
Via Middleware |
Centralised routing, consistent error handling |
| DEC-005 |
Clearance model |
Two-push (CLEARANCE + DELIVERY records) |
Unified delivery pipeline, clean state machine, no duplicate statuses |
| DEC-006 |
Middleware stack |
.NET 9 / SQL Server / Azure VM |
Aligns with Chart standards, sufficient for volume |
| DEC-007 |
AR data source |
ERP push (direct) |
ERPs push UCF directly to middleware via REST API |
| DEC-009 |
Supplier data source |
TBD (Snowflake PROPOSED — OQ-001) |
Snowflake proposed for supplier/company lookup refresh. Awaiting CIO approval. Alternative source TBD if not approved |
| DEC-008 |
Intercompany invoices |
Conditional — depends on mandated_network and lta_required flags |
Intercompany invoices route via EDICOM only when the destination country mandates a network (BE: Peppol, FR: PA) or requires certified LTA. Germany and Denmark intercompany is direct (unless LTA). Malaysia: EDICOM for clearance only, delivery direct. See Rule #12 |
- Middleware Hub — single orchestration layer, all invoices route through it
- Push-Only — no polling from middleware to ERP or EDICOM
- State Machine is cross-cutting — updated at every step, not a discrete processing box
- DLQ is a status — not a table, not a queue; it's
status = 'DLQ' in InvoiceRegister
- Callback Handler receives ALL callbacks — success and failure, not just errors
- ERP Writeback Service is middleware — sits inside the middleware, pushes Gov ID + QR to ERP
- FreshService is infrastructure — grey (#E6E6E6), NOT government red
- Append-only audit — no UPDATE/DELETE on AuditLog
- Idempotency — on every API hop
- Circuit breaker — 5 exceptions in 30s → OPEN for 60s
- Two-push model for clearance — clearance countries (MY) involve two separate ERP pushes, each going through the full staging → validate → promote → send pipeline. Phase 1: clearance request (JSON only) → AWAITING_CLEARANCE → CLEARED. Phase 2: cleared invoice (JSON + PDF with ID+QR) → standard delivery. Phase 2 is identical to non-clearance. Both phases produce the same number of stages.
- Intercompany routing is conditional — invoices between Chart entities are routed based on the destination country's
mandated_network and lta_required flags on the company table:
MANDATED_NETWORK = YES → via EDICOM (legal requirement to use the mandated network — BE: Peppol, FR: PA platform)
LTA = YES → via EDICOM (certified long-term archival required — TBD per country, ACT-062 pending)
- Clearance required (MY) → EDICOM for clearance Phase 1 only, then middleware delivers directly for Phase 2
- None of the above → middleware delivers directly to the receiving Chart entity (no EDICOM)
AR invoice → middleware validates → is it intercompany?
├── NO (external customer) → EDICOM (always)
└── YES (intercompany) →
├── MANDATED_NETWORK = YES? → EDICOM (legal requirement)
├── LTA = YES? → EDICOM (certified archival)
├── Clearance required? → EDICOM for clearance only, then direct delivery
└── None of the above → direct delivery (middleware delivers to customer)
| Scenario |
EDICOM? |
Government? |
Delivery Method |
Records Created |
| External → DE |
Yes |
No |
EDICOM → Peppol → Customer |
1 DELIVERY |
| External → DK |
Yes |
No |
EDICOM → Peppol/NemHandel → Customer |
1 DELIVERY |
| External → BE |
Yes |
No |
EDICOM → Peppol → Customer |
1 DELIVERY |
| External → FR |
Yes |
Copy to PPF |
EDICOM → PA → Customer + Gov copy |
1 DELIVERY |
| External → MY |
Yes |
Yes (clearance) |
EDICOM → MyInvois → EDICOM → Customer |
1 CLEARANCE + 1 DELIVERY |
| Intercompany → DE |
No (unless LTA) |
No |
Middleware → direct |
1 DELIVERY |
| Intercompany → DK |
No (unless LTA) |
No |
Middleware → direct |
1 DELIVERY |
| Intercompany → BE |
Yes (mandated network) |
No |
EDICOM → Peppol → Receiving entity |
1 DELIVERY |
| Intercompany → FR |
Yes (mandated network) |
Copy to PPF |
EDICOM → PA → Receiving entity + Gov copy |
1 DELIVERY |
| Intercompany → MY |
Clearance only |
Yes (self-bill) |
EDICOM → MyInvois (clearance), then middleware → direct |
1 CLEARANCE + 1 DELIVERY |
| Country |
mandated_network |
lta_required |
Effect on Intercompany |
| DE |
NO |
TBD (ACT-062) |
Direct delivery (unless LTA turns out to be mandatory) |
| DK |
NO |
TBD (ACT-062) |
Direct delivery (unless LTA turns out to be mandatory) |
| BE |
YES (Peppol) |
TBD |
EDICOM — Peppol network is legally mandated |
| FR |
YES (PA) |
TBD |
EDICOM — PA platform is legally mandated |
| MY |
NO (clearance only) |
TBD |
EDICOM for clearance, direct for delivery |
Note: LTA (Long-Term Archive) status per country is pending — ACT-062 (Kevin Bangert). If LTA is mandatory for a country, intercompany invoices for that country must go through EDICOM even if the network is not mandated.
| # |
Question |
Context |
Status |
| OQ-001 |
Confirm data source for supplier population |
The Supplier Refresh Job now reads directly from each ERP's vendor master (JDE: F0101/F0101G/F0401, SAP: CDS view, AX: OData, Epicor: REST/BAQ, IFS: API). Snowflake is NOT in the invoice data path (DEC-009). Updated: the new multi-identifier design populates from ERP vendor masters directly — Snowflake may still be useful as a consolidated view but is no longer the primary source. Dana to confirm. |
OPEN — awaiting Dana |
| OQ-002 |
What identifiers does EDICOM put in the UCF JSON? |
Root dependency for AP supplier lookup. Per-country schemes: DE 9930 (VAT), DK 0184 (CVR), BE 0208 (KBO/CBE), FR 0009 (SIRET), MY TIN (LHDN). Does EDICOM pass raw identifiers or Peppol-prefixed form? Updated: the new multi-identifier lookup with Peppol schemeID mapping handles both formats — but we still need to confirm what EDICOM actually sends. |
OPEN — blocked until EDICOM API spec received (ACT-072) |
| OQ-003 |
Tax ID normalisation rules |
RESOLVED by design. Normalisation is a single shared function in app code, used by both Supplier Refresh Job (on write) AND runtime lookup (on read). Values are normalised on write (stored pre-normalised in supplier_identifier.id_value). Inbound invoice values are normalised the same way before the lookup query. See spec 14-supplier-lookup.md §7. |
RESOLVED — normalise on write, same function on read |
| OQ-004 |
Do any Chart companies share a tax ID? |
The company table has a unique constraint on (tax_id, country). Updated: the new buyer fallback matching (buyer_biz_reg, buyer_duns, buyer_gln) provides alternatives if tax_id is shared. Still need to confirm all Phase 1 companies have distinct tax IDs. |
OPEN — needs confirmation (ACT-069) |
| OQ-005 |
Extraction method per ERP for Supplier Refresh Job |
Updated: JDE algorithm fully specified (F0010→F0101→F0101G→F0401 + Parent AN8 fallback). Non-JDE: SAP CDS view/BAPI, AX OData, Epicor REST/BAQ, IFS API. Exact endpoints TBD per ERP team. See spec 14-supplier-lookup.md §4-5. |
PARTIALLY RESOLVED — JDE specified, non-JDE endpoints TBD (ACT-070) |
| OQ-006 |
Process for new/unknown suppliers (first invoice) |
When a supplier is unknown, the lookup logs outcome = 'EXCEPTION' in AuditLog (SUPPLIER_LOOKUP_RESULT event). Updated: dashboard Supplier Exceptions section (filtered view on AuditLog) allows manual resolution — add supplier + identifiers with source = 'MANUAL', which are never overwritten by sync. Manual resolution logged as SUPPLIER_LOOKUP_RESOLVED event. Still a question: is the manual-add workflow sufficient or do we need an automated quick-add? |
OPEN — business process decision (ACT-071) |
| OQ-007 |
Peppol Participant ID mapping |
RESOLVED by design. The supplier_identifier table stores normalised values by type (VAT, GLN, REG, etc.). Peppol schemeID mapping (0088→GLN, 9930→VAT, 0208→REG, etc.) is implemented in app code at invoice receipt time. Raw values are extracted and normalised before lookup. See spec 14-supplier-lookup.md §Identifier Types Reference. |
RESOLVED — Peppol schemeID→id_type mapping in app code |
Update this file whenever architecture facts are confirmed or corrected.