Problem/Motivation

Everything a resource offers, it offers everywhere. A site that runs a mini-site for a special event, on its own domain, cannot say that a tariff or a resource belongs to that mini-site and must not appear on the main site, and there is no way to offer a rate on one front without offering it on all of them.

The axis wanted here is the booking channel: the front a booking is made through. A mini-site on its own domain is one, a partner integration through yoyaku_api is another, and a box office would be a third. Yoyaku has no box office today, the only operator booking entry being the test form at modules/yoyaku_ui/src/Form/PlaceBookingForm.php, so nothing here models one. The point is that it can become a channel later without the seam changing.

Proposed resolution

A booking channel is a config entity, yoyaku_booking_channel, scoped to exactly one tenant. Mini-sites are per tenant, so unlike a resource type a channel is never shared across tenants.

Which channel is being served is answered by a priority ordered chain of services tagged yoyaku.booking_channel_resolver, the first non-empty answer winning, shaped like the tenant chain in src/TenantContext.php. It answers a channel or nothing, and nothing is the honest answer for the main site, for an administrative page, and for drush and cron. There is deliberately no default channel, unlike the tenant with its default: an entity naming no channel is on offer everywhere, including there, so a site running no mini-site never meets the feature at all. A site that later wants a main-site-only tariff creates a channel for the main site and binds its domain to it, which is also how a box office would join.

yoyaku_domain gains the binding the mini-sites need: a booking channel names the domains it is served on, as a third-party setting edited on a Domains tab, the same shape as the tenant binding already there. A domain no channel claims defers, because Domain falls back to the default domain record for any hostname it cannot match, so an unmatched host would otherwise resolve whichever channel names that record. The form refuses a domain whose tenant binding names another tenant, so the two answers cannot disagree.

Restriction is one shared field, stamped on yoyaku_resource and on yoyaku_tariff the way the tenant field is stamped today: naming no channel means on offer everywhere, naming one or more means on offer only there. A slot inherits from its resource, and the tier can gain the field later without anything moving.

The filter is one service applied at the read seams, rather than rewritten at each of them: BookingManager::tariffsByResource(), which feeds the availability map and therefore the booking form, the calendar, the venue map and the API in one move, then the listing entry points in yoyaku_calendar, yoyaku_node and yoyaku_placement. Views builds its own query, so yoyaku_views gets a filter plugin of its own. Engine reads pass accessCheck(FALSE) throughout, so none of this can ride on a query access tag.

Hiding is not enforcing, so RequestBooker, the one booking path, refuses a line naming a resource or a tariff the channel does not offer.

The channel is recorded on the transaction when it is opened, and read from there afterwards. The ambient request only seeds it: a basket outlives the request, a booker can arrive on the mini-site and come back on the main site, and mail and the back office render in a different context entirely. The field ships nullable now, so the day a box office exists its bookings are already told apart in reporting.

Offers become channel dependent, so a cache context ships with this.

One thing deliberately not done: the booking channel does not belong in YoyakuAccessControlHandler. The tenant is a hard boundary there for everybody, administrators included. A channel restricts what is on offer, and putting it in the access handler would blind the back office to the mini-site's own resources.

Remaining tasks

  • Kernel coverage for the resolution chain, for the domain binding and for the restriction, plus functional coverage of the admin screens.
  • A documentation page, and the module maps that list what each submodule does.
  • French translations in the same commit.
  • The admin screens accessible from the first commit.

User interface changes

A Booking channels collection under the booking administration, a Domains tab per channel when yoyaku_domain is installed, and a Booking channels element on the resource form and on the tariff form.

API changes

New: the yoyaku.booking_channel_resolver service tag, and the service answering the channel being served.

Data model changes

A new yoyaku_booking_channel config entity, a booking channels base field on yoyaku_resource and on yoyaku_tariff, and a booking channel base field on the transaction. Pre 1.0, so no update hook: reinstall is the upgrade path.

Issue fork yoyaku-3614926

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

Title: A sales channel decides which resources and tariffs are on offer » A booking channel decides which resources and tariffs are on offer
Issue summary: View changes

The noun is now booking channel, not sales channel. Half of what yoyaku books is free, so a name built on selling reads wrong in the engine core, and the word is now absent from it entirely.

Rejected along the way: bare channel, because the docs already say channel for the notification delivery channels and the log channel; surface, because a surface is defined in the use-cases page as what produces selection lines, and two mini-sites on two domains are the same surfaces; outlet, as retail-flavored as sales. French is canal de reservation.

  • mably committed fed9dcaf on 1.x
    feat: #3614926 A sales channel decides which resources and tariffs are...
mably’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.