Langchain ECM contract-history backfill
PR #6397 adds a Dealops 2 import pipeline that reconstructs Langchain’s Salesforce-loaded historical contracts into native frozen Dealops quote graphs, with an engine-replay gate before anything is persisted.
A one-off Langchain ECM backfill under apps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill.
It extracts Salesforce custom objects, reconstructs PricingQuote.input, validates TCV through the real engine, then builds the native graph.
Frozen quotes now stay frozen when opened in the pricing UI or loaded through pricingQuote/get.ts.
The PR blocks several existing auto-mutation paths that could reprice historical quotes to current catalog defaults.
The import is self-validating: reconstructed quote TCV must match Order_Form_Total__c within ±$1.
Failures are quarantined and skipped, not partially imported.
Against UAT sandbox copies: 591/591 contracts and 629/629 members pass the engine-replay gate.
1. Why this exists
Langchain loaded historical contracts directly into Salesforce as ECM custom objects:
Those contracts do not exist as native Dealops records.
The ECM renewal seeder does not read Salesforce order forms.
It walks a real Dealops chain and reads stored quote inputs through resolveEngagementRenewalSeed → accumulateChainProducts.
2. What changes
Server: backfill pipeline
| Area | Files | Material change |
|---|---|---|
| server pipeline | 01_extract.ts, 02_validate.ts, 03_build.ts, 04_rollback.ts |
CLI entrypoints for staged extraction, engine validation, graph creation, and pre-cutover rollback. |
| core import logic | extract.ts, invert.ts, engineContext.ts, plan.ts, buildGraph.ts |
Turns Salesforce rows into Dealops quote input, checks it with the real pricing engine, and persists the ECM graph. |
| tests | invert.test.ts, plan.test.ts, reconstruct.test.ts |
Pure Mocha tests for inversion math, engagement planning, and prior-contract reconstruction. |
| client guards | CategorySelectionStep.tsx, CategoryStep.tsx, ProductSelectionStep.tsx, pricingQuote.util.ts |
Bail out of auto-add, auto-remove, multi-period rewrite, and suggested-price application when isQuoteFrozen is true. |
| server read guard | apps/server/src/trpc/router/pricingQuote/get.ts |
Skips amendment recompute and runtime date/endDate rewrites for frozen quotes. |
| flags/docs | packages/feature-flags/flags.ts, CONTEXT.md, rfcs/2026-07-17-langchain-sf-history-backfill.md |
Enables Langchain engagement/contract-model rendering and documents the ECM import design. |
Before / after
- Langchain historical contracts live in Salesforce only.
- Renewal seeding cannot use them because there is no native
PricingQuote.inputchain. - Opening a frozen quote can silently mutate products, prices, dates, or commitments through existing effects.
- Passing contracts become native
Account → Engagement → DealGroup → OpportunityV2 → PricingQuoterecords. - Imported quotes are frozen to the current catalog and stamped as already CRM-submitted.
- Frozen quotes are served and displayed exactly as stored.
3. How it works
01_extract.ts reads Salesforce and writes staged JSON only.
- Seeds accounts from worklist opportunities.
- Pulls all opportunities on those accounts in full-engagement mode.
- Discovers deal groups via direct links, reverse opportunity links, and primary-opportunity links.
invert.ts reconstructs a native PricingQuoteInput from Salesforce output rows.
- Commit OLIs become
multiCommitments. - Usage rate rows become per-unit product prices.
- Recurring lines become platform-subscription flat fees.
02_validate.ts runs the real pricing engine through EngineContext.
Computed tcv.all.value must match the Salesforce order-form total within ±$1.
03_build.ts is dry-run by default and writes only with --commit.
It is idempotent on the opener opportunity CRM id, so reruns skip already-imported contracts.
Key inversion rules
| Salesforce shape | Dealops shape | Reason |
|---|---|---|
Commit Order_Form_Line_Item__c |
multiCommitments[].tiers[] |
Each commit line carries tier bounds, order, future-option status, and committed amount. |
Rate_Table__c Usage Rate rows |
products[].quotePriceFlat plus commitmentTierPrice |
The real usage product and per-tier rates live on rate rows, not on the generic commitment line. |
| Recurring line item | Flat-fee product with term-adjusted quotePriceFlat |
Full-term recurring amount is divided by annual billing boundary count, so engine replay returns the Salesforce total. |
| Order-form replacement edges | Synthesized prior contract terms | Older prior contracts often have no Salesforce Deal Group; the pipeline reconstructs them from replacement links. |
| SF owner ids | Dealops User ids, with fallback |
The builder resolves salesforceUserId and requires --owner-user-id when needed. |
Frozen quote protection
The PR fixes three mutation paths found by opening imported quotes:
applySuggestedPricesToProducts(..., isFrozen)returns unchanged products for frozen quotes.- Category and product selection effects stop auto-adding/removing rows when
isQuoteFrozen(initialPricingQuote). pricingQuote/get.tsdoes not recompute amendments or rewrite runtime dates for frozen quotes.
isQuoteFrozen
→
stored input survives
Important file groups touched
apps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/README.mdapps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/extract.tsapps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/invert.tsapps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/validate.tsapps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/buildGraph.tsapps/server/src/dealops2/scripts/2026_07_17__langchain_ecm_backfill/04_rollback.tsapps/client/src/dashboard/PricingFlowV2/components/CategorySelectionStep.tsxapps/client/src/dashboard/PricingFlowV2/components/CategoryStep.tsxapps/client/src/dashboard/PricingFlowV2/components/ProductSelectionStep.tsxapps/server/src/trpc/router/pricingQuote/get.ts
4. What it doesn't change
- No new tRPC route or REST API is introduced; this is a script-based backfill plus existing quote-load behavior changes.
- No Prisma schema migration or new import-batch column is added.
- No Salesforce writes happen during extract or validation.
- No historical catalog snapshot is built; the import uses the current catalog because Langchain quote prices are absolute.
- No non-frozen draft quote editing flow should change; the new client guards are gated by
isQuoteFrozen. - No automatic enablement of Langchain deal-group writeback for backfilled contracts; the PR description explicitly warns that doing so could clobber loaded SF data.
- No quarantined contract is imported by
03_build.ts.
5. Risks / rollback / open questions
04_rollback.ts deletes the imported graph by natural CRM keys and clears app-generated quote children scoped to the imported quote/opportunity ids. Do not run it after reps edit imported contracts or after other data references those rows.
The gate prices against the latest catalog. That is intentional because Langchain prices are absolute, but structural drift in calculation specs would show up as quarantine failures.
Mitigation engine replay must match Salesforce TCV before build.
EngineContext warns if the org name is not exactly Langchain, because the variable registry is keyed by org name.
Reviewer check verify UAT vs prod org selection before trusting reports.
Frozen-quote guards touch broad pricing-flow components.
Reviewer check confirm non-imported, non-frozen draft quote editing still auto-selects products and terms as before.
- Spot-check an imported quote after a
03_build --commitrun. - Verify products, net unit price, dates, and TCV in the UI.
- Confirm imported frozen quotes do not mutate on open.