Plaid Implementation Services land on Partnership in 4 currencies

Closes the v2-vs-SFDC gap: 4 Impl Services SKUs become quotable on the Partnership pricebook in USD, CAD, EUR, and GBP — with deterministic-id datapoints and a regression test that locks the shape.

PRdealops#6011 Author@mehulshinde Branchmehul/plaid-impl-services-partnership Files5 +/-+3522 / −16 AreaDealops 2 onboarding Follows#5999 · #5956

What it adds
Partnership availability for the 4 Implementation Services SKUs across USD, CAD, EUR, GBP in v2 onboarding — 4 Partnership pricebooks wired into each productSpec and 80 net-new datasheet rows.
What it changes
listPriceCurrencies grows from ["USD","CAD"] to ["USD","CAD","EUR","GBP"]. pricebookIds gains 4 Partnership entries per SKU. Datasheet gets LIST_PRICE + APPROVAL_PRICE L0–L3 rows for each new (SKU × currency).
What it preserves
The pre-existing orphan Partnership USD LIST_PRICE rows keep their legacy ids. CPQ USD + CPQ CAD wiring, prices, and tiers are untouched. CPQ EUR/GBP are deliberately not added.
CPQ pricebook
Partnership pricebook
Newly added datapoints
Legacy USD list (id preserved)
Out of scope

1. Why this exists

v1 and Salesforce already let reps quote the 4 Impl Services SKUs on Partnership in USD, CAD, EUR, and GBP. v2 onboarding was the lone outlier: its productSpec only advertised CPQ USD + CPQ CAD, so the Partnership pricebook never appeared in the UI for these SKUs.

The smoking gun
The datasheet already contained one orphan Partnership USD LIST_PRICE row per SKU (pricebook 42b3ff71…) — but no productSpec ever consulted it, and no CAD/EUR/GBP companions existed.
The fix shape
Make the gating real (currencies + pricebookIds) and complete the matrix in the datasheet. Mirror CPQ USD prices 1:1 with only the currency code swapped — no FX, matching how Plaid stores these in SFDC.

2. The SKUs in scope

SKUProduct IDListL0L1L2L3
Impl Services (Standard) — 3 mod00540ac…20340360,00060,00030,00015,0000
Impl Services (Custom) — 4 moe0598304…2b33cc80,00080,00040,00020,0000
Impl Services (Custom) — 5 mo205c9927…d7f0b7100,000100,00050,00025,0000
Impl Services (Custom) — 6 mof26bb12f…36cc43120,000120,00060,00030,0000

These same numeric values are written into every target currency. CAD/EUR/GBP rows just swap the currency field.

3. The availability matrix

Each cell shows what each (SKU × pricebook) pair looks like after this PR. The two CPQ EUR/GBP columns are intentionally not wired — they're a separate scope.

CPQ USD
CPQ CAD
Partnership ×4
CPQ EUR/GBP
Standard 3mo
had
had
+ list × 3
+ approval × 16
+ legacy USD list
Custom 4mo
had
had
+ list × 3
+ approval × 16
+ legacy USD list
Custom 5mo
had
had
+ list × 3
+ approval × 16
+ legacy USD list
Custom 6mo
had
had
+ list × 3
+ approval × 16
+ legacy USD list

Per-SKU Partnership row math: 1 legacy USD LIST_PRICE (kept) + 3 new LIST_PRICE (CAD/EUR/GBP) + 4×4 = 16 new APPROVAL_PRICE (USD/CAD/EUR/GBP × L0–L3) = 20 Partnership rows per SKU. Across 4 SKUs: 80 datapoints, of which 4 are legacy and 76 are new.

4. What changes, file by file

pricingSpec.json (× 2 orgs)
For each of the 4 productSpecs: append the 4 Partnership pricebook ids to pricebookIds, expand listPriceCurrencies to ["USD","CAD","EUR","GBP"]. Both plaid/ and plaid-test-org/ are real files — they must move in lockstep.
datasheet.json
Append 76 net-new datapoints. The Partnership USD orphan rows are left alone with their legacy ids; only CAD/EUR/GBP LIST_PRICE rows and USD/CAD/EUR/GBP APPROVAL_PRICE L0–L3 rows are added. plaid-test-org/datasheet.json is a symlink to plaid/datasheet.json, so one write covers both orgs.
apply_impl_services_partnership_backfill.ts
Idempotent backfill script (scripts/penguin/2026_06_18_…). Re-running is a no-op. Computes deterministic uuidv5 ids, reads CPQ USD as the source of truth for approval tier values, and refuses to proceed if the symlink invariant or CPQ EUR/GBP exclusion is violated.
datasheet.partnershipImplServices.test.ts
698-line lock test (Mocha). Pins gating fields, datapoint envelopes, currency coverage, the legacy-USD-id allowance, the CPQ EUR/GBP exclusion, the symlink target, CPQ CAD preservation, and global id uniqueness.

5. How the deterministic ids work

New datapoint ids are not random. They're generated via uuidv5(name, namespace) using two fixed namespaces — one per datapoint type — so the same (productId, pricebookId, [tier]) tuple always resolves to the same id, on any machine, in any re-run.

