AI quoting posture parameters foundation
This draft adds the Dealops 2 data model, defaults, seeding path, resolver, and e2e coverage for good/better/best quote posture parameters.
A new QuotingParameter model stores one posture value per organization and parameter, with room for future user-level and derived values.
definitions.json is the single defaults file. New orgs are seeded in the existing create-org transaction at default value 5.
resolvePosture(orgId, userId?) returns every defined parameter with an org baseline plus the rep override when present.
The Prisma migration is deliberately absent. Until it lands, the new e2e tests fail because the table does not exist in CI.
1. Why this exists
This is T1 of the good/better/best quoting chain: DEA-7341 → DEA-7345.
- This PR creates the posture storage and read model.
- Later PRs add APIs, prompt wiring, and 3-quote generation.
The quote generator needs a durable answer to “what posture should this org or rep use?” before UI and prompt work can safely build on top.
2. What changes
packages/prisma/schema/models/v2/quoting-parameter.prisma
Adds QuotingParameter, QuotingParameterScope, and QuotingParameterName.
scopeORG or USER; user-level rows are future-ready.parameterEnum-backed vocabulary: DEAL_SIZE, CLOSE_CHANCE.valueConfigured posture value, intended range 1–10.derivedValueReserved observed value for later outcome-derived posture display.updatedByUserIdAudit hook for later admin/API updates.Uniqueness is database-enforced for both row shapes.
@@unique([organizationId, parameter], where: { scope: "ORG" })permits only one org baseline per parameter.@@unique([organizationId, scope, userId, parameter])permits only one user row per parameter.- The partial unique handles Postgres
NULLbehavior for ORG rows, whereuserIdis null.
| File / area | Change | Why reviewers should care |
|---|---|---|
posture/definitions.json |
Defines DEAL_SIZE and CLOSE_CHANCE, descriptions, and default 5. |
Adding a future parameter should be one JSON entry plus matching enum/migration work. |
posture/definitions.ts |
Loads JSON from disk and validates with Zod. | Deploys fail loudly if definitions are malformed, duplicated, or missing an enum member. |
seedDefaultQuotingParameters.ts |
Creates ORG rows from definitions using createMany({ skipDuplicates: true }). |
Seeder is idempotent and is intended to be the only creator of ORG rows. |
quotingParameterRepository.ts |
Lists ORG rows and, optionally, one user's USER rows in a single query. | Read path is org-scoped and centralized ahead of APIs. |
resolvePosture.ts |
Builds the resolved posture payload: description, org value, rep value. | Downstream prompt rendering can consume a stable shape. |
organizations.prisma, users.prisma |
Add relation fields back to QuotingParameter. |
Enables real FKs to Organization and User. |
merge-test/tests.ts |
Adds e2e coverage for seeding, uniqueness, missing seed behavior, and resolver precedence. | Validates the actual DB constraints and resolver behavior, not just unit-level mapping. |
3. How it works
definitions.json declares each parameter and default value.
definitions.ts requires valid range, no duplicates, and full enum coverage.
createOrganization calls the seeder inside the existing transaction.
orgValue and repValue.
loadQuotingParameterDefinitions() reads the JSON file next to the module and validates each item.
parametermust be aQuotingParameterName.defaultValuemust be an integer from1to10.- Every enum member must be present exactly once.
apps/server/src/utils/organizations.ts now seeds quoting parameters next to existing org settings.
await seedDefaultOrgSettings(tx, org.id, { pricingFlowType });
await seedDefaultQuotingParameters(tx, org.id);
resolvePosture(orgId, userId?) makes rep-level values additive, not required.
- ORG row exists →
orgValue. - USER row exists →
repValuefrom user row. - No USER row →
repValue = orgValue. - Missing ORG row → throw loudly.
No durable quoting posture vocabulary or seeded per-org baseline existed for the good/better/best renderer to read.
- No
QuotingParametertable. - No global defaults file.
- No resolver contract for prompt code.
The system has a database-backed org baseline and a stable resolver shape that future APIs and prompt wiring can build on.
- Org defaults are created transactionally.
- Uniqueness is enforced in the DB.
- Rep overrides are represented without another migration.
4. Test coverage
Creates one ORG row per definition, at the JSON default, and can be run twice without extra rows.
Attempts to insert a second ORG row for DEAL_SIZE reject with a unique constraint error.
resolvePosture rejects when an org has no ORG row, treating it as a provisioning bug.
Org edits win over file defaults; USER rows become repValue; missing USER rows mirror orgValue.
The e2e tests create TEST-prefixed throwaway organizations and clean up quotingParameter, orgSetting, and organization rows in finally.
5. What it doesn't change
- No Prisma migration is included yet; the schema is staged, but the table is not created in CI.
- No tRPC API for reading or updating quoting parameters. That is
DEA-7342. - No prompt rendering or strategy-block integration. That is
DEA-7343. - No good/better/best quote generation behavior. That is
DEA-7344. - No UI for admins or reps to change values.
- No derived-value computation;
derivedValueandderivedAtare schema placeholders for later observed values.
6. Risks / rollback / open questions
QuotingParameter, enums, FKs, and partial unique indexes.
Before the migration lands, rollback is just reverting code and schema additions. After migration, rollback also needs to drop the new table/enums or leave them unused.
The migration needs to backfill existing organizations with ORG rows; otherwise resolvePosture correctly throws for those orgs.
The 1–10 range is validated in app code and future tRPC schemas, not as a DB CHECK constraint.