Issue #3614349: Cap what one transaction may take of a tariff, and move the policy system to the engine
Closes #3614349
cart_limit on a tier, seeded from default_cart_limit on the tariff exactly as quota is seeded from default_quota. Empty means no limit, so nothing changes for a site that sets none.
Two departures from the issue, both deliberate:
- Enforced in
BookingManager::holdGroup(), not inRequestBooker. The seat map callsholdGroup()directly (PlaceSelectionController, three sites) precisely because it already knows the place, so enforcing in the booker would have left the seated venue uncapped, which is the case the issue is most about.holdGroup()already receives the order being appended to, so it knows the cart without the engine depending on the cart service. - Checked once per group, never per line. On a placed venue the composer turns one quantity into one line per seat, so ten single-place lines are one request for ten; a per-line check passes every one of them against a limit of four. There is a test for exactly that.
CartLimitExceededException is separate from CapacityExceededException and carries the limit, what the order holds and what it asked for: "there are none left" and "there are plenty, but not for you" are different answers, and a surface that confuses them sends a visitor away from seats they could still have.
Not re-checked under the slot lock, unlike capacity: the only way to race it is to send two of your own orders at once, and the worst that wins is a few places over in one order rather than an oversold slot.
Left open, as the issue lists them: per customer over time (needs an identity), and whether an operator may exceed it. Neither is assumed here.