Skip to main content
The entitlement engine is the most complex subsystem in MUZE. It determines which uniform products an employee is entitled to, in what quantities, and under what payment terms, based on configurable rule sets and criteria matching. File: backend/src/entitlements/entitlement-engine.service.ts The engine answers one question for every order: what is this employee entitled to right now, and how much of it remains?

Key components

The two pure functions have no Prisma or NestJS dependencies. This makes the most intricate business logic independently testable and lets the reporting service reuse it.

Evaluation flow

Rule sets and rule items

A rule set links a uniform category to a list of allocation items: Each rule item (EntitlementRuleItem) allocates one product:

Criteria matching

Each rule set has zero or more criteria rows. Each row specifies an attribute (department, store, employmentType, jobTitle), an operator, and a list of values.

Evaluation rules

  • AND across rows. Every row must pass for the employee to match.
  • OR within a row. The employee’s value matches if it equals any value in the row.
  • Null handling. If an employee attribute is null, any row referencing it fails. Employees cannot match on attributes they do not have.
  • Case-insensitivity. All comparisons are case-insensitive.

Operator semantics

Rule sets with no criteria rows act as a catch-all for the category. Runtime matching deliberately queries only rule sets that have at least one criterion (active: true, criteria: { some: {} }), so legacy catch-all rules are excluded from live evaluation.

Specificity scoring

When several rule sets match, the most specifically targeted one wins: a rule set matching on department plus store outranks one matching on department alone. RuleSetsService.findActiveForEmployee performs this scoring.

Dual implementation

Criteria evaluation exists twice with identical semantics:
  1. In-memory (evaluateRuleCriteria()) for single-employee eligibility checks
  2. SQL (buildCriteriaSql()) for bulk operations such as reports and previews
The SQL translation generates parameterized Postgres expressions:
SQL safety rules: all values are passed as parameters (nothing is interpolated), CONTAINS uses LIKE with escaped wildcards, unknown operators return FALSE (fail closed), and empty IN lists return FALSE while empty NOT_IN lists return TRUE.

Phases and balance

computeEntitlementPhase() classifies the employee as INITIAL (before the first replacement boundary) or REPLACEMENT (after it). The phase is anchored to the store’s rollout date where one exists, otherwise to the employee’s start date. See Replacement system for the exact calculation. For the matched rule set, the engine finds or creates the employee’s EntitlementPeriod for the current cycle and computes each product’s balance:
In free-flow mode (enforcement off) the allowed quantity is the merged maximum across all rule items that allocate the same product, because the phase split is ignored. See Enforcement mode. Consumption is written onto each OrderItem (entitlementPeriodId + entitlementQuantityConsumed) inside the order-creation transaction, so concurrent orders cannot over-allocate. Cancelling an order releases the consumed quantity.

Payment split and enforcement

During order preview the engine classifies every line item:
  • Company-paid when it fits within the remaining allocation for an allocation-eligible product
  • Employee-paid when it exceeds the allocation
When a client has entitlementEnforcementEnabled turned on, company-paid quantities beyond the remaining balance are rejected outright instead of becoming employee-paid. The default is off.

Enforcement mode: free flow vs strict

The client-level entitlementEnforcementEnabled switch on the Client record controls two behaviours: how the engine treats the INITIAL / REPLACEMENT phase split, and how order overflow is classified. In free flow the engine still computes the current phase, cycle boundaries, and replacement dates so that phase-aligned reporting stays accurate; only the product eligibility filter is phase-agnostic. Consumption is recorded against the order’s entitlement period in both modes and the current-cycle consumed quantity is deducted from the merged balance. Criteria matching still applies unchanged, so an employee who does not match any rule set remains NOT_ELIGIBLE. Worked example — a rule item for the Boxer product BOX023 is allocated as INITIAL only:
  • Under strict mode a replacement-phase employee does not see the item at all.
  • Under free flow the same employee sees it and can order it at the rule-set quantity, because the phase split no longer hides it.

Boxer policy semantics

The engine implements Boxer’s entitlement policy:
  • One replacement cycle per rule set (for example 24 months)
  • Rollouts are scheduled store by store: a store is anchored to a rollout date
  • Replacement boundaries fall every cycleMonths after the store anchor
  • Employees are not phased on their own anniversary; the store anchor drives the cycle
  • Stores without an anchor fall back to per-employee start dates

CSV import

Rule sets and their criteria can be bulk-imported. The CSV format and validation rules are documented in Import system.