Issue #3614361: Stepping a quantity up places one seat at a time, so a party is spread around the venue

Work in progress: the seating layer and the engine's exchange are in, the surfaces are not.

Why both halves of the bug are one bug

The seam was per rate, per click. RequestBooker::holdMany() loops requests and composes each on its own; compose() calls place() per tariff; place() asked for one rate over free places only. So neither a second click nor a second rate could be seated with the first: each ask was blind to the others.

In this branch

  • A seating strategy plugin type in yoyaku_placement: propose(SeatingRequest): SeatingProposal, one strategy per session, chosen per resource and overridable per session, so a gala can seat differently from a Tuesday.
  • The request models a party, several rates and several clicks at once, carrying the visitor's anchors. Each anchor knows where it sits and whether the visitor chose it.
  • Together, the default: extend beside the held places (nothing moves, nothing to announce), else re-place the whole party giving up only places the house handed out (announced), else spread (warned).
  • Sharing a run out across rates is a matching, not a hand-out: a rate prices only the grades it lists, and first-come refuses arrangements that exist, which is the family whose children cannot sit because the adults took the eligible seats. Uniformly priced rates skip it, which is the usual case.
  • holdGroup() can give lines up as part of taking others. Both under one set of locks in one transaction: release after the locks and before the capacity check, so freed places read as free and the order is never counted as holding the party twice. Only the order's own held lines. A failed exchange gives nothing up.
  • Three new base fields: seating_strategy on a resource and a session, place_chosen on a booking.

Performance

The seating read is one linear pass over the session's free places, which is inherent: the best contiguous run cannot be found without knowing what is free. What changes is the multiplier, from once per rate per click to once per hold. Everything else is O(party): the matching stops once every seat is filled, the eligibility filter stops at the party size, and a share-out is rejected on count before any matching. The exchange adds no query, and its releases are bounded by the party.

Still to come

The basket seam through place()/compose()/holdMany(), the place_chosen marker written by the map controller, the SlotBookingForm reconcile rewrite, the "your places have moved" notice, then the seating tests, docs and fr.po.

Merge request reports

Loading
Loading