Issue #3616344: Let a venue apply the orphan rule to the automatic placement only, not to a place a booker chose

The rule from [#3616316] was all or nothing: attached, the seating avoids leaving a place available on its own and the hold refuses one; absent or switched off, neither happens. A venue can want the first without the second, letting the engine seat parties tidily while a booker clicking a seat map takes whatever seat they like.

One setting, off by default, so an existing attachment behaves exactly as it does now: Let a booker choose a place that leaves one on its own. With it on, check() drops any line whose place was chosen rather than assigned, and still judges the lines the engine placed. That asymmetry is the point: a proposal that strands a place is a fault of the seating rather than a choice anybody made, and the strategy is handed the same rule precisely so it does not make one. Nothing about the rungs changes either way.

No new field. VenuePlacementProvider stamps place_chosen FALSE on the lines it places, PlaceSelectionController stamps it TRUE on a place clicked on the map, and a proposed policy line carries those values since #3616316. Dropping the chosen lines in placesBySlot() rather than later also means a request made only of them is answered without reading the session's places at all, so the one extra query per hold disappears in that mode.

Why it stays in yoyaku_placement rather than being offered by every policy, which was considered and rejected: it only means something for a rule about where a booking sits. On transaction_quantity_limit or either per-booker limit it would let a booker exceed the cap by clicking the map instead of using the stepper, which is what those policies exist to prevent, and place_chosen is a placement value that yoyaku core has no business reading. When a second placement rule wants the same setting it becomes a trait here, the way LimitSettingTrait was extracted once three policies shared a number.

What it cannot express: place_chosen says the place was named rather than assigned, not who named it. An operator at the counter looks like a booker on the map. "Operators may, bookers may not" needs a permission or the booking channel and is a larger design.

Tests

OrphanedPlacesTest goes from 4 to 7:

  • a chosen place that would strand another is taken when the venue allows it;
  • an engine-placed line in the same configuration is still refused;
  • allowing choices reads nothing of its own, asserted as a budget against a hall with no attachment rather than as an exact count, since two environments count a hold differently.

Green locally: OrphanedPlacesTest, PlaceOrphansTest, SeatWithoutOrphansTest, ShippedPolicySettingsSchemaTest, PlaceBookingTest, SlotBookingPlacementTest. phpcs and phpstan clean, run after the last edit.

Docs: the policy's section in docs/constraint-policies.md and the rule's section in docs/seating.md. French translation included.

AI-Generated: Yes (Claude Code was used to draft this description, write the code and its tests, and run them. I reviewed the result.)

Merge request reports

Loading
Loading