Plaid renewal quotes stop coming up blank

Two-layer pre-fill for pricingQuote.create: reuse a frozen prior-contract snapshot when available, otherwise read the prior CPQ contract from Salesforce live — matching v1's renewal UX on prod Plaid.

PR dealops#6122 Author @mehulshinde Branch mehul/dealops-gxapmain Files 9 +/- +663 / −21 Area Dealops 2 / pricingQuote Risk prod-affecting

Problem
New quotes on prod Plaid renewal opps came up blank — no products pre-selected. The auto-seed reactor was gated to Plaid Test Org, and even with that gate widened, its dedupe guard skips any opp that already has a quote (every existing prod renewal does).
Fix
Add two seed fallbacks to pricingQuote.create: reuse an existing snapshot if one was frozen on a prior quote, otherwise read the prior CPQ contract from Salesforce inline via a new previewRenewalSeed. Widen the reactor gate to both Plaid orgs.
Tradeoff
The live read reintroduces a synchronous Salesforce call on the create path — which v2 had deliberately moved to a background job. Accepted to match v1 (penguin) renewal UX. Any SF failure degrades to a blank quote, not a broken create.
create handler
snapshot path (DB)
live-read path (Salesforce)
auto-seed reactor
blank-quote outcome

1. Why this exists

Pre-fill in pricingQuote.create only worked when a priorContractSnapshot existed on a sibling quote of the renewal opp. That snapshot is only ever written by the background auto-seed chain:

plaidRenewalAutoSeedReactor → dealops2RenewalInit job → CpqImportService.import()
Gate problem
The reactor was hard-gated to orgName === "Plaid Test Org". Prod "Plaid" was still served by v1, so the chain never fired there.
Dedupe problem
Even with the gate widened, the reactor's dedupe guard at plaidRenewalAutoSeedReactor.ts:120 skips any opp that already has a non-deleted quote. Every existing prod renewal opp already has options — so the snapshot can never be backfilled through that path.
User-visible result
pricingQuote.create's snapshot fallback finds nothing → blank quote → rep starts from scratch instead of editing the prior contract.

2. The seeding flow, step by step

The carousel walks through how a "Create New Quote" request now resolves a seed. Each step lights up the path that fires for one of three opp scenarios.

3. The four-stage fallback in code

The new logic in create.ts layers on top of the existing seed resolver. The order matters — cheaper paths first, expensive Salesforce call last.

OrderSourceCostWhen it winsCarries snapshot?
1 resolveSeedInputFromPreviousOpportunity DB only Opp belongs to a DealGroup chain (the v2 default) n/a (existing path)
2 Sibling-quote snapshot DB only Auto-seed reactor previously wrote a snapshot on this opp yes — re-frozen on the new quote
3 RenewalInitService.previewRenewalSeed Live SOQL No snapshot exists — existing prod Plaid renewal opps no — products only
4 Blank quote All above miss, or live read throws no

4. What each commit changes

596b208 — Snapshot reuse
Adds resolvePlaidRenewalSeed in create.ts. Finds a non-deleted sibling quote with a non-empty priorContractSnapshot, deep-clones it onto the new quote via buildSeedFromPriorContractSnapshot, and persists the snapshot on the new row too. Also wraps buildEngagementRenewalSeed's prior-contract lookup in try/catch so a resolver throw degrades to legacy seeding instead of failing the create.
7182d90 — Enable prod Plaid
In plaidRenewalAutoSeedReactor.ts, swap org?.name !== PLAID_V2_ORG_NAME for !isPlaidOrgName(org.name). Same swap in the pure gate isPlaidV2RenewalSeedCandidate and in the create handler's read gate. The constant PLAID_V2_ORG_NAME stays in the file (still useful for other v2-only paths) but its doc-comment is rewritten.
bccd34c — Diagnostics
Greppable plaidRenewalSeedFallback: info logs at every early-return: not-Plaid org, non-renewal type, no snapshot found, no CRM id, live read found no products, live read threw. Each carries enough context (quote counts, opp id, prior quote id, product count) to diagnose a blank quote in prod without a repro.
379c7c9 — Live SF read
New RenewalInitService.previewRenewalSeed() mirrors initialize() but delegates to CpqImportService.previewImport() — no quote persisted, no events emitted. Reuses the v2 import pipeline so there's no SOQL duplication. The create handler calls it from resolvePlaidRenewalSeedFromLivePriorContract when the snapshot path misses; any throw is caught and logged.

5. previewRenewalSeed vs initialize

The new service method shares the gate and the live SF read with the existing initialize(), then forks at the import call.

initialize() — auto-seed path
  • Called from background job dealops2RenewalInit
  • Calls CpqImportService.import()
  • Writes a PricingQuote row
  • Emits quote.created
  • Freezes priorContractSnapshot (with usage data)
previewRenewalSeed() — live-read path
  • Called inline from pricingQuote.create
  • Calls CpqImportService.previewImport()
  • No DB write
  • No event emit
  • Returns a PricingQuoteInput for the caller

The caller in create.ts uses the returned input as the seed but does not attach a priorContractSnapshot — the live preview reconstructs products from the prior quote's line items, but there's no usage data, so freezing a partial snapshot would mislead later renewals.

6. Snapshot vs live read: caller-side

Snapshot path output
{
  pricingQuoteInput: {
    ...sourceInput,
    products: snapshot.products,
    startDate: snapshot.startDate,
    endDate: snapshot.endDate,
    minimumCommitment: snapshot.minimumCommitment
  },
  priorContractSnapshot: { ... }  // ← carried
}
Live-read path output
{
  pricingQuoteInput: {
    products: [ ...from previewImport ]
    // no commitment, no dates
  }
  // priorContractSnapshot: undefined ← intentional
}

7. What it doesn't change

8. Verification

Type check
pnpm run typecheck — clean.
RenewalInitService
15 passing, including 3 new previewRenewalSeed tests: happy path, no-prior-quote returns null, non-renewal type throws.
create handler
Two new Plaid tests: snapshot path seeds from a sibling quote's frozen snapshot; live-read path stubs RenewalInitService.previewRenewalSeed and verifies products land on the new quote with no snapshot persisted.
Reactor
15 passing. The gate test that previously asserted prod "Plaid" is rejected now asserts it's accepted; a new test verifies non-Plaid orgs are still rejected.

9. Risks & open questions

Prod-affecting: commit 7182d90 reverses an earlier deliberate exclusion (dealops-zwl) that kept v2 quote creation off the v1-served prod "Plaid" org. After merge, new prod Plaid renewal opps will dispatch the v2 auto-seed job, and any "Create New Quote" on a prod Plaid renewal opp without a snapshot will fire a synchronous Salesforce SOQL on the request thread. Reviewers should sanity-check v1/v2 coexistence on prod Plaid before merging.
Synchronous SF on the create path — v2 had deliberately moved Salesforce reads off the request thread into dealops2RenewalInit. The live-read fallback reintroduces one. Author notes this is accepted to match v1 (penguin), which read SF live on every renewal-quote create with no cache or dedupe. There is no rate limiting or short-circuit on repeated creates against the same opp — each create attempt re-hits SF.
Failure posture: the live-read path is best-effort. Any SF throw, missing CRM id, or empty product list logs via plaidRenewalSeedFallback: and degrades to a blank quote — the create itself never fails on a seed-resolution error.
Branch is based on an older main. Rebase before merge (noted by the author).