Plaid v2 diff harness: end-to-end at last
The replay_pending_input_transform placeholder is gone — the harness can now reconstruct a v2 PricingQuoteInput from v1 SF data and diff the would-write QLI bodies against v1's baseline.
A real v1→v2 input transform in runPlaidDiffHarness.ts, plus a new PlaidWriteBack.buildHarnessDryRunQliPayload entry point that produces would-write QLI bodies without touching SF or DB.
The ≥1-week clean-window countdown for Plaid v2 writeback rollout. The harness can now produce real diffs on every replayed quote instead of bailing at the placeholder.
Phase 1 SF preprocessing, Phase 3 composite POST, the canary feature flag for live writes, and the --smoke-replay mode. All untouched.
1. Why this exists
The diff harness shipped in #5603, but it had no way to turn a v1 PricingFlow into a v2 PricingQuoteInput. Every replayed quote returned replay_pending_input_transform with a v1-baseline-only record — useful for spot-checks, useless as a clean-window signal.
- dealops-ee8 #5648 — v2 CPQ Import reconstruction
- dealops-n5u #5654 — renewals + amendments port
- dealops-cjd #5568 — supporting infra
- canary install #5604 — seeded canary org
≥1 week of replayed quotes where v2's would-write QLI bodies match v1's actual SF writes within the documented diff tolerances. This PR is the gate: no transform, no countdown.
2. The pipeline, end to end
Per-quote flow. Step through to see which stages run, which are skipped, and where the org boundary sits.
V1ReplayInput per quote: the SF Quote Id, the PricingFlow's additionalData, and metadata. No SF reads yet.
3. The new public method: buildHarnessDryRunQliPayload
Lives on PlaidWriteBack. Runs Phase 2 prep (resolvePlaidProductReferences) and Phase 2 build (buildPlaidCompositeRequest) against the canary org, then filters the composite sub-requests down to just the QLI create bodies.
- Loads
pricingSpecfor the canary org - Resolves product/pricebook references via live SF reads on the canary's Test SF instance
- Builds a minimal in-memory
PricingQuotewith placeholder ids - Best-effort sales-rep resolution (omits the field if no
userId) - Filters composite sub-requests to
POST→QUOTE_LINE_SOBJECT_PATH
- Phase 0: DB PricingQuote lookup
- Phase 1: Opportunity preprocessing (SF deletes)
- Phase 3: Composite/graph POST to SF
- Feature flag bypassed — the harness needs to see v2 output regardless of canary flag state
- Opportunity PATCH and quote-create sub-requests (filtered out of the return value)
Safety contract
crmService's read-only path (queryPricebookEntriesRaw against the canary Test SF org). Prisma access is read-only: pricingSpec, salesforceProduct, pricebookEntries. The placeholder opportunity id (__harness_dry_run_opportunity__) is never sent to SF because Phase 3 doesn't run.
4. Two design calls worth flagging
Both bypass paths in reconstructV2InputForHarness exist for the same root cause: the canary's SF connection points at a different SF org than where v1's prod quotes live.
CpqImportService.previewImport()Canary install e846ccc7-… connects to the Plaid Test SF org. v1 prod quotes live in a different SF org reachable only via v1 install 4045adc1-….
Passing a v1 prod quote id to previewImport would fail — the quote doesn't exist in the canary's SF instance.
Fix: read SF data through the v1 connection, then call ee8's reconstruction functions directly with the canary's specs.
ee8's reconstructPlaidQuoteMetadata resolves pricebook by crmPricebookId, which is per-SF-org. The prod pricebook id will never match a canary pricebook seeded against the Plaid Test Org.
Fix: resolveCanaryPricebookId uses v1's additionalData.pricebook ('CPQ' / 'Partnership') + the quote currency to look up by name ('CPQ USD', 'Partnership EUR', …) with a prefix fallback.
5. The silent bug caught during self-review
SBQQ_QUOTELINE_FIELDS_TO_READ was missing three fields that ee8's SalesforceQuoteLineItem shape requires. The cast v1Rows as unknown as SalesforceQuoteLineItem[] silenced the gap entirely — at runtime, those fields would be undefined, every product match would fail (no SBQQ__ProductCode__c), and every reconstructed quote would have products: []. Blank v2 payloads, useless reports, only caught on a Render smoke run.
SBQQ__ProductCode__c— matched against v2productSpec.productCodeSBCF_Net_Unit_Price__c— primary price source forProductInputSBQQ__TotalDiscountAmount__c— fallback price math
A new _QliCoverageCheck type-level assertion derives the set of non-optional fields from ee8's SalesforceQuoteLineItem and requires that SBQQ_QUOTELINE_FIELDS_TO_READ contains all of them. If ee8 adds a required field and the SOQL SELECT doesn't pick it up, this file fails to compile.
Show the type assertion
type _RequiredQliFields = keyof {
[K in keyof SalesforceQuoteLineItem as undefined extends SalesforceQuoteLineItem[K]
? never
: K]: true;
};
type _ReadFields = (typeof SBQQ_QUOTELINE_FIELDS_TO_READ)[number];
type _QliCoverageCheck = _RequiredQliFields extends _ReadFields
? true
: `Missing ee8-required QLI field(s) in SBQQ_QUOTELINE_FIELDS_TO_READ`;
const _qliCoverageCheck: _QliCoverageCheck = true;
6. Date arithmetic: matching ee8 exactly
End-date computation lives in two places now (harness + ee8's reconstructPlaidQuoteMetadata), so they must produce identical output for the diff to be meaningful. Both use dayjs.utc — the month-end edge case is the load-bearing reason.
| Input | Native Date.setMonth | dayjs.utc (used by both) |
|---|---|---|
2026-01-01 + 12mo − 1d |
2026-12-31 |
2026-12-31 ✓ |
2026-01-31 + 1mo − 1d |
2026-03-02 (rolls into March) |
2026-02-27 ✓ (clamps to Feb 28, then −1d) |
7. Test coverage
19 unit tests in the new runPlaidDiffHarness.test.ts, all running under Mocha (matches the new-tests-default-to-Mocha convention).
- Exact
{type} {currency}match - Currency disambiguation between siblings
- Type disambiguation between siblings
- Prefix fallback when exact name is missing
- Error: no pricebook matches type at all
- Error:
additionalDatanull / missing / non-string
- Validation: unsupported currency, missing/negative term, invalid date
- Metadata pass-through: currency, term, pricebook id, CRM id, null start date
- End-date arithmetic: clean boundary + Jan 31 month-end clamp
buildHarnessDryRunQliPayload. Per scoping doc §6, integration-tested via post-merge smoke run. Also out of scope: a reconstructPlaidQuoteMetadata refactor to accept a pricebook-resolver strategy — filed as a follow-up so the harness can drop its inline metadata construction.
8. What it doesn't change
--smoke-replaymode is unchanged and still requiresplaidWritebackPipelineV2on for the canary.- Phase 1 (opportunity preprocessing) and Phase 3 (composite POST) in
PlaidWriteBackare untouched — the new dry-run method is a sibling, not a replacement. - The canary feature flag still gates real writes.
buildHarnessDryRunQliPayloadbypasses the flag by design, but only because it produces no writes. - v1 prod's SF write surface — the harness only reads.
diffQuoteslogic itself; this PR just feeds it real v2 payloads instead of empty stubs.
9. Risks & rollback
buildHarnessDryRunQliPayload is a new method with no callers outside the harness, so removing it can't regress production writeback.
10. Open questions
- How tolerant should the diff be of QLI ordering differences between v1 (insertion order in the prod SF org) and v2 (composite request order)? Out of scope here but the first batch of smoke-run diffs will surface this.
- Should
reconstructPlaidQuoteMetadatagrow a pricebook-resolver strategy parameter so the harness stops duplicating metadata construction? Filed as follow-up; the duplication is small and well-commented for now.