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.
A central Generic variables registry with reusable defaults: product_list_price_lookup, blendedListPrice, suggestedPrice, productName, defaultApprovalLevel, and subscriptionTerms.
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.
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.
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.
1. Why this exists
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.
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.
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
Generic is now a registered variables registry.
apps/server/src/dealops2/variables/default/registry.tsDefines default variables and
createDefaultProductCalculationSpec().variables/registry.tsRegisters
defaultVariableRegistry before org-specific registries.trpc/router/v3Admin/catalog.tsFirst product in a no-donor org receives Generic calculation wiring.
variables_registry selects the effective registry key.
variables/registryKey.tsReads
{ registryKey }, resolves alias ?? organization.name, and fails closed on DB read errors.variables/registryKeyValidation.tsEnsures alias keys match an actually registered in-code registry.
trpc/router/organizations/create.tsWrites the alias for every new org; blank means
Generic.Staff-only tRPC + UI manage aliases.
dealops2/trpc/variablesRegistry/*Adds
listKeys, get, checkCompatibility, and set.trpc/router/_app.tsMounts
variablesRegistry.VariablesRegistrySection.tsxInternal staff can choose a registry and inspect unresolved config references.
Config-time audit replaces late runtime surprises.
variables/registryCompatibility.tsScans latest PricingSpec and all export templates.
scripts/checkVariablesRegistryCompatibility.tsRead-only all-org or per-org sweep.
trpc/orgSettings/reservedKeys.tsBlocks generic orgSettings CRUD from writing
variables_registry.Before / after at the system boundary
- Registry lookups used the real
Organization.name. - Unregistered orgs got legacy
baseVariablesonly. - Demo clone needed a same-name registry or custom code to reuse formulas.
- Misconfigured variable IDs surfaced during quote compute/export.
- New orgs are pinned to
Genericunless 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.setvalidates compatibility before persisting.
3. How it works
organizations.create validates the requested registry key up front, then writes variables_registry = { registryKey: "Generic" } when the user leaves the registry field blank.
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.
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 |
orgVariable → baseVariable |
termVariableId is treated as a quote-term reference, not a registry variable. |
QuoteExportDocumentTemplate.keywordsMapping |
Only items with source: "formula" |
globalFormula → orgFormula → orgVariable → baseVariable |
Path, DocuSign, and invoice-detail mappings are ignored because they do not hit registry dispatch. |
Runtime files moved to alias-aware resolution
PricingQuoteService.ts, pricingQuote/get.ts, pricingQuote/update.ts, VariablesController.ts, evaluationEngine.ts, pricingEngineSummary.tspricingQuoteExportService.ts, mappingUtils.ts, pricingQuote/getComQuoteByIds.tsPricebookController.ts, MetronomeService.ts, runPlaidE2EWritebackParityHarness.ts4. What it doesn’t change
- It does not add a Prisma migration or new database table; aliasing uses existing
OrgSetting. - It does not expose registry selection to customer admins; routes use
dealopsInternalStaffOnlyProcedure, and UI is gated byisInternalStaff(user). - It does not let generic orgSettings CRUD write
variables_registry; create/update/delete reject that reserved key. - It does not make DB-owned product, price, flow, approval, or CRM specs live in the registry; registries remain code extensions for variables/formulas.
- It does not make unregistered legacy orgs automatically receive every Generic variable in the shown diff; they still get base fallback unless explicitly pinned to
Generic. - It does not remove org-specific registries such as Merge, Plaid, Campfire, Clay, Harvey, Payrails, and others.
5. Tests, seeds, and docs
- ✓
pnpm run typecheckclean for server + client after building@dealops/types. - ✓Targeted mocha green: 85 passing.
- ✓Prettier clean on changed files.
variablesRegistry/router.test.tscovers staff gates, list/get/set/check flows, invalid keys, and incompatible config.registryCompatibility.test.tscovers reference collection and dispatch resolution.registryKey.test.tscovers parsing, alias lookup, malformed values, injected clients, and DB read failure.reservedKeys.test.tsblocks the generic CRUD side door.
packages/prisma/seed.tscan seed avariablesRegistryAlias.Catalog Test Orgis pinned toGeneric.- Catalog e2e now checks first-product Generic calculation wiring and list-price resolution.
- 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
- Run full server suites: PR description says only targeted mocha was run, not full mocha (~5.8k) and jest (~1.4k).
- Run seed e2e: CI should exercise both Campfire Test Org and Merge Alias Demo Org.
- Re-run all-org compatibility sweep:
checkVariablesRegistryCompatibility.tsshould reflect the final Generic/default behavior and should produce fewer gaps where defaults resolve. - Resolve behavior mismatch: confirm whether the final intended default model is “all orgs receive default variables automatically” or “new/selected orgs are pinned to Generic via alias while legacy unregistered orgs stay base-only.”
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.
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.