Plaid v1↔v2 writeback parity harness

A destructive Salesforce E2E script that generates the same Plaid quote in both Dealops v1 and v2, writes both to SFDC, reads them back, and reports every structural diff with v1 as ground truth.

PR dealops#6046 Author @yijunz166 Base main Files 26 +LOC 130,444 Status Open Touches Dealops 1 + 2

What it adds
A single ~3.7k-line TS script runPlaidE2EWritebackParityHarness.ts that drives both v1 and v2 quote creation, calls Salesforce writeback on each side, and diffs the readback payloads.
What it changes
One flag-system addition in packages/feature-flags/flags.ts. Everything else is new scripts, docs, and checked-in run artifacts.
What it preserves
Application code paths are untouched. The harness is an external integration test — no tRPC, Prisma, UI, or pricing logic changes ship in this PR.
What it produces
Two checked-in destructive prod runs (20 June use-cases, 21 June sheet cases) with full SFDC readback JSON and a v1-ground-truth markdown report.
Dealops v1 (Penguin)
Dealops v2
Salesforce CPQ
Structural diff

1. Why this exists

Dealops v2 is being rolled out as the replacement for the v1 (Penguin) pricing flow for Plaid. Before flipping customers over, the team needs confidence that a v2 quote, once written through to Salesforce CPQ, produces the same SBQQ__QuoteLine__c rows and field values that v1 would have produced for the same input.

Unit tests and golden-file fixtures can't reach that far — the truth lives in what Salesforce stores after both sides hit the SFDC writeback path. So this PR adds a destructive end-to-end harness that exercises the real path on disposable prod opportunities, then mechanically compares the readback payloads.

v1 is treated as ground truth. A "diff" means v2 wrote something different from what v1 wrote for the same quote input. The output of this PR isn't a passing test suite — it's a list of 448 structural diffs that become implementation tickets for the v2 writeback to close.

2. How the harness runs

Each case in the input file describes one quote and two Salesforce opportunities. For every case the script does six things, in order:

case input JSONL row v1 Penguin quote pricing flow + flatten v2 Dealops 2 quote use-case or SKU bundle SFDC writeback v1 opp (disposable) SFDC writeback v2 opp (disposable) readback QLI rows, with retries diff v1 vs v2

The same logical quote (a use-case bundle or an explicit SKU list) is materialized twice — once in v1's product/price shape, once in v2's — written to two separate but topologically-equivalent disposable opportunities, then both quote line item sets are read back from Salesforce and diffed row-by-row.

3. Case file shape

The runner accepts JSONL, a JSON array, or { "cases": [...] }. Each case identifies both sides and describes the quote. Two case styles are supported:

Style A — Use-case bundle

Picks every product the v2 pricing spec marks as part of a named Plaid use case. Used for the 20 June sweep over all 11 v2 use cases.

{
  "caseId": "income-verification-10k",
  "quote": {
    "useCase": "Income Verification",
    "pricebook": "CPQ",
    "currency": "USD",
    "defaultVolume": 10000
  },
  "v1": { "organizationId": "...",
          "userId": "...",
          "opportunityId": "..." },
  "v2": { "organizationId": "...",
          "userId": "...",
          "opportunityId": "..." }
}
Style B — Explicit SKUs

For sheet-driven custom bundles. Supports tiered prices, price/percent/volume ramps, linear ramps, and ramped monthly minimums.

{
  "caseId": "custom-identity-skus-10k",
  "quote": {
    "pricebook": "CPQ", "currency": "USD",
    "defaultVolume": 10000,
    "products": [
      { "productCode": "AUT - UF", "volume": 10000 },
      { "productCode": "IDV1 - UF", "volume": 10000,
        "tiers": [...],
        "flattenTierStrategy": "blended" }
    ]
  },
  "v1": { ... },
  "v2": { ... }
}

Product resolution

FieldResolvesWhen to use
productCodeSame code on both sidesDefault — works for most Plaid SKUs.
v1ProductIdv1 directlyv1 has duplicate product codes.
v2ProductSpecIdv2 directlyv2 spec has ambiguous names.
nameDisambiguatorCombined with code when needed.

4. Safety surface

This script writes to real Salesforce. The PR description leads with a "safety / false-positive audit" for that reason. Four guards stack on top of each other:

