@primocaredentgroup/conventions
v0.1.3
Published
A conventions component for Convex.
Readme
Convex Conventions
A Convex component for managing healthcare conventions across dental and specialist clinics.
The component is designed for scenarios where the business entities already exist in other systems:
- clinics are referenced by external string IDs
- list rows are referenced by external string IDs
- service categories are referenced by external string IDs
- service category parents are referenced by external string IDs
What it models
A convention has:
nameenabled- optional
expiresAt - either a generic percentage discount or a set of specific rules
Specific rules can target:
- a single
list_row - a
service_category - a
service_category_parent
Each specific rule can apply either:
- a percentage discount
- a fixed price
Business rules
The current version enforces these rules:
- A convention in
generic_discountmode cannot have specific rules. - A convention in
specific_rulesmode cannot definegenericDiscountPercent. - Rule precedence is deterministic:
list_rowservice_categoryservice_category_parent
- A convention only applies when:
- it is enabled
- it is not expired at the provided
evaluatedAt - the target clinic is linked to the convention
Schema
The component uses flat relational tables:
conventionsconventionClinicsconventionRules
This keeps lookups indexed and avoids deep nested documents.
Installation
Install the package and register the component in your app:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import conventions from "@primocaredentgroup/conventions/convex.config.js";
const app = defineApp();
app.use(conventions);
export default app;Wrapper usage
The package exposes a ConventionsComponent class that wraps the component API.
// convex/api.ts
import { mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import { v } from "convex/values";
import { ConventionsComponent } from "@primocaredentgroup/conventions";
const conventions = new ConventionsComponent(components.conventions);
export const createConvention = mutation({
args: {
name: v.string(),
},
returns: v.id("conventions"),
handler: async (ctx, args) => {
return await conventions.createConvention(ctx, {
name: args.name,
enabled: true,
expiresAt: null,
pricingMode: "generic_discount",
genericDiscountPercent: 15,
});
},
});
export const resolvePrice = query({
args: {
conventionId: v.id("conventions"),
clinicExternalId: v.string(),
listPrice: v.number(),
evaluatedAt: v.number(),
listRowExternalId: v.optional(v.string()),
serviceCategoryExternalId: v.optional(v.string()),
serviceCategoryParentExternalId: v.optional(v.string()),
},
returns: v.object({
conventionId: v.id("conventions"),
conventionName: v.string(),
applies: v.boolean(),
rejectionReason: v.union(
v.null(),
v.literal("disabled"),
v.literal("expired"),
v.literal("clinic_not_linked"),
),
listPrice: v.number(),
finalPrice: v.number(),
appliedRuleSource: v.union(
v.literal("none"),
v.literal("generic_discount"),
v.literal("list_row"),
v.literal("service_category"),
v.literal("service_category_parent"),
),
adjustmentType: v.union(
v.null(),
v.literal("discount_percent"),
v.literal("fixed_price"),
),
adjustmentValue: v.union(v.null(), v.number()),
appliedRuleId: v.union(v.null(), v.id("conventionRules")),
}),
handler: async (ctx, args) => {
return await conventions.resolveConventionPrice(ctx, args);
},
});Public API
The component currently exposes:
conventions.createConventionconventions.updateConventionconventions.setConventionClinicsconventions.getConventionconventions.listConventionsrules.addConventionRulerules.removeConventionRulepricing.resolveConventionPrice
Example scenarios
The example app included in this repository seeds two realistic conventions:
- a generic discount convention for fitness partners
- a rule-based convention with:
- linked clinics
- a
service_category_parentrule - a
service_categoryrule - a
list_rowfixed-price override
It also demonstrates these resolution outcomes:
- generic discount
- list row precedence
- category precedence
- parent category fallback
See example/convex/example.ts and example/src/App.tsx.
Development
Install dependencies:
npm installRun tests:
npm test
npm run lint
npm run typecheckStart local development:
npm run devNotes
- The example app inside this repository uses local source imports so it can run before the package is built.
- Consumer applications should import from
@primocaredentgroup/conventionsand@primocaredentgroup/conventions/convex.config.js.
Found a bug or want to request a feature? Open an issue.
