Issue #3622060: The Tugboat preview installs one submodule of thirty, so a reviewer cannot try a booking, its place map, its workflow, its payment or its emails
The preview now installs 29 of the project's 31 modules — every one but Yoyaku Domain and Yoyaku Booking Fee — and seeds itself from the two example modules, so the whole lifecycle can be tried against the branch under test: pick seats on the Grand Hall's plan, pay with the fake-card gateway, watch the workflow confirm and settle, and read the notification it sent.
What a reviewer gets
Two pages. A front page saying what is installed, with a direct link to each seeded resource's booking page and to every operator screen. And How to test Yoyaku, twelve numbered things to do, in steps, each ending in what it proves: book and pay, have an operator validate one, choose seats on the plan, see a fare offered only where its categories allow, put two bookings in one order, book something free, decline a payment on purpose, cancel your own booking from the email link, read the ticket, close half the hall, attach a rule and watch it refuse a booking, and fire a parked timer so the branches that wait for the booking date run while you watch.
Every path either page links was resolved against the routing tables, and every claim about what happens was read out of the code rather than assumed. Three that were wrong when first written, and are not now: the fare restriction shows as an absence in a seat's own fare menu rather than as a refusal (venue-map.js offers only the tariffs a seat's grade names); the "places apart" policy asks the booker to accept scattered seats and refuses nothing, ever; and a parked timer is not a work item, so it is not at /orchestra/tasks but on the instance, which is also where Fire timer now is.
What changed
.tugboat/prepare.sh (new) installs the bcmath extension Kessai needs and asks Composer for Orchestra, Kessai, Easy Email, Webform, Views Bulk Operations, dompdf and endroid/qr-code. All of them are require-dev, so nothing put them in a built site. It runs from both update and build, because an issue-fork preview is built from the base preview's filesystem with build alone: a branch needing a package the base does not have would otherwise install silently without it.
.tugboat/setup.php replaces the hand-rolled court-and-conference seed. The examples seed themselves, so the script is left with what a site answers rather than a module: the module set, a notification channel, where mail goes, who may book, the seated concert's workflow and price, and the two pages.
A new tugboat CI job lints it, because a dot-directory is invisible to both linters: phpcs walks the project and leaves that file out, and phpstan skips such a directory too, so the one PHP file that runs on every preview build was the only file with no checks on it. It had earned them — a self:: in a script with no class, and a call to node_add_body_field(), removed in Drupal 12. Moving it into the shipped tree is not the answer and CI said so: it names Node and Easy Email classes, which the engine must never depend on and ModuleBoundariesTest rightly refuses. So it stays tooling and is linted by name, not advisory, since a build script that does not parse takes every preview with it.
Mail: the backend is Drupal's collector, so nothing is delivered — the preview URL is public and the booking form asks a visitor for an address. Every notification is readable at /admin/reports/email instead. The ticket is the exception: Orchestra attaches it to the message and deliberately keeps attachments off the saved entity, so the preview puts [yoyaku-order:ticket-url] in the confirmation template, or the whole ticket feature would be unreachable here.
The example modules
yoyaku_orchestra_example no longer depends on orchestra_mail. Both delivery channels answer the same OrchestraNotificationEvent at the same priority, so both installed delivers every notification twice, which orchestra_easy_email reports as a misconfiguration on the status report; and a dependency on either makes the other impossible to uninstall. The dependency's own comment said enabling orchestra_easy_email "instead" needed no code change, which the line below it forbade. A new hook_runtime_requirements reports it while neither channel is installed, so a demo whose messages go nowhere stops looking like a working one.
The demo meeting room no longer books by the day over a contiguous date range. Every slot the example seeds is an hour long, so "book by the day" presented a room as a hall let out for whole days — and it collapses a day's slots into one entry the booker cannot choose between, which is the symptom reported on #3622043. It is now a single timed slot, exclusive through capacity 1, which is what whole-room hire meant. The three resources that do sell days keep the date-only calendar and now get day-long slots, stepped by date arithmetic: adding 86400 across a DST change starts a day-long slot at 23:00 the evening before, and a date-only calendar would name the wrong day for it.
yoyaku_placement_example seeds four performances a week apart rather than one at 20:00 on the day of install. A calendar offers only slots that have not started, so the single performance was in the past by the same evening and the hall it exists to demonstrate had nothing left to book.
A defect that made the notification channel uninstallable
easy_email.easy_email_type.orchestra_booking_cancelled.yml declared id: orchestra_booking_canceled, one letter apart. ConfigInstaller::createConfiguration() takes the object's name from the file and its data from inside; when they disagree it saves the entity under the name its id implies and then throws a LogicException. Nothing catches it, and installOptionalConfig() runs inline in installDefaultConfig(), so enabling Orchestra Easy Email on any site that already had Yoyaku Orchestra Order failed part-way through, having already written config — which is exactly what this preview needed to do.
ShippedConfigIdentityTest now checks the file name of every shipped config entity against the id inside it, across all 30 of them, so the class of defect cannot come back in any module. It runs without process isolation, unlike most of this project's unit tests: it reads files and decodes YAML, and isolating a case per config file cost 2m47s to answer a question that takes 0.03s.
Not in the preview
Yoyaku Domain resolves the tenant and the booking channel from the domain being served, and a preview serves one hostname. Yoyaku Booking Fee hangs its fees on a booking channel, and the only resolver that can put a visitor on one ships in Yoyaku Domain — so fees are unreachable on any single-domain site, which BookingChannelContext says is deliberate. Yoyaku API is installed, being the contract rather than the transport, but it has no routes of its own.
Checks
phpcs (Drupal,DrupalPractice, CI's extension list, whole module) and phpstan level 5 clean; cspell clean against the gitlab_templates config rather than core's, which is what caught a British neighbours in the walkthrough; scripts/check-translations.php reports every string translated, French included. PlacementExampleTest (854 assertions), NotificationChannelRequirementHooksTest, ShippedConfigIdentityTest (30 cases), MinimumAgeTest, RejectionPathShapeTest and PaymentLapsePathShapeTest pass locally.
Pipeline 955802 is green, D12 lanes included: composer (next major), phpunit (next major) (both splits) and phpstan (next major) all pass, and so does the new tugboat job. The two test-only changes jobs fail, which is what proves the new tests pin their subjects: without the hook the requirement reads NULL, and without the seeding change the hall has one performance where the test wants four. The config rename is the one thing that job cannot demonstrate, a rename being a delete plus an add that its revert does not undo, so it was verified by hand instead: a copy under the old name fails the check with the name and the id both quoted.
One note for whoever looks at the preview: the base preview needs rebuilding once, because update does not run for issue-fork previews and the base's Composer lock has none of the new packages. prepare.sh also asks for what is missing from build for that reason, so a child preview repairs itself at the cost of one slow build.
AI-Generated: Yes (Claude Code was used to help write this change and its tests. I reviewed the work and ran the linters and the impacted tests before posting it.)