Issue #3612598: Individual vs collective booking mode: one booking per place (core + placement seam)

Phase 1 of the placement generalization: the core booking-mode split.

What it adds

  • A booking_mode (collective | individual) on the resource type (editable on the resource type form) and an override on the resource (empty inherits the type), resolved by BookingModeResolver, defaulting to collective.
  • BookingComposer: turns a request (slot, category, quantity, resource) into hold lines, one line of N for collective, N lines of 1 for individual. Capacity is unchanged either way.
  • A PlacementProviderInterface seam with a core AnonymousPlacementProvider default (individual bookings placed anonymously). The placement submodule (#3612599) will register a provider that assigns a seat or a zone.
  • The calendar booking path (SelectionBooker, used by the calendar and the webform) routes through the composer, so booking N on an individual resource creates N bookings.

Why

Each attendee of a per-attendee event (a conference pass, a concert seat) needs their own booking: one QR, its own check-in state. A conference pass is simply individual plus a capacity, no venue.

Tests

  • Unit: resolver cascade; composer collective vs individual line building.
  • Kernel: an individual resource books one per unit; a collective one keeps a single booking of the quantity.
  • Existing calendar tests still pass (collective is the default; behavior unchanged).

Notes

  • The booking_mode resource field is a new base field: fresh installs and tests get it automatically; an existing site installs the field storage definition by hand (no update hook pre-1.0).
  • Remaining entry points (the API client, the admin place-booking form) can route through the composer in a follow-up; the calendar and webform path is done here.

Phase 1 of the placement generalization. Phase 2 is #3612599 (placement module: rename + seats/zones); #3612600 adds the ticketing opt-in.

Merge request reports

Loading