Issue #3614532: Constraint policies should declare what they need rather than when they run, and stop assuming a tariff
A policy declared a phase, out of two, and carried one method per phase on two different context classes. Two phases cannot express the rules this engine wants: a minimum quantity needs an order nobody is still adding to, a maximum spend needs resolved prices, a per-booker rule needs a named booker. Each is a fact that either holds where the policy is asked or does not.
What changed
- A policy declares the facts it needs (
requires) and is asked wherever they hold, through a singlecheck().ConstraintPhase,HoldContextandcheckHold()are gone. - The fact vocabulary is open: a fact is a string plus a service tagged
yoyaku.policy_fact, keyed by the fact it answers. The engine names only the three it can answer itself;yoyaku_orderownsbooker,yoyaku_paymentownsprices, and each publishes its own constant beside its own resolver. - A fact nothing resolves is refused and logged rather than skipped, which is the direction #3614604 settled for a missing plugin.
- The manager hands each attachment the lines its reach covers. Three policies were deriving reach three different ways, and all three were wrong about a session sold without rates.
- An evaluation reports what it could not ask as well as what refused. The confirm form and the Orchestra review step list those; the last checkpoint logs them. It never blocks.
- The three limits share their settings through
QuantityLimitTrait, including the unit the number is measured in.
Two defects this fixes
- A limit attached to a resource selling no rates did nothing at all, silently, while still listed on its Policies tab: the policy walked the rates of the basket and an untiered line had none to walk. The quantity control for such a session was also built without a ceiling.
- Because that walk applied the number once per rate, a limit of four on a hall selling three rates permitted twelve. A limit attached to a resource now caps the sum across what it reaches.
Behavior change to be aware of
A max already stored on a resource or a resource type now means that many in total rather than that many per rate. Strictly tighter, never looser, so it cannot cause an oversell.
No plugin id changes, so no stored attachment needs rewriting. Each limit keeps its previous measure as its default unit, so no attachment changes what it counts either.
Tests
The three new limit tests were run against the unfixed code first and seen to fail: the two limits silently permitted the booking, and ceilingFor() could not be asked about an untiered session at all. Two shipped tests caught real problems while this was being built, a stale evaluation defeating the operator waiver and a refusal that had stopped naming the rate it capped.
Whole yoyaku kernel suite green locally (114/114 classes). phpcs clean including warnings, phpstan reports nothing in the changed files, cspell clean.