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.
PlaceSelectionBookeris gone. - The seam can speak.
Placement(lines plus notices) is whatPlacementProviderInterface::place()andBookingComposer::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.
VenuePlacementProviderwarns 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. RequestBookeris how a surface books a quantity (new,yoyaku.request_booker): compose through the seam, then hold as one group, returning aHeldRequest(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'shold()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_alterwhen the venue has something left to pick.yoyaku_calendarnow referencesyoyaku_placementnowhere.
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.