Plaid live-read renewals get the full snapshot

The Plaid renewal live-read path now seeds the prior-contract snapshot — Previous Price, Existing Product tags, and prior usage — so renewal quotes stop pricing as fresh catalog items.

PR dealops#6126 Author @mehulshinde Base main Head mehul/plaid-renewal-live-snapshot Files 6 Diff +173 / −57 Follow-up to #6122 Area Dealops 2 · tRPC

Problem
Live-read renewal quotes on Plaid pre-selected products but had no Previous Price column, no Existing Product tags, and 0/mo volumes — so reps were pricing renewals as fresh deals and triggering L1/L2/L3 approvals.
Fix
Lift snapshot assembly out of CpqImportService.import() into a shared helper, return it from previewImport(), and carry it through RenewalInitService onto the quote created by pricingQuote.create.
Bonus
Persisting the snapshot on the first live read means the next "Create New Quote" on the same opp uses the fast DB snapshot path — no second Salesforce round trip.
CpqImportService
RenewalInitService
pricingQuote.create
PriorContractSnapshot

1. Why this exists

This is the follow-up to #6122, which added a live Salesforce read so a Plaid renewal create wouldn't fall through to a blank quote when the auto-seed listener hadn't fired yet. That PR fixed the empty-quote bug but only seeded products: it deliberately skipped the prior-contract snapshot and the prior-usage fetch, leaving the rep with a quote that looked renewal-ish (products pre-selected) but priced like net-new.

What the rep saw (post-#6122)
  • Products pre-selected yes
  • Previous Price column missing
  • "Existing Product" tags missing
  • Volume / sparklines 0 / mo
  • Discount approval flow L1/L2/L3
What the rep should see (this PR)
  • Products pre-selected yes
  • Previous Price column populated
  • "Existing Product" tags populated
  • Volume / sparklines from billed actuals
  • Discount approval flow renewal at prior price

The root cause was structural: CpqImportService.import() assembled the snapshot inline inside its DB write, so anything that called previewImport() instead (the live-read path) had no way to reach that logic.

2. What changes

Refactor: buildPriorContractSnapshot as a shared helper

The snapshot assembly — prior-price products, committed minimum, contract dates, and the fetchPriorUsageData call — moves out of import()'s inline insert payload into a private method. import() behavior is unchanged; previewImport() gains the ability to return a snapshot when capturePriorContractSnapshot is set.

Before — inline in import()
const usageDataForRenewal = args.capturePriorContractSnapshot
  ? await fetchPriorUsageData({ ... })
  : null;

await repo.create({
  ...
  ...(args.capturePriorContractSnapshot
    ? { priorContractSnapshot: {
        products: built.pricingQuoteInput.products,
        multiCommitments: ...,
        minimumCommitment: ...,
        startDate: ...,
        endDate: ...,
        usageDataForRenewal,
      } }
    : {}),
});
After — shared helper
const priorContractSnapshot = args.capturePriorContractSnapshot
  ? await this.buildPriorContractSnapshot(args, built)
  : null;

await repo.create({
  ...
  ...(priorContractSnapshot ? { priorContractSnapshot } : {}),
});

// previewImport() can now call the same helper:
const priorContractSnapshot = args.capturePriorContractSnapshot
  ? await this.buildPriorContractSnapshot(args, built)
  : undefined;

The snapshot payload

What buildPriorContractSnapshot returns — and now flows end-to-end:

Wiring: service boundary → handler

The three layers chain the snapshot through. RenewalInitService.previewRenewalSeed now requests it and re-narrows the unknown-typed preview field at the service boundary. The handler then forwards both pieces onto the repository create call.

previewImport previewRenewalSeed resolvePlaidRenewalSeedFromLivePriorContract PricingQuoteSeed repository.create
// RenewalInitService.previewRenewalSeed — now requests the snapshot
const preview = await this.cpqImportService.previewImport({
  sfdcOpportunityId: args.sfdcOpportunityId,
  opportunityV2Id: opp.id,
  sfdcQuoteId: priorQuoteId,
  capturePriorContractSnapshot: true,  // ← new
});

return {
  pricingQuoteInput: preview.pricingQuoteInput as PricingQuoteInput,
  priorContractSnapshot:
    (preview.priorContractSnapshot as PriorContractSnapshot | undefined) ?? null,
  ...
};
// pricingQuote/create.ts — live-read path now carries the snapshot
return {
  pricingQuoteInput: preview.pricingQuoteInput,
  ...(preview.priorContractSnapshot
    ? { priorContractSnapshot: preview.priorContractSnapshot }
    : {}),
};

Call-site gate on Plaid org name

A small but pointed change to the create handler: the isPlaidOrgName check moves up to the call site, so the Plaid fallback is now a true no-op (no DB lookups, no log lines) for every other org. The original guard inside resolvePlaidRenewalSeed stays as defense-in-depth, but silently.

Before
if (!seedInput) {
  const plaidRenewalSeed = await resolvePlaidRenewalSeed({...});
  // ↑ runs for every org, then logs "skipped"
}
After
if (!seedInput && isPlaidOrgName(ctx.organization.name)) {
  const plaidRenewalSeed = await resolvePlaidRenewalSeed({...});
}
// Inner guard is now a silent return.

3. How it works end-to-end

  1. Rep clicks "Create Quote" on a Plaid renewal opp. The normal seed path (deal-group memberships → existing snapshot) finds nothing — the auto-seed listener hasn't run yet.
  2. The handler's new gate evaluates !seedInput && isPlaidOrgName(...) as true and calls resolvePlaidRenewalSeed.
  3. That resolver locates the prior contract quote in Salesforce and calls RenewalInitService.previewRenewalSeed with capturePriorContractSnapshot: true.
  4. CpqImportService.previewImport runs the same pipeline as import(), then calls buildPriorContractSnapshot — which fires fetchPriorUsageData against usage_and_mrr__c for billed actuals.
  5. The handler receives both pricingQuoteInput and priorContractSnapshot and passes them to repository.create in a single transactional insert.
  6. The quote row is created with the snapshot already persisted — the next "Create New Quote" on this opp uses the fast snapshot path with zero Salesforce reads.
Atomicity preserved. The snapshot is captured before the insert (so a Salesforce failure aborts the seed with no quote row left behind) and persisted in the same insert (so a renewal quote never exists without its snapshot, keeping the auto-seed listener's dedupe guard accurate).

4. Tests

SuiteWhat it pins down
CpqImportService (14 passing) Refactor preserves import()'s snapshot persistence — the inline-to-helper move is invisible to callers.
RenewalInitService (15 passing) New asserts: previewRenewalSeed passes capturePriorContractSnapshot: true and returns the snapshot (incl. usageDataForRenewal) for the caller to carry.
pricingQuote.create Plaid live-read test Asserts the full snapshot lands on repository.create's relationsArg.priorContractSnapshot — flipping the previous "snapshot must be undefined" assertion.
pricingQuote.create regression test (new) Non-Plaid org create never calls previewRenewalSeed and never looks up a snapshot — the new call-site gate is exercised directly.

5. What it doesn't change

6. Risks & rollback

New Salesforce dependency on the create critical path (Plaid only). The live-read path now makes an additional fetchPriorUsageData call before the quote insert. Existing posture holds: any Salesforce/import failure degrades to a blank quote rather than failing the create — same try/catch as the rest of resolvePlaidRenewalSeedFromLivePriorContract. Worth watching p95 on Plaid renewal creates after rollout.
Rollback is clean. Reverting this PR restores the post-#6122 behavior (products-only live seed). No data migration needed — snapshots already persisted by this code remain valid; they were always intended to be present.