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.
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.
hasAnyParent — and a stable Prisma id tiebreaker. The parent-linked row now wins ties, and ordering no longer depends on Postgres row order.
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.
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.
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.
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
after: [ parentMatch, tierChildCount, pricebookMatch, hasAnyParent ]
Non-tiered products
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.
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.
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 — listPriceOverride → fallbackListPriceOverride — 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.
| Test | What 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
- The Prisma query in
loadSalesforceProductsByProduct2Idsis unchanged — samewhere, same currency filter (beadops-94r still in force). - Duplicate-row detection logging (
dupProduct2Ids/errorNotificationService.logWarning) is unchanged; only the surrounding comment was rewritten. - No catalog cleanup. The duplicate
SalesforceProductrows in Plaid's Test Org and prod org remain — this PR makes the writeback robust to them, it doesn't remove them. - v1 writeback path is untouched. This is Dealops 2 only (
apps/server/src/dealops2/services/crm/writeback/implementations/PlaidWriteBack.ts). - The fallback behavior — child max-ramp override as
SBQQ__ListPrice__cwhen no parent SF row is loaded — is preserved verbatim under the renamedfallbackListPriceOverride. - Tiered fan-out logic and child QLI emission are unchanged.
7. Risks & open questions
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.