Bare-flag confirmation
Prod writes require --writes-to-prod-sfdc as a bare flag. --writes-to-prod-sfdc=false is explicitly rejected, not silently treated as falsy.
Bare-flag cleanup
--clean-opportunities-before-writeback is also bare-only. It deletes existing OLIs and clears the primary quote before each writeback. Intended for disposable opps only.
Duplicate-row safety
Diffing compares occurrences of quote lines, not a keyed overwrite by product/segment/threshold. Two identical QLIs on one side stay distinguishable from one QLI on the other.
Topology-aware diff
SBQQ__RequiredBy__c is compared by normalized parent/child shape, not Salesforce ID strings — so a parent-child relationship that exists on both sides doesn't false-positive just because the IDs differ.

What counts as "acceptable" vs structural

FieldTreatmentWhy
SBQQ__Number__cAcceptableSalesforce-assigned display order.
SBCF_Net_Unit_Price__cAcceptableDerived formula field. Source price/discount stay structural.
SBQQ__Quote__c, SBQQ__SegmentKey__c, SBQQ__Dimension__cAcceptableRun-specific IDs that can't be expected to match across orgs.
Price fields within ±1¢AcceptableRounding noise, not a logic diff.
Everything else listed belowStructuralRow presence, pricing, discount, subscription, billing, approval, parent topology.

5. The 21 June destructive run

The PR also checks in the final prod run that motivated shipping the harness. The input is the Plaid pricing-team sheet, converted to JSONL. S11–S15 were empty templates in the sheet and are skipped.

Coverage
11 cases requested → 11 compared → 0 failed before comparison.
Structural diffs
448 real structural diffs across the 11 compared cases.
Pinned artifact
all-11-clean-pinned/report.v1-ground-truth.md — the human-readable diff the team works from.

Top recurring diff fields (21 June run)

SBQQ__EndDate__c
high
SBQQ__SubscriptionBase__c
high
SBQQ__DefaultSubscriptionTerm__c
high
SBQQ__ListPrice__c / OriginalPrice / CustomerPrice
med
SBQQ__Discount__c
med
__missing_on_v2__ (S5, S7)
med
SBQQ__RequiredBy__c topology
low

Bar widths are illustrative relative ranking from the PR's "top repeated diffs" summary, not absolute counts. See report.real-diffs.md for exact tallies per field.

How to read a diff

v1 (expected)
{
  "ProductCode": "PILDL-UF",
  "SBQQ__CustomerPrice__c": 6.30,
  "SBQQ__Discount__c": 0.05,
  "SBQQ__DefaultSubscriptionTerm__c": 12,
  "SBQQ__RequiredBy__c": "<parent: Plaid Income (LDL)>"
}
v2 (actual)
{
  "ProductCode": "PILDL-UF",
  "SBQQ__CustomerPrice__c": 6.30,
  "SBQQ__Discount__c": null,            // diff
  "SBQQ__DefaultSubscriptionTerm__c": null,  // diff
  "SBQQ__RequiredBy__c": "<no parent>"        // diff
}

6. Run artifacts checked in

Two run directories ship in this PR. Each contains both the structured Salesforce readback payloads and the human-readable reports.

DirectoryCasesComparedFailedDiffsNotes
2026-06-20-all-usecases-10k/ 11 v2 use cases 9 2 290 Failures: inbound-bank-payments-10k, outbound-bank-payments-10k — v1 couldn't resolve Transfer (Same-day ACH) - High Risk Customers.
2026-06-21-prod-v1-v2-sheet-cases/all-11-clean-pinned/ S1–S10, S16 11 0 448 Run with --clean-opportunities-before-writeback. Pinned as the canonical reference.
Output file layout per run

7. What this PR does not change

8. Risks & open questions

The script is destructive by design. Running it against the wrong opportunity will delete its existing OLIs and replace the primary quote. The bare-flag requirement on both --writes-to-prod-sfdc and --clean-opportunities-before-writeback is the main guard. Reviewers should confirm the v1/v2 opportunity IDs checked into the run artifacts are the agreed-upon disposable test opps.
Two cases fail before comparison on 20 June. The v1 side can't resolve Transfer (Same-day ACH) - High Risk Customers by product code. Real diff coverage for inbound/outbound bank payments is therefore missing in that run. The 21 June sheet run compares all 11 of its cases successfully, so the gap is bundle-specific, not harness-wide.
The 448 diffs are the deliverable. They aren't bugs in the harness — they're the v2 writeback work that needs to happen next. Top suspects from the recurring fields above: v2 isn't setting line-level subscription terms, isn't propagating discounts to the readback, and is producing different end-date / subscription-base values than v1.

Things worth asking in review