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 prod account; BC virtual tables exist on sandbox only).
  • Names: account Name - Site, project 2414 - Description, asset 7168 - Type (dash, never em-dash; display numbers unpadded, canonical skps_projectnumber padded).
  • 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 UpsertMultiple CREATES against a fresh alternate key fail 0x80040216; use CreateMultiple for 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 PHONES needs the multivalue parser (three shapes, repeated tails).
  • The live BC↔CE contact sync copies CE loads into BC — pause the mapping before big loads.