Combined variables registry defaults + per-org aliases

PR #6426 lands the Generic registry, a staff-managed variables_registry alias, and compatibility checks so new/demo Dealops 2 orgs can reuse pricing variables and export formulas without code changes.

Author: @mehulshinde PR: dealops#6426 Ticket: DEA-7042 Stack: Dealops 2 State: open Files: 47 Diff: +2692 / -181 Head: mehul/dea-7042-combined-variables-registry

What it adds

A central Generic variables registry with reusable defaults: product_list_price_lookup, blendedListPrice, suggestedPrice, productName, defaultApprovalLevel, and subscriptionTerms.

What it changes

New organizations now persist a variables_registry OrgSetting. Blank selection pins them to Generic; staff can point demo orgs at richer registered org registries such as Merge or Plaid.

Guardrail

A staff-only tRPC router and Admin UI validate registered keys and run a config compatibility check before saving aliases, preventing quote/export-time “variable not found” failures.

Important nuance

The diff preserves legacy base-only fallback for unregistered existing orgs; the Generic registry is selected explicitly via alias, especially for newly created orgs and seeded Catalog Test Org.

Generic registry
OrgSetting alias
tRPC + Admin UI
Compatibility checker
Runtime call sites
Seeds + docs + tests
85targeted mocha tests passing
4new variablesRegistry routes
1reserved OrgSetting key
3merge conflicts resolved as union/rename

1. Why this exists

Problem today

Variables registries are keyed by exact organization names and mostly live as in-code org extensions.

  • New no-code orgs can lack common variables used by Editor-authored specs.
  • Demo orgs that need an existing org’s formula suite currently require code or naming tricks.
  • Bad registry/config combinations fail late during quote computation or export.
Combined fix

This branch combines PR #6385 and PR #6383 so the two registry changes land together.

  • Generic raises the floor for newly created DB-configured orgs.
  • Alias lets a demo org reuse a richer registered registry.
  • Checker catches unresolved references before the alias is saved.
Read this nuance before review: the PR description says the earlier default-registry PR made every org resolve a shared default set and made getOrganizationRegistry() never return undefined. The combined diff shown here instead keeps getOrganizationRegistry() returning undefined for unregistered orgs and pins new orgs to Generic through variables_registry. Reviewers should confirm this is the intended combined behavior.

2. What changes

Default registry

Generic is now a registered variables registry.

New file
apps/server/src/dealops2/variables/default/registry.ts
Defines default variables and createDefaultProductCalculationSpec().
Registry boot
variables/registry.ts
Registers defaultVariableRegistry before org-specific registries.
Catalog
trpc/router/v3Admin/catalog.ts
First product in a no-donor org receives Generic calculation wiring.
Alias storage

variables_registry selects the effective registry key.

New helper
variables/registryKey.ts
Reads { registryKey }, resolves alias ?? organization.name, and fails closed on DB read errors.
Validation
variables/registryKeyValidation.ts
Ensures alias keys match an actually registered in-code registry.
Create org
trpc/router/organizations/create.ts
Writes the alias for every new org; blank means Generic.
Management surface

Staff-only tRPC + UI manage aliases.

Router
dealops2/trpc/variablesRegistry/*
Adds listKeys, get, checkCompatibility, and set.
App route
trpc/router/_app.ts
Mounts variablesRegistry.
Admin UI
VariablesRegistrySection.tsx
Internal staff can choose a registry and inspect unresolved config references.
Compatibility + safety

Config-time audit replaces late runtime surprises.

Checker
variables/registryCompatibility.ts
Scans latest PricingSpec and all export templates.
Script
scripts/checkVariablesRegistryCompatibility.ts
Read-only all-org or per-org sweep.
Reserved key
trpc/orgSettings/reservedKeys.ts
Blocks generic orgSettings CRUD from writing variables_registry.

Before / after at the system boundary

Before
  • Registry lookups used the real Organization.name.
  • Unregistered orgs got legacy baseVariables only.
  • Demo clone needed a same-name registry or custom code to reuse formulas.
  • Misconfigured variable IDs surfaced during quote compute/export.
After
  • New orgs are pinned to Generic unless staff chooses another registry.
  • Aliased orgs resolve variables/formulas through the selected registered key.
  • Runtime call sites use resolveRegistryOrganizationName() instead of hand-rolled org-name reads.
  • variablesRegistry.set validates compatibility before persisting.

3. How it works

Registry resolution
const { effectiveRegistryKey } =
  await resolveEffectiveRegistryKey(organizationId);

const organizationName = effectiveRegistryKey ?? '';

Call sites now thread the effective key into getAllVariables(), formula contexts, export mapping, pricebook selection, Metronome, and parity harness paths.

Alias write path
assertRegisteredRegistryKey(registryKey);

const report = await checkRegistryCompatibility({
  organizationId,
  registryKey,
});

if (report.failures.length > 0) throw BAD_REQUEST;

variablesRegistry.set rejects typos and incompatible aliases before the OrgSetting row is upserted.

Compatibility checker dispatch rules

Reference source Collected IDs Resolution order Notable exclusion
PricingSpec.pricingSpecData variableId, variableIds, *VariableId orgVariablebaseVariable termVariableId is treated as a quote-term reference, not a registry variable.
QuoteExportDocumentTemplate.keywordsMapping Only items with source: "formula" globalFormulaorgFormulaorgVariablebaseVariable Path, DocuSign, and invoice-detail mappings are ignored because they do not hit registry dispatch.

Runtime files moved to alias-aware resolution

Quote compute
PricingQuoteService.ts, pricingQuote/get.ts, pricingQuote/update.ts, VariablesController.ts, evaluationEngine.ts, pricingEngineSummary.ts
Exports
pricingQuoteExportService.ts, mappingUtils.ts, pricingQuote/getComQuoteByIds.ts
Related services
PricebookController.ts, MetronomeService.ts, runPlaidE2EWritebackParityHarness.ts

4. What it doesn’t change

5. Tests, seeds, and docs

Verified in branch
  • pnpm run typecheck clean for server + client after building @dealops/types.
  • Targeted mocha green: 85 passing.
  • Prettier clean on changed files.
New test coverage
  • variablesRegistry/router.test.ts covers staff gates, list/get/set/check flows, invalid keys, and incompatible config.
  • registryCompatibility.test.ts covers reference collection and dispatch resolution.
  • registryKey.test.ts covers parsing, alias lookup, malformed values, injected clients, and DB read failure.
  • reservedKeys.test.ts blocks the generic CRUD side door.
Seeds + e2e
  • packages/prisma/seed.ts can seed a variablesRegistryAlias.
  • Catalog Test Org is pinned to Generic.
  • Catalog e2e now checks first-product Generic calculation wiring and list-price resolution.
Knowledge base
  • Adds knowledge/concepts/variables-registry-alias.md.
  • Indexes the concept in knowledge/concepts/index.md.
  • Records the implementation note in knowledge/log.md.

6. Risks / rollback / open questions

Before merge
Operational rollback

For a bad alias on one org, clear or change the variables_registry OrgSetting via the staff-only router/UI. Resolution is read per request, so no server restart is required.

Code rollback blast radius

Reverting the PR removes the router/UI, Generic seeding behavior for new orgs, alias-aware runtime resolution, the compatibility script, and the Catalog Test Org Generic pinning.