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.
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).
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.
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()
orgName === "Plaid Test Org". Prod "Plaid" was still served by v1, so the chain never fired there.
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.
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.
pricingQuote.create. The handler will try the existing engagement-renewal seed first; everything else only runs if that returns null.
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.
| Order | Source | Cost | When it wins | Carries 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
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.
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.
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.
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.
- Called from background job
dealops2RenewalInit - Calls
CpqImportService.import() - Writes a PricingQuote row
- Emits
quote.created - Freezes
priorContractSnapshot(with usage data)
- Called inline from
pricingQuote.create - Calls
CpqImportService.previewImport() - No DB write
- No event emit
- Returns a
PricingQuoteInputfor 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
{
pricingQuoteInput: {
...sourceInput,
products: snapshot.products,
startDate: snapshot.startDate,
endDate: snapshot.endDate,
minimumCommitment: snapshot.minimumCommitment
},
priorContractSnapshot: { ... } // ← carried
}
{
pricingQuoteInput: {
products: [ ...from previewImport ]
// no commitment, no dates
}
// priorContractSnapshot: undefined ← intentional
}
7. What it doesn't change
- The
PLAID_V2_ORG_NAMEconstant still exists — only its doc-comment is updated. Other v2-only behaviors that must stay off prod Plaid still reference it. - The auto-seed reactor's dedupe guard (
plaidRenewalAutoSeedReactor.ts:120) is untouched — opps with existing quotes are still skipped by the reactor; the create-handler fallbacks are what cover them. CpqImportServiceitself is unchanged.previewImport()was already there; this PR only consumes it through a new service method.- The DealGroup-based renewal seed (
resolveSeedInputFromPreviousOpportunity) keeps priority. Non-Plaid orgs and Plaid opps that do have a DealGroup chain hit the existing path first and never reach the new fallback. - No new tRPC routes, no schema changes, no Prisma migration.
8. Verification
pnpm run typecheck — clean.
previewRenewalSeed tests: happy path, no-prior-quote returns null, non-renewal type throws.
RenewalInitService.previewRenewalSeed and verifies products land on the new quote with no snapshot persisted.
"Plaid" is rejected now asserts it's accepted; a new test verifies non-Plaid orgs are still rejected.
9. Risks & open questions
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.
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.
plaidRenewalSeedFallback: and degrades to a blank quote — the create itself never fails on a seed-resolution error.
main. Rebase before merge (noted by the author).