Plaid v2 writeback: three independent canary parity fixes

One bundle, three logically independent diffs uncovered by the same canary-run-15 forensic trace — approval ceilings, full parent bodies, and a soft-skip that stops one bad product from blanking entire quotes.

PRdealops#5777 Author@mehulshinde Branchmehul/dealops-020.25 Files6 +/−+848 / −14 AreaDealops 2 · Plaid writeback StatusOpen

What it fixes
Three concrete v1↔v2 canary diffs on the Plaid writeback pipeline: approval levels capped at 1, skeletal bundle parent bodies, and a single missing pricebook entry crashing entire v2Payloads.
How it ships
Two JSON config edits, ~155 lines of new .ts in PlaidWriteBack.ts, and three new Mocha test files. No schema migrations, no API surface changes.
What it preserves
Segment-fan-out and monthly-minimum branches keep their existing SBCF_Approval_Level__c: null omission. The MM-omission regression pin stays green.
What's deferred
Datasheet backfill for 6 L1-only SKUs (v1 source ambiguity) and a SOQL probe to determine why IDM-AD's PricebookEntry is missing. Both have follow-up beads.
FA1 — Approval ceiling activation
FA2 — Parent-body field set
FA3a — Soft-skip missing PBEs
FA3b — Tier-suffix product wiring

1. Why this exists

Canary-run-15 surfaced a cluster of v1↔v2 writeback diffs on real Plaid quotes. A single forensic trace identified four independent root causes that all live in (or feed) PlaidWriteBack.ts. Each is logically separable; they're bundled because they share a touch surface and were uncovered together.

Symptom on canaryRoot causeFix area
Every Plaid product capped at approval level 1, regardless of price l2price/l3price/l4price defined in registry but not declared on the spec — engine never evaluates them FA1
Bundle umbrella QLI emits 5 fields vs v1's ~19 named + extraFields emitParentIfNeeded writes a skeletal parent body; parent SF row was never plumbed to the build phase FA2
11 of 14 failed quotes blanked entirely on canary; all hit IDM-AD Resolver throws PLAID_PIPELINE_PRICEBOOK_ENTRY_NOT_FOUND for one product, losing the whole v2Payload FA3a
Tier-child QLIs (TRR-T1/T2, INT-T1/T2/T3) dropped with "no v2 productSpec" additionalCrmProductIds schema field exists and is consumed, but zero Plaid specs populated it FA3b

2. The four fixes at a glance

FA1Activate L2/L3/L4 approval-price variables. Pure config edit to pricingSpec.json.

Before · productShared.variables
[
  { "name": "l1price" }
]

Variables exist identically in the registry via makeApprovalPriceVariable, but the evaluation engine only walks variables declared on the spec.

After · productShared.variables
[
  { "name": "l1price" },
  { "name": "l2price" },
  { "name": "l3price" },
  { "name": "l4price" }
]

Engine now emits all four ceilings into pricingEngineSummary.byProduct[*].variables; computePlaidApprovalLevel's rc>0 guard now sees real values.

Sufficient for ~96 products with full L1..L4 datasheet coverage. Six L1-only SKUs still need a v1 data backfill — see deferred items.

3. How FA2 wires the parent body together

The parent-body field set is the largest single change. It requires plumbing data that wasn't previously in scope at the emitParentIfNeeded call site.

Resolver side — only pay the cost when needed

if (parentProduct2IdsToResolve.size > 0) {
  const parentSfRows = await this.loadSalesforceProductsByProduct2Ids({
    organizationId,
    product2Ids: Array.from(parentProduct2IdsToResolve),
    quoteCurrency,
  });
  for (const [product2Id, row] of parentSfRows) {
    parentSfRowsBySfProductId.set(product2Id, {
      Product2Id: row.Product2Id,
      UnitPrice: row.UnitPrice,
      CurrencyIsoCode: row.CurrencyIsoCode,
      Description: row.Description,
      extraFields: row.extraFields,
    });
  }
}

The size > 0 guard means non-bundle quotes pay zero extra DB cost. The map is keyed by parent SF Product2Id — same key as the existing parentPricebookEntryIdBySfProductId.

Build side — spread order matters

body: {
  // Hardcoded defaults FIRST so extraFields can override on collision
  // (mirrors v1's constructPenguinQuoteLineItemData spread order).
  SBQQ__PriceEditable__c: true,
  SBQQ__SubscriptionType__c: 'Renewable',
  SBQQ__BillingType__c: 'Monthly Usage',
  // ... 5 more constants
  ...safeParentExtraFields,
  // Structural fields LAST so they always win
  SBQQ__Quote__c: quoteRefPlaceholder,
  SBQQ__Product__c: parentSfProductId,
  SBQQ__ListPrice__c: listPriceOverride,
  SBQQ__OriginalPrice__c: parentSfRow?.UnitPrice ?? 0,
  CurrencyIsoCode: parentSfRow?.CurrencyIsoCode ?? pricingQuote.input.targetCurrency,
  SBCF_Approval_Level__c: 0,  // hardcoded, not via resolver
  // ...
}

4. What this PR explicitly does NOT change

5. Tests

pricingSpec.ceilingVariables.test.ts
FA1 activation pin (l2/l3/l4 in productShared.variables) + FA3b additionalCrmProductIds pin for TRR and ITH specs. 3 tests .skip() with TODOs for the deferred L1-only product backfill.
PlaidWriteBack.parentBody.test.ts
FA2 parent-body contract: SBCF_Approval_Level__c: 0 literally pinned (not via resolver), all 14 named fields literal-pinned, extraFields spread verified, SBQQ__RequiredBy__c asserted absent on parent.
PlaidWriteBack.softSkip.test.ts
FA3a resolver returns skippedProductIds, no throw, paging error logged with the skipped product id, good products still emit QLIs.
PlaidWriteBack.mm.test.ts
Extends REFERENCES_KEYS contract pin to include the two new resolver outputs (parentSfRowsBySfProductId is a build param; skippedProductIds is resolver-output-only).
Local test status: pnpm run test:mocha --grep "020.25" from apps/server → 16 passing, 3 pending, 0 failing. Full suite has 9 failures that pre-exist on origin/main (Phase A managedBy schema work in #5775 landed without a matching writebackSpec.json update; verified by checking out clean main). Same applies to pnpm run typecheck.

6. Intentionally deferred

FA1 secondary data backfill — 6 L1-only products (Additional History / Audit Copy / PDF Copy of Assets, in 1-Acct and 5-Accts variants).

Each affected SKU has two v1 Product rows in prod Plaid org 0ea42205-b945-46fc-af48-7d4c838edcfa:

The existing v2 L1 entry shape matches the second; canary bN19N: SF=4 evidence implies v1 production walks the first. Unblocking requires tracing which v1 row v1's production resolver actually uses at runtime. Backfill script + investigation plan filed locally.

FA3a SF SOQL probe — determine whether IDM-AD's PricebookEntry is genuinely missing/inactive in the canary org, vs. queryPricebookEntriesRaw having a filter bug. The soft-skip prevents the crash; root-cause fix lands separately.

7. Reviewer attention points

Spread order in parent body
Named defaults BEFORE the extraFields spread; structural fields AFTER. This mirrors v1's constructPenguinQuoteLineItemData precedence. Reordering will silently change emitted output.
SBCF_Approval_Level__c: 0 is literal
Do NOT route this through resolvePlaidApprovalLevelForQli, even when it's tempting after FA1 lands. v1 hardcodes 0 on parents; the resolver is never called.
SBCF_Net_Unit_Price__c strip
The destructure-then-spread pattern (const { SBCF_Net_Unit_Price__c: _omit, ...safe } = extraFields ?? {}) is load-bearing — SF hard-rejects inserts carrying this non-createable formula field.
REFERENCES_KEYS contract
parentSfRowsBySfProductId is listed (it's a build param). skippedProductIds is intentionally NOT — it's resolver-output-only and rides the spread as a harmless extra key.