LIST_PRICE namespace
f2a1c8d4-3b9e-5d6a-8c1b-e7d5f0a3b9c2
LIST_PRICE name
LIST_PRICE|{productId}|{pricebookId}
APPROVAL_PRICE namespace
6c0e3a1b-7d8f-4c2e-9f1a-d3b2e0c5f6a8
APPROVAL_PRICE name
APPROVAL_PRICE|{productId}|{pricebookId}|{tier}

The pre-existing Partnership USD LIST_PRICE rows predate this scheme, so they keep their legacy ids. The test asserts presence/value/uniqueness for those rows but deliberately does not pin the id — that's the only exception in the entire spec.

6. How the backfill script protects itself

The script in scripts/penguin/… is the only piece of executable code in the PR. It produced the JSON edits and is checked in so it remains re-runnable. Key invariants it enforces before writing:

Symlink check
plaid-test-org/datasheet.json must be a symlink pointing at ../plaid/datasheet.json. If someone replaced it with a real file, the script refuses to run rather than silently writing only one copy.
CPQ EUR/GBP guard
If a productSpec's pricebookIds already mentions the CPQ EUR or CPQ GBP pricebook, the script throws — its scope explicitly excludes those, and finding them means something else has drifted.
Source-of-truth reads
Approval tier values are read out of the existing CPQ USD APPROVAL_PRICE rows (not hardcoded). CPQ USD LIST_PRICE and the legacy Partnership USD LIST_PRICE are both verified against the expected numeric list price before any writes happen.
Upsert semantics
upsertDatapoint returns one of created / updated / unchanged. A second run reports all 76 rows as unchanged and skips the file write entirely. A final pass asserts no duplicate ids exist in the entire datasheet.
Excerpt — id generation and upsert
function computeApprovalPriceDpId(productId, pricebookId, tier) {
  return uuidv5(
    `APPROVAL_PRICE|${productId}|${pricebookId}|${tier}`,
    APPROVAL_PRICE_DP_ID_NAMESPACE,
  );
}

function upsertDatapoint(datasheet, datapoint) {
  const matches = findDatapoints(datasheet, { ...tags });
  if (matches.length > 1) throw new Error("refusing to upsert: duplicate selector");
  if (matches.length === 0) { datasheet.push(datapoint); return 'created'; }
  if (JSON.stringify(matches[0]) === JSON.stringify(datapoint)) return 'unchanged';
  datasheet[datasheet.indexOf(matches[0])] = datapoint; return 'updated';
}

7. The lock test

The new datasheet.partnershipImplServices.test.ts intentionally hardcodes every deterministic id and every expected envelope, rather than re-deriving them from the generator. That makes the test a true regression lock: a future change that alters either the id formula or the price shape will fail the test rather than silently pass.

Gating assertions
Per SKU per org: listPriceCurrencies is exactly [USD,CAD,EUR,GBP]; pricebookIds is exactly the 2 CPQ + 4 Partnership ids; CPQ EUR/GBP are explicitly absent. Plaid and plaid-test-org are pinned identical.
Datasheet assertions
Per (SKU × currency): exactly one LIST_PRICE with the right value/currency/pageId; exactly four APPROVAL_PRICE rows with deterministic ids and tiered envelope shape. Per SKU: exactly 20 Partnership rows; zero CPQ EUR/GBP rows.
Cross-PR guards
CPQ CAD rows from the sibling backfill (#5999) are pinned to verify this PR doesn't disturb them. The Partnership USD LIST_PRICE count is asserted to be exactly one — a guard against someone later adding a fresh deterministic-id row alongside the legacy one.
Structural guards
plaid-test-org/datasheet.json is asserted to be a symlink with the correct target. Every id in the entire datasheet is asserted globally unique.

8. Before / after gating

Before
"pricebookIds": [
  "6bbd64a3…",  // CPQ USD
  "2bfb3b00…"   // CPQ CAD
],
"listPriceCurrencies": ["USD", "CAD"]

Partnership orphan USD LIST_PRICE sits in the datasheet, unreachable.

After
"pricebookIds": [
  "6bbd64a3…",  // CPQ USD
  "2bfb3b00…",  // CPQ CAD
  "42b3ff71…",  // Partnership USD
  "faf43b56…",  // Partnership CAD
  "bbc87bef…",  // Partnership EUR
  "b3556e19…"   // Partnership GBP
],
"listPriceCurrencies": ["USD","CAD","EUR","GBP"]

9. What it doesn't change

10. Risks & rollback

Low risk. The change is data-only inside apps/server/src/dealops2/onboarding/plaid*. No live schema, no runtime code paths, no migration. Rollback is reverting the commit; no data cleanup needed because nothing has been written to a database.
Watch items. (1) The Partnership USD LIST_PRICE orphan keeps a legacy id; if anyone later regenerates ids from the deterministic formula and forgets the carve-out, a duplicate-USD-list row would appear. The test guards against this. (2) The symlink invariant is load-bearing — replacing plaid-test-org/datasheet.json with a real file would silently halve future backfills. Both the script and the test refuse to proceed in that case.

11. Testing

From the PR description: in each of USD / CAD / EUR / GBP, on a new v2 quote with pricebook set to Partnership, the 4 SKUs should be selectable at the expected list prices:

Each price equals the USD number with only the currency code swapped. Approval metadata should resolve to L0–L3 with thresholds matching the table in §2. Manual screenshots for CAD and GBP are attached to the PR.