Native MSP invoice history
The September 2026 migration stores reviewed MSP attribution on existing native
Invoiced_Items, reconciles selected CRM invoices to issued Xero values, and
replaces redundant custom captures in dependent reports. See the
scope and decisions and
business process.
Prepare and apply
Use an ignored private directory with source.json and decisions.json.
source.json contains full CRM before-images in records, xero_invoices,
xero_credits, tenant_id and captured_at. Capture using GET-only clients in
commercial_services/sources.py; never commit raw exports. Record source
metadata separately. Every supporting service, Plan, charge, Company, invoice
and custom capture used in the mapping belongs in the snapshot.
Decisions are keyed by CRM Invoice ID. Each contains mode (attribution or
reconcile_xero), headers, reason, and items keyed by native item ID.
Each mapped item supplies source_line_id, service_id, charge_id, start,
end, reason, product_exception (empty when unnecessary) and original capture_id.
Unbilled zero rows require an entry in retained_items keyed by item ID, with
a reason.
The crosswalk never relies on row order, a generic item code, or Product alone.
Consult build_preview and its tests for the exact validated decision shape.
python -m commercial_services.native_invoice_migration --directory "$batch_dir"
python -m commercial_services.native_invoice_migration --directory "$batch_dir" \
--apply --expected-batch "$reviewed_batch_hash"
Preview emits only counts/hash and writes the full private manifest. Apply
rebuilds it from sources/decisions, validates metadata compatibility and source
relationships, then updates one existing parent Invoice at a time. It uses
If-Unmodified-Since, trigger: [], GET-only Xero, preflight checks and a
journal containing intent, response and full readback. Existing native item and
Product IDs are retained. A complete matching rerun skips invoice writes;
intervening changes stop the batch. Cross-system reads are not atomic. After
retiring capture evidence, an old preview that depends on those deleted records
will stop at preflight; prepare fresh native evidence for later reconciliation.
Attribution mode cannot change billed values. Reconciliation mode is reserved for explicitly approved source corrections and copies Xero description, quantity and price. The current adapter supports Exclusive/NoTax invoices without source discounts. Other financial representations require a reviewed adapter, not guessed conversion. A CRM-only zero row can be retained untraced; a nonzero untraced row prevents validation.
native_invoice_schema.py defines five item fields and six source-header fields.
Invoice/due dates are source dates, independent of coverage. Source status,
paid/due/credited balances and reconciliation time are snapshots, not ongoing
sync. Operational creation/sync markers are preserved.
Zoho's nominal tax calculation can differ from Xero's sum of rounded line taxes.
tax_representation verifies each source line against the existing nominal rate
and permits only a bounded rounding adjustment. Exact source tax is stored in
Xero_Tax_Amount; the normal Tax field retains Zoho's calculation. Two live
Sharps invoices required a one-cent adjustment, preserving their issued totals.
Native report and existing reader
MSP Native Invoice History is saved in Reconciliation Reports. It joins Invoices with Invoiced Items, filters the six reviewed invoice numbers and excludes empty Xero Line Item IDs. The verified report has 43 rows. Sum line net within currency/source status; do not sum repeated header totals. Coverage, invoice date and payment status answer different questions.
native_invoice_history.validate_history checks exact line/crosswalk coverage,
header totals, nominal versus source tax, source status and payment balances.
It preserves negative lines, avoids duplicating headers, and leaves multi-month
amounts unallocated. Selected credit allocations or credited balances block
this invoice-only validator. The two inspected credits apply outside the six
selected invoices; this does not establish complete customer credit history.
The native_invoice_history entries in config/commercial_services.json
select explicit CRM/Xero IDs and source dates. The margin collector's
collect_native_history loads those invoices, validates native source values,
and replaces only their custom capture rows in memory. It runs before earlier
history collection so the selected June invoice remains available. No transient
row is written to CRM. Source links open native Invoices. Unselected captures
and credits retain their existing behavior. The original capture writers reject
creation of Service Invoice Lines for these migrated source IDs.
Retirement and recovery
The initial 43-record batch completed on 8 September 2026. All records were
verified in the recycle bin, no longer active, and their native replacements
still reconciled. Its original snapshots, exact plan and journal are retained
under the private native-msp-20260908/retirement/ evidence directory.
native_invoice_retirement.build_retirement_plan requires a complete one-to-one
capture crosswalk, matching source/service/Product identities (or an explicit
historical Product exception), and passing native history validation. Archive
the full resulting plan with private_json. Deploy and verify the dependent
native reader before calling retire_captures with its exact batch hash.
The separate retirement client permits only GETs and an exact
DELETE /crm/v8/Service_Invoice_Lines request with wf_trigger=false. It cannot
write invoices, configuration, modules or recycle-bin records. The operation
archives before-images, checks workflows and related notes/files/activities/
emails, rereads every capture and native replacement, rechecks Xero, then
journals the response and verifies recycle-bin presence and unchanged native
replacements. The 100-record API limit bounds each plan. Missing, changed or
unverifiable records stop it; uncertain responses require journal and recycle-bin
inspection before any retry. This is not an unattended recurring cleanup.
There is no documented conditional DELETE in this API. The last reads reduce the concurrency gap but cannot eliminate edits between verification and deletion. Retirement must run during a controlled migration window. Restore an incorrectly retired record from CRM's recycle bin using the archived IDs; verify its native replacement and reader policy before rolling the reader back. The archive retains complete original values beyond the recycle-bin recovery window. Never empty the recycle bin as part of this migration.
API references: Delete Records and Get Deleted Records.
Verification
Tests cover exact scope and identities, source drift, conditional invoice writes, financial corrections, tax rounding, preserved IDs/Products, idempotency, negative/zero/multi-period lines, credits, pre-July reader coverage, duplicate capture prevention and retirement guards. Private live evidence records six reconciled invoices, 43 traced lines, one retained zero row and the native report readback. No Xero write, invoice generation, billing configuration change or customer communication is part of this command.