CE Customer Load (AFTEC → Dynamics 365 CE)
The phase-1 customer master load: AFTEC’s three fragmented registries (ACCOUNT.1287 sites,
CUSTOMER bill-tos, SHIPTO delivery addresses) consolidated into one evidence-based model and
loaded into Dynamics 365 CE. Repo: d365-ce-migration, everything under
scripts/consolidation/ and docs/consolidation/ — PLAN.md there is the living plan;
BUILD_LOG.md is the chronology. Read
AFTEC Accounts & Customers first for the source model.
State (2026-09-03)
Sandbox rehearsal complete, verified, and enriched (verify_load.py: ALL CHECKS PASS).
Live sandbox holds 851 accounts / 9,067 contacts / 459 ship-to addresses / 1,878 machine
assets / 2,825 projects — zero duplicates, zero records without a customer. Production is
untouched; the prod run is the same scripts with --target prod --allow-prod after the
gates in PLAN.md (backup, BC-sync pause, ambiguity zero, schema scripts).
The load, in one paragraph
build_customer_records.py + build_ce_preview.py derive the records from AFTEC evidence
(the business-first relationship graph; machines keep customers forever; addresses recovered
via project→payer→ship-to and a human-adjudicated address-decisions.csv; corporate
successors fully merged via successor-decisions.csv — never partially). load_ce.py writes
CE idempotently: every record carries psi_aftecsource (<FILE>!<record id>), which is
both the reporting lineage and the alternate key the loader upserts on. prune_to_model.py
realigns CE after policy changes; verify_load.py is the acceptance gate.
Key decisions
- CE
accountnumber= the AFTEC number (4-digit site code; 6-digit for payer-derived). - One asset per SERIAL — birth-certificate rule; cancelled builds mint nothing.
- Bill-to carried as data (
psi_billtocustomer) until BC connects (no bill-to field on prodaccount; BC virtual tables exist on sandbox only). - Names: account
Name - Site, project2414 - Description, asset7168 - Type(dash, never em-dash; display numbers unpadded, canonicalskps_projectnumberpadded). - Enrichment on load: Relationship Type=Customer, website (AFTEC
WEB.ADDR, else contact email domains), derived primary contact, warranty dates (WARR.START/END, latest coverage), support level from warranty end,psi_decommissionstatus+ evidence from lifecycle signals, Explorer deep link (…/#asset/S{serial}), machine photos from Explorer.
Gotchas that will bite again (details in the repo CLAUDE.md)
- Bulk
UpsertMultipleCREATES against a fresh alternate key fail0x80040216; useCreateMultiplefor new rows. - The authenticated UniData API resolves callers against on-prem AD — cloud-only accounts (progadmin) 500 on every route; the real error is only in the response body.
- Explorer’s photo API keys unpadded job numbers; AFTEC
PHONESneeds the multivalue parser (three shapes, repeated tails). - The live BC↔CE contact sync copies CE loads into BC — pause the mapping before big loads.