Plaid v2 Phase 1: deterministic bundle parent QLI emission

Fix duplicate-row tie-breaking in the Plaid writeback so FCRA bundle umbrellas survive deduplication, and source the parent QLI's list price from the parent SF row instead of the child's override.

PRdealops#6044 Author@mehulshinde Branchmehul/dealops-f5cn TicketDEA-6423 Files3 Δ+323 / −20 AreaDealops 2 · CRM writeback

Problem
Plaid's catalog has duplicate SalesforceProduct rows per (org, Product2Id, currency). The reducer's tie-break was unstable, so the parent-linked duplicate was sometimes dropped — and with it the entire bundle umbrella QLI.
Fix
Rank candidates with a fourth tuple slot — hasAnyParent — and a stable Prisma id tiebreaker. The parent-linked row now wins ties, and ordering no longer depends on Postgres row order.
Bonus fix
Parent QLI SBQQ__ListPrice__c now sources from parentSfRow.UnitPrice instead of the child's max-ramp override. Matches the v1 writeback for FCRA bundles where the parent is a $0 umbrella.
Child product row
Parent product row
Winner (post-fix)
Loser / dropped duplicate

1. Why this exists

DEA-6423 reported missing bundle parent QLIs for Plaid's FCRA services bundles and base report when writing back from Dealops v2 to Salesforce CPQ. The umbrella line — the row that stitches child QLIs together via SBQQ__RequiredBy__c — was silently absent on some writes and present on others against the same quote.

Root cause, one paragraph
The Plaid Test Org (and prod) has multiple SalesforceProduct rows for the same (organizationId, Product2Id, CurrencyIsoCode) triple — a catalog sync artifact. Some duplicates carry TIER children, some carry a parent link, some carry neither. The reducer in loadSalesforceProductsByProduct2Ids used result.set(...) in iteration order over Postgres's unordered output. Whichever duplicate landed last won. When the winner was a row with no parent link, parent?.Product2Id was null downstream, and the bundle parent QLI was never emitted.

2. The duplicate-row scenario

Concrete shape of what the database returns for one FCRA child Product2Id. Both rows match the requested currency; one has a parent link, one doesn't.

Candidate A PICKED (post-fix)
id: sf-child-with-parent
Product2Id: 01tFCRA_CHILD parent → 01tFCRA_PARENT TIER children: 0
Candidate B previously won by accident
id: sf-child-without-parent
Product2Id: 01tFCRA_CHILD parent: null TIER children: 0

Before the fix, isPlaidSalesforceProductRowBetter ranked both rows to the same tuple (no expected parent context, no tiers, no pricebook match), so the comparator returned false and the reducer kept whichever row Postgres emitted last. After the fix, the new hasAnyParent slot breaks the tie in favor of Candidate A.

3. The rank tuple, before / after

The ranking lives in isPlaidSalesforceProductRowBetter. Higher tuple wins, lexicographically. The shape depends on whether the product is tiered.

Tiered products

before: [ parentMatch, tierChildCount, pricebookMatch ]
after:   [ parentMatch, tierChildCount, pricebookMatch, hasAnyParent ]

Non-tiered products

before: [ parentMatch, pricebookMatch, tierChildCount ]
after:   [ parentMatch, pricebookMatch, tierChildCount, hasAnyParent ]

The new hasAnyParent slot is always last, so it only matters when every earlier slot ties. That preserves existing behavior: a row with more TIER children still beats a parent-linked sibling with fewer tiers, because tierChildCount sits at index 1 (tiered) or index 2 (non-tiered) and breaks the tie before hasAnyParent is consulted.

Finally, when even hasAnyParent ties, the comparator falls through to a stable candidate.id < incumbent.id string compare on the Prisma row id — making selection deterministic across runs.

if (candidate.id && incumbent.id && candidate.id !== incumbent.id) {
  return candidate.id < incumbent.id;
}
return false;

4. Parent list price: child override → parent SF row

The second change is independent of the dedup fix. Even when the umbrella QLI was being emitted before, its SBQQ__ListPrice__c and SBQQ__OriginalPrice__c were being sourced from the child's max-ramp override. For FCRA bundles the parent is a $0 umbrella while the child carries the actual $80k service fee — so the parent QLI ended up with the child's price stamped on it.

Before
SBQQ__ListPrice__c:
  listPriceOverride,
SBQQ__OriginalPrice__c:
  parentSfRow?.UnitPrice ?? 0,

ListPrice from child's max-ramp; OriginalPrice from parent SF row. Asymmetric and wrong for FCRA.

After
SBQQ__ListPrice__c:
  parentSfRow?.UnitPrice
    ?? fallbackListPriceOverride,
SBQQ__OriginalPrice__c:
  parentSfRow?.UnitPrice ?? 0,

Both fields prefer the parent SF row's UnitPrice. Child override is a fallback only.

The renamed local — listPriceOverridefallbackListPriceOverride — signals its demoted role: it's no longer the primary source, only the v1-compatibility fallback used when the parent row is unavailable.

5. Test coverage

The bulk of the diff (+297 LOC) lives in PlaidWriteBack.test.ts. Four new tests pin the duplicate-row reducer behavior, plus one new test in parentBody.test.ts and one in the composite-request suite for the list-price change.

TestWhat it locks down
parent-linked row wins on TIER tie Two duplicates, both with 0 TIER children. The parent-linked row is selected; summary.parent.Product2Id survives into the result.
more TIER children still wins over parent link Regression guard: the new tiebreaker is strictly the lowest priority. A 3-tier orphan still beats a 1-tier parent-linked sibling.
stable row id as final fallback Two duplicates identical on every rank slot. The lexicographically smaller Prisma id (a-earlier-id) wins, regardless of array order.
resolver carries parent link into metadata maps End-to-end through resolvePlaidProductReferences: the parent SF row survives into parentSfRowsBySfProductId and the PBE map keys match.
parent QLI uses parent SF row UnitPrice buildPlaidCompositeRequest emits ListPrice = 0 when parentSfRow.UnitPrice = 0, even though the child override is 80,000.
parentBody.test.ts: ListPrice = 4242 Updated existing assertion. Was 99.99 (child override); now 4242 (parent UnitPrice). Test comment updated to reflect new contract.

6. What this PR doesn't change

7. Risks & open questions

TODO(cjd review) left in source. The author flags that fallbackListPriceOverride defaults to 0 when the child's quotePriceFlat is tiered or absent, and asks for confirmation that Plaid's SF org accepts ListPrice = 0 on the parent during the diff harness. The new test asserts ListPrice = 0 is emitted, but doesn't verify SF acceptance.
Low blast radius. The new rank slot sits at the bottom of the tuple, so it only changes selection when all earlier slots tie — which is exactly the duplicate-row pathology this PR targets. Non-Plaid orgs and non-duplicate Plaid rows take the same path as before.
Related PR. The author calls out #6043 as touching the same logic; their FCRA + Assets-5 verification covers the interaction. Worth confirming merge order with that PR's author.