Add allotments, named slices of a session's capacity that given tariffs draw from
Sets aside part of a session's capacity under a name, so that only the tariffs drawing on that name may take it. Places held under an allotment leave the general count the moment they are declared, with no booking behind them.
This is MR1 of two: the counted allotment. MR2 will pin named places to an allotment in yoyaku_placement, where the venue and the map already live.
Why core rather than a submodule
An allotment is a capacity concept, and capacity is what BookingManager owns: capacity, quota, limitFor() and assertFits() all live there. The yoyaku.availability_bound seam exists for what core genuinely cannot see, a hall's physical places; an allotment is a number core owns outright, so reaching for the seam would simulate a core concept from outside. It also removes work: no bound provider, no constraint plugin, no install hook putting a field into somebody else's entity.
The model
yoyaku_allotment, tenant level, is the name itself and carries no number. How many places are held changes every season; the name outlives it and is what a ticket and a report keep meaning.yoyaku_resource_allotmentsays how many places every session of a resource holds, and when it lets go.yoyaku_slot_allotmentsays how one session departs from that.
Resolution is most specific first, and the two sides of a record answer separately, so a matinee can hold more places and keep the season's deadline. AllotmentSettings is the only home of that rule.
Counting
general = capacity − everything taken − Σ over allotments still holding of max(0, places held − taken from it)That last term is what actually sets places aside: an allotment nobody has taken from still withholds its places. A line drawing on an allotment is bounded by min(its quota, the allotment's remainder, the room); a line drawing on none is bounded by the room minus what the allotments hold.
Nothing is booked to achieve this. Placeholder bookings would reach the same number, poison every transaction report, and turn a deletion into a migration.
Release
An allotment nobody fills is places nobody takes. release_at on the session and release_before as an offset on the resource; once past, the allotment withholds nothing and its tariff falls back to the general capacity rather than refusing. Nothing is written when the deadline passes, because the term was always derived.
Performance
A site holding nothing under an allotment pays no query at all: whether the feature is used is answered from a cache item tagged with the allotment list, and every session then resolves to "holds nothing" without a read. Once allotments exist, a page of sessions costs a fixed number of reads rather than one per session. AllotmentQueryBudgetTest counts the queries rather than asserting this in prose.
Also here
- The reference on all three tariff scopes, including the preset cell, and the applier moves it on re-apply as it moves the price.
- The booking is stamped with what it took, resolved by the same rule that bounds it, so a line is never counted against one allotment and recorded against another. An operator may name a different one at the counter.
- Admin screens: the tenant's allotments, an Allotments tab on the resource, and one on the session showing the split with a standing over-allocation warning.
- The allotment name on the ticket, on screen and in both mails.
- Deleting an allotment that bookings came out of is refused, on the screen and from a script.
docs/allotments.md, and the French.
Known gap, inherited on purpose
An empty allotment on a slot tariff means inherit, so one session cannot send a tariff back to the general capacity while its resource tariff points at one. quota has the identical gap.