Issue #3612888: Re-home group placement as a category-based auto-assign on the slot booking page

The problem

PlaceAssignment::proposeForCategory() already prefers a run of consecutive positions in one row, but the slot booking page reached it through PlaceSelectionBooker, a second path beside the yoyaku.placement_provider seam that #3613506 built for exactly this. The two had drifted:

  • A tiered booking on a venue whose sections are pooled (#3613507) failed from the slot page with "Only 0 of N places are available": availablePlaces() excludes pooled areas and that path never asked the pools, though the picker sells the same pool.
  • SlotBookingForm::isPlaced() was a raw place count that ignored section modes, so it routed to that path and offered a map link for a venue selling no seats.
  • It skipped validateResourceRules() (single-unit, uniform quantity), which the composer path enforces.

And the contiguous flag the proposal returns was dropped by every caller, so a party given scattered seats was never told.

Auditing for other paths that never meet the seam turned up the same bug class on two more surfaces, both proven with a kernel probe: yoyaku_api and the operator booking form in yoyaku_ui called BookingManager::hold() / book() with a quantity, so a request for 3 on a placed venue became one place-less booking of 3. It took the tier's capacity while leaving all three seats free to be sold again, so a hall could be sold past its size through the API alone.

The change

  • One home for group placement. The slot page asks for a quantity and nothing else; the units are composed through the placement seam and held as one group. PlaceSelectionBooker is gone.
  • The seam can speak. Placement (lines plus notices) is what PlacementProviderInterface::place() and BookingComposer::compose() return. The party is only knowable at the request, since a held order no longer says which of its lines were one party, so the notice travels with the lines.
  • A scattered party is placed and warned, not refused. VenuePlacementProvider warns once for the whole party when no run held it: one unit is never warned about, nor a party taken wholly out of one pool (an area is one place to stand), but a party split between the seats and an area, or across two areas, is.
  • Placement outranks the booking mode. A provider that puts each unit somewhere of its own says so through requiresIndividual(), so a venue-placed resource left on the collective default no longer produces a single place-less line that no place bound protects.
  • RequestBooker is how a surface books a quantity (new, yoyaku.request_booker): compose through the seam, then hold as one group, returning a HeldRequest (the order plus the notices). The calendar, the API client, the operator form and the demo seeding all go through it, so a quantity means the same thing wherever it is asked for. The engine's hold() stays the primitive, for a caller that already knows where its unit goes (the seat map, which names the place the booker clicked).
  • The picker link moved to placement, added by form_yoyaku_calendar_slot_booking_alter when the venue has something left to pick. yoyaku_calendar now references yoyaku_placement nowhere.

API contract change (pre-1.0)

BookingClientInterface::hold() and book() now answer with the order {id, reference, state, tenant_id, notices, bookings} rather than a single booking, because a quantity may reserve several bookings; holdGroup() gains the same notices key. Ids to confirm() or release() come from bookings.

Removed

PlaceSelectionBooker, its service and its test; SlotBookingForm::isPlaced() and placePickUrl(); SelectionBooker's own compose-then-hold (it delegates to RequestBooker) and its now-unused booking-manager dependency; three stale fr.po msgids plus the one left behind by #3612887.

Tests

New SlotBookingPlacementTest (party seated together, scattered party warned on the page, pooled venue books from the page, picker link present and absent) and ApiPlacementTest (hold, book and holdGroup seat their units, the seats stop being free, a shortfall is refused, a scattered party is reported), plus group and collective-venue cases in VenuePlacementProviderTest, pool notice cases in PooledSectionProviderTest, and an individual-mode case in LocalBookingClientTest.

Whole yoyaku kernel suite green (95 classes), unit tests green, placement Functional green, phpcs and cspell clean.

Docs and the French translation are updated: placement.md, capacity-and-holds.md, and api.md, whose BookingManager signatures had drifted (they still showed hold(BookingCategoryInterface $category, …) with a $subject parameter that does not exist, and a worked example putting the category on the slot). It now documents the real signatures, holdGroup(), RequestBooker, and the accessors each entity really has.

Edited by Frank Mably

Merge request reports

Loading