Anti-Corruption Layer (ACL)

v0.1.0

Decoupled cross-domain ingestion from Billing, WMS, POS, and CRM into balanced General Ledger postings without operational coupling.

Anti-Corruption Layer (ACL)

In enterprise Domain-Driven Design (DDD), operational systems—such as Sales, Warehouse Management (WMS), Invoicing, and Point of Sale (POS)—must never write directly to accounting database tables. Doing so couples business logistics to statutory ledger models.

The Anti-Corruption Layer (ACL) (ium()->accounting()->acl()) acts as a protective boundary that translates upstream domain events into balanced, zero-sum double-entry vouchers.


1. Architectural Boundary Isolation

flowchart TD
    subgraph OperationalDomains["Operational Bounded Contexts"]
        POS["POS / Invoicing<br/>InvoiceSettledEvent"]
        WMS["Warehouse (WMS)<br/>StockWritedownEvent"]
        PUR["Purchasing<br/>GoodsReceivedEvent"]
    end

    subgraph ACLBoundary["Anti-Corruption Layer (ACL)"]
        INGEST["ium()->accounting()->acl()->ingest($dto)"]
        MAP["Event-Account Mapping Registry<br/>Resolves DR / CR Chart Codes"]
        VAL["Validation & Minor-Unit Math<br/>Asserts Σ Debits === Σ Credits"]
    end

    subgraph AccountingCore["General Ledger Core"]
        JV["Posted Journal Voucher"]
        GL["Immutable GL Postings"]
        AUD["Forensic Audit Log"]
    end

    OperationalDomains -->|Domain Event Payload| INGEST
    INGEST --> MAP
    MAP --> VAL
    VAL --> AccountingCore

2. Registering Event Account Mappings

Before ingesting domain facts, register which Chart of Accounts codes handle each event type:

// Map customer checkout event to Cash Debit and Sales Revenue Credit
ium()->accounting()->acl()->registerMapping(
    eventName: 'billing.invoice_settled',
    debitAccountCode: '1111',  // Bank Operating Checking
    creditAccountCode: '4100'  // Product Sales Revenue
);

// Map inventory write-down event to Cost of Goods Sold and Inventory Asset
ium()->accounting()->acl()->registerMapping(
    eventName: 'inventory.stock_writedown',
    debitAccountCode: '5120',  // Inventory Shrinkage & Loss
    creditAccountCode: '1310'  // Finished Goods Inventory
);

3. Ingesting Cross-Domain Facts

When an upstream event occurs, dispatch an AclIngestEventDto:

use Obelaw\Ium\Accounting\Data\AclIngestEventDto;

// Called from an event listener or background queue worker
$voucher = ium()->accounting()->acl()->ingest(
    new AclIngestEventDto(
        eventName: 'billing.invoice_settled',
        amountMinor: 850000, // $8,500.00 stored in integer cents
        currencyCode: 'USD',
        transactionDate: '2026-03-31',
        description: 'Customer INV-9821 settled via Stripe Checkout',
        sourceType: 'billing.invoice',
        sourceId: '9821'
    )
);

echo "Automated Voucher Created: {$voucher->voucher_number}\n";
echo "Voucher Status: {$voucher->status}\n"; // POSTED immediately

4. Key Guarantees Enforced by the ACL

  1. Zero Database Coupling: The billing module has zero foreign keys to ium_accounting_* tables. It only passes its own sourceType and sourceId.
  2. Idempotency Protection: Ingesting the same sourceType and sourceId combination a second time will detect the existing voucher and safely return it without double-posting.
  3. Audit Cross-Reference: The resulting JournalEntry and audit logs record the originating event name, source model, and source ID, allowing statutory auditors to trace from financial reports back to the operational document in one click.