Skip to content

Data model reference

Canonical entities

The ERP and CRM exports are both projections of the same internal canonical entities: CanonicalCustomer, CanonicalInvoice, and CanonicalPayment. See src/datagen/identities.py for the full field list.

ERP and CRM field mapping

Customers

Canonical field ERP CSV column CRM JSON field
customer_id (internal only) -- --
business key CUST_ID, for example C000001 customerId, for example CUST-000001
full_name CUST_NAME customerName
email EMAIL_ADDR email
phone PHONE_NUM phone
street ADDR_LINE1 address.street
city CITY address.city
region REGION address.region
postal_code POSTAL_CD address.postalCode
country COUNTRY address.country
created_at CREATED_DT createdAt

Invoices

Canonical field ERP CSV column CRM JSON field
business key INV_NO, for example INV-000001 invoiceNumber, for example INV000001, nested under the owning customer
issue_date ISSUE_DT issueDate
due_date DUE_DT dueDate
currency CURR_CD currency
amount AMT amount
status STATUS_CD (upper-cased) status
exists_in_erp / exists_in_crm row omitted if False entry omitted if False

Payments

Canonical field ERP CSV column CRM JSON field
business key PMT_NO, for example PMT-000001 paymentNumber, for example PAY000001, nested under the owning invoice
payment_date PMT_DT paymentDate
amount AMT (amount + erp_amount_drift) amount (canonical amount, undrifted)
method PMT_METHOD_CD (upper-cased) method

erp_amount_drift exists specifically so the ERP-recorded amount can legitimately differ from the CRM/true amount -- simulating bank fees or FX rounding that a real reconciliation pipeline must detect rather than silently average away.

Messiness knobs

Each ratio (0.0-1.0) applies independently, per export, with independently seeded RNG streams (see datagen.rng.spawn_rngs) -- so the ERP and CRM copies of the same underlying row diverge realistically instead of matching each other's corruption exactly.

Knob Module Effect
missing_value_ratio datagen.messiness.missing Replaces cells with None, "", "N/A", "NULL", "--", or "unknown"
bad_date_ratio datagen.messiness.dates Rewrites dates as MM/DD/YYYY, DD-MM-YYYY, a Unix timestamp, or an invalid placeholder like 0000-00-00
encoding_issue_ratio datagen.messiness.encoding Corrupts text with mojibake (UTF-8 bytes misread as CP-1252)
duplicate_ratio datagen.messiness.duplicates Appends near-duplicate rows (whitespace/case perturbed on a non-key column)
orphan_ratio datagen.generators.invoices / payments Decides which invoices/payments exist in only one system
amount_drift_ratio datagen.generators.payments + datagen.messiness.mismatches Decides which payments get a legitimate ERP/CRM amount mismatch

Ground truth mapping

With --with-ground-truth, ground_truth.json records, for every invoice and payment: whether it exists in ERP, CRM, or both (expected_match), its business key in each system (or null if absent), and -- for payments -- the exact amount_drift applied. A summary block totals orphan and amount-mismatch counts. This is the answer key: you can score a downstream reconciliation pipeline's output against it for precision and recall.

Pipeline data model

The reconciliation pipeline (src/pipeline/) re-validates every ERP and CRM row into its own target schemas before matching:

Model Purpose
CleanCustomer / CleanInvoice / CleanPayment Post-cleaning record shape, one instance per source-system row, tagged with source_system
QuarantineEntry entity_type, source_system, reason_code, stage, and the row's original data -- written for every row that fails cleaning/validation or ties on entity resolution
CrosswalkEntry entity_type, a freshly minted canonical_id (UUID v4), and the nullable erp_key / crm_key -- one row per matched, orphan, or ambiguous entity
ReasonCode unparseable_date, unrecoverable_encoding, missing_required_field, schema_invalid, ambiguous_match

Entity resolution never uses ID-format shortcuts

CUST_ID (ERP) and customerId (CRM) are cosmetically different encodings of the same underlying sequence number, but only because this is a synthetic dataset -- see the note in src/datagen/keys.py suggesting a pipeline "could" match them by normalizing digits. The reconciliation pipeline deliberately does not do this: a real legacy migration has no such convenient correlation, so matching on it would be pattern-matching a lucky detail of the test fixture rather than doing real entity resolution. Instead, pipeline.resolve matches customers on fuzzy name similarity (rapidfuzz) combined with normalized email/phone comparison, and matches invoices/payments on their already-resolved customer/invoice plus date and amount proximity. Business keys are still used, just only as stable identifiers within a single system, never as the cross-system join key.

Crosswalk persistence and quarantine

data/processed/crosswalk.json is upserted (never overwritten) on every reconcile run, keyed on (entity_type, erp_key, crm_key) so re-running on unchanged input never duplicates a canonical entity -- see pipeline.crosswalk. Every quarantined row is written to data/processed/quarantine_log.json by pipeline.quarantine; an orphan is a valid outcome and is never quarantined. Every intra-system near-duplicate cluster collapsed during cleaning (see "Intra-system near-duplicates" above) is written to data/processed/duplicate_log.json by pipeline.duplicate_log, replaced in full each run like the quarantine log.

Postgres target schema

reconcile run --postgres-dsn <dsn> additionally loads the same run into a Postgres schema (see pipeline.postgres and schema.sql) -- an additional persistence layer, not a replacement for the JSON files above, which remain the source of truth reconcile reads back on the next run. customers / invoices / payments are upserted on (source_system, business_key); crosswalk mirrors the JSON file's reject-on-conflict upsert (a canonical_id is never reassigned), using Postgres 15's UNIQUE NULLS NOT DISTINCT so two NULLs in erp_key/crm_key compare equal, same as the JSON crosswalk's tuple-keyed semantics; quarantine_log is replaced in full each run, matching quarantine_log.json's overwrite behavior.