Kaizen
Browse modulesPricingpricing/serverClasses

Class: PricingMatrixService

Defined in: server/pricing/pricing-matrix-service.ts:222

Orchestration layer for the pricing tier x schedule matrix (PLA-136). Owns the full cell lifecycle (add/update/delete tiers and schedules, set/unset cell prices) and is the ONLY code path permitted to construct matrix-managed Rules/Conditions rows — RuleService.create/ createVersion and ConditionService.create/createVersion/ softDelete all guard against direct construction of these rows (see MatrixManagedResourceError) and this service bypasses those guards everywhere, including unsetCellPrice, by writing through its own internal repository access (this.repos) rather than calling through the guarded service methods. unsetCellPrice used to call through the guarded, generic RuleService.softDelete instead (soft-deleting a cell rule never touches rateTierId/scheduleConditionId, so there was nothing for that guard to catch) — but that path opens its own transaction independent of this class's row locks, which defeated the locking added to close a revive/price-update race; it now stays inside this.repos.transaction(...) like every other mutating method, holding the same tier lock (see unsetCellPrice's own doc).

Constructors

Constructor

new PricingMatrixService(options: PricingMatrixServiceOptions): PricingMatrixService;

Defined in: server/pricing/pricing-matrix-service.ts:226

Parameters

ParameterType
optionsPricingMatrixServiceOptions

Returns

PricingMatrixService

Methods

addRateTier()

addRateTier(input: AddRateTierInput): Promise<{
  createdAt: Date;
  createdBy: string | null;
  defaultAmountCents: number | null;
  defaultPer: string | null;
  defaultUnit: string | null;
  deletedAt: Date | null;
  deletedBy: string | null;
  description: string | null;
  id: string;
  name: string;
  organizationId: string;
  scopes: JsonValue;
  updatedAt: Date;
  updatedBy: string | null;
}>;

Defined in: server/pricing/pricing-matrix-service.ts:241

Create a new rate tier ("row" of the matrix). Rejects any caller-supplied rate_tier_id scope entry in input.scopes and always injects exactly one canonical entry using the newly-generated tier's own id — this is what makes the exclusivity mechanism work (see the class doc). No Rules rows are created here.

Parameters

ParameterType
inputAddRateTierInput

Returns

Promise<{ createdAt: Date; createdBy: string | null; defaultAmountCents: number | null; defaultPer: string | null; defaultUnit: string | null; deletedAt: Date | null; deletedBy: string | null; description: string | null; id: string; name: string; organizationId: string; scopes: JsonValue; updatedAt: Date; updatedBy: string | null; }>


addSchedule()

addSchedule(input: AddScheduleInput): Promise<ConditionRecordWithVersion>;

Defined in: server/pricing/pricing-matrix-service.ts:413

Validates the schedule config (Zod refinements: non-wrapping time_range, degenerate day_of_week, holidays-requires-date_range, not-fully-wildcard), the start_field/end_field/zone consistency rule against the org's existing active schedules, acquires the namespaced advisory lock, runs the exact overlap check against every existing active schedule for the org+domain, and creates the PRICING_SCHEDULE Conditions row (bypassing ConditionService's guard via direct repository access). No Rules rows are created here.

Parameters

ParameterType
inputAddScheduleInput

Returns

Promise<ConditionRecordWithVersion>


deleteRateTier()

deleteRateTier(rateTierId: string): Promise<{
  createdAt: Date;
  createdBy: string | null;
  defaultAmountCents: number | null;
  defaultPer: string | null;
  defaultUnit: string | null;
  deletedAt: Date | null;
  deletedBy: string | null;
  description: string | null;
  id: string;
  name: string;
  organizationId: string;
  scopes: JsonValue;
  updatedAt: Date;
  updatedBy: string | null;
}>;

Defined in: server/pricing/pricing-matrix-service.ts:382

Soft-deletes the tier and cascades to soft-delete every cell rule under it, clears PricingRateTierRequirements, and nulls capacity settings references to mirror the FK's hard-delete behavior. Without clearing the join rows, ConditionService.softDelete would keep rejecting those eligibility conditions forever — its guard counts join rows by conditionId alone, and updateRateTierRequirements can no longer detach them once lockById returns null for the soft-deleted tier. Row-locks the tier first (same lockById used by setCellPrice/updateRateTierRequirements) so a concurrent setCellPrice for this tier can't commit a brand-new live cell in the window between this method's cell scan and its tiers.softDelete — without the lock, such a cell would be permanently orphaned under a now-soft-deleted tier.

Parameters

ParameterType
rateTierIdstring

Returns

Promise<{ createdAt: Date; createdBy: string | null; defaultAmountCents: number | null; defaultPer: string | null; defaultUnit: string | null; deletedAt: Date | null; deletedBy: string | null; description: string | null; id: string; name: string; organizationId: string; scopes: JsonValue; updatedAt: Date; updatedBy: string | null; }>


deleteSchedule()

deleteSchedule(conditionId: string): Promise<ConditionRecord>;

Defined in: server/pricing/pricing-matrix-service.ts:619

Soft-deletes the schedule and cascades to soft-delete every cell rule referencing it. Row-locks the schedule's Conditions row (via conditionsCrud.lockById) before scanning for live cells — setCellPrice takes the SAME lock before creating/reviving a cell against a schedule, so the two serialize instead of racing: without this lock, a concurrent setCellPrice for ANY tier against this schedule could commit a brand-new live cell in the window between this method's cell scan and the schedule's soft-delete, permanently orphaning that cell under a now-deleted schedule.

Parameters

ParameterType
conditionIdstring

Returns

Promise<ConditionRecord>


setCellPrice()

setCellPrice(input: {
  amountCents: number;
  per?: "day" | "hour" | "night" | null;
  rateTierId: string;
  scheduleConditionId: string;
  unit: "flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person";
}): Promise<RuleRecordWithVersion>;

Defined in: server/pricing/pricing-matrix-service.ts:662

Row-locks the tier, then finds-or-creates-or-revives the cell rule for (rateTierId, scheduleConditionId) — never a naive upsert, since the partial unique index isn't usable as a compound upsert selector:

  • Found + soft-deleted → revive (clear deletedAt/deletedBy), new RuleVersion.
  • Found + live → new RuleVersion (updates the price).
  • Not found → create fresh with the deterministic identifier pricing-matrix-cell-${rateTierId}-${scheduleConditionId}.

Constructs domain_config itself (exactly one BASE_PRICE action, level: "line", calculation_phase: "base") — never accepts one from the caller. condition_logic is an AND node combining the schedule condition with every one of the tier's CURRENT requirement condition ids, read fresh from PricingRateTierRequirements inside this same locked transaction (not passed in stale).

Still constructs the whole domain_config: per (PLA-233 #2) is one more scalar input alongside amountCents/unit, not a caller-supplied config fragment. An OMITTED per preserves the cell's existing axis rather than dropping it — see SetCellPriceInputSchema.per and writeCellPrice.

Parameters

ParameterTypeDescription
input{ amountCents: number; per?: "day" | "hour" | "night" | null; rateTierId: string; scheduleConditionId: string; unit: "flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person"; }-
input.amountCentsnumber-
input.per?"day" | "hour" | "night" | nullThe optional SECOND pricing axis — the tier modal's "Duration" select (PLA-233 #2): unit: "person", per: "hour" is "per attendee per hour". Rides inside the cell rule's existing domain_config JSON, so no PricingRateTiers column and no migration. The per-with-a-count-unit invariant is NOT restated here: this schema deliberately validates only the shape, and PricingDomainConfig.parse in writeCellPrice — the single owner of that rule (BasePriceAction) — rejects e.g. flat + per before anything is written. One source of truth, at the cost of the ZodError naming actions.0.per rather than per. undefined and null mean different things, the same split updateRateTier's defaultPrice uses: - omitted / undefined → leave the cell's existing axis alone. The grid's quick-edit path commits an amount and a unit and knows nothing else about the cell, so treating an omitted per as "no second axis" made an ordinary inline amount tweak silently downgrade "$10/attendee/hour" to "$10/attendee" — a price DECREASE with no error and no audit trail. - null → remove the axis. Explicit, greppable, and the only way to lose one, mirroring how clearing a cell is unsetCellPrice rather than setCellPrice({ amountCents: 0 }).
input.rateTierIdstring-
input.scheduleConditionIdstring-
input.unit"flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person"-

Returns

Promise<RuleRecordWithVersion>


unsetCellPrice()

unsetCellPrice(rateTierId: string, scheduleConditionId: string): Promise<void>;

Defined in: server/pricing/pricing-matrix-service.ts:823

Row-locks the tier, then finds the live cell rule and soft-deletes it.

Parameters

ParameterType
rateTierIdstring
scheduleConditionIdstring

Returns

Promise<void>


updateRateTier()

updateRateTier(rateTierId: string, patch: {
  defaultPrice?:   | {
     amountCents: number;
     per?: "day" | "hour" | "night" | null;
     unit: "flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person";
   }
     | null;
  description?: string | null;
  name?: string;
}): Promise<{
  createdAt: Date;
  createdBy: string | null;
  defaultAmountCents: number | null;
  defaultPer: string | null;
  defaultUnit: string | null;
  deletedAt: Date | null;
  deletedBy: string | null;
  description: string | null;
  id: string;
  name: string;
  organizationId: string;
  scopes: JsonValue;
  updatedAt: Date;
  updatedBy: string | null;
}>;

Defined in: server/pricing/pricing-matrix-service.ts:292

Simple parent-row update (name/description only) — no cascade.

Parameters

ParameterTypeDescription
rateTierIdstring-
patch{ defaultPrice?: | { amountCents: number; per?: "day" | "hour" | "night" | null; unit: "flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person"; } | null; description?: string | null; name?: string; }-
patch.defaultPrice?| { amountCents: number; per?: "day" | "hour" | "night" | null; unit: "flat" | "unit" | "year" | "month" | "day" | "hour" | "night" | "person"; } | nullPLA-141. Omit to leave the default alone; pass null to clear it (which is what the grid's "All open hours" column sends when its cell is emptied — distinct from amountCents: 0, which means free). Setting it changes the effective price for lines that match no schedule column, but deliberately does NOT rewrite existing cells — those hold deliberate per-cell prices and silently overwriting them would destroy work. It only ever seeds cells that don't exist yet, which in practice means a schedule column added after this point.
patch.description?string | null-
patch.name?string-

Returns

Promise<{ createdAt: Date; createdBy: string | null; defaultAmountCents: number | null; defaultPer: string | null; defaultUnit: string | null; deletedAt: Date | null; deletedBy: string | null; description: string | null; id: string; name: string; organizationId: string; scopes: JsonValue; updatedAt: Date; updatedBy: string | null; }>


updateRateTierRequirements()

updateRateTierRequirements(rateTierId: string, newRequirementConditionIds: string[]): Promise<void>;

Defined in: server/pricing/pricing-matrix-service.ts:331

Under a tier lock, replaces its requirements and rebuilds every live cell's condition_logic in one transaction.

Parameters

ParameterType
rateTierIdstring
newRequirementConditionIdsstring[]

Returns

Promise<void>


updateSchedule()

updateSchedule(conditionId: string, input: UpdateScheduleInput): Promise<ConditionRecordWithVersion>;

Defined in: server/pricing/pricing-matrix-service.ts:544

Same validation + advisory lock + overlap check as addSchedule (excluding this schedule itself from the comparison set), then a new ConditionVersion.

Parameters

ParameterType
conditionIdstring
inputUpdateScheduleInput

Returns

Promise<ConditionRecordWithVersion>

On this page