Issue #3626903: A confirmed booking can be deleted outright, and so can a tariff, resource type or tariff class still in use
Closes #3626903.
A booking that still counts is canceled, not deleted (first commit). The bookings list's bulk delete removed a confirmed booking: its order then read empty although it was paid, the place was free again, and the booker heard nothing. An order is already protected (archived or canceled only, admins included); the booking now is too.
BookingDeletionHooks::yoyakuBookingAccess()forbidsdeleteon a held, confirmed, completed or no-show booking, for everyone.- It sits in access, like the order's rule, because the order purge deletes an archived or draft order's bookings from code. And it is a hook rather than a handler method because
yoyaku_managergives the booking another handler; a hook answers beside whichever it has. The class has no dependencies, so it costs nothing to build on each booking access check. - The bulk delete skips such a booking as one the operator may not delete, and its confirmation now says "A booking that still counts is kept: cancel it first."
A record still in use is not deleted (second commit).
- A resource tariff that bookings name, a resource type that resources use and a tariff class that tariffs belong to each refuse in their own
preDelete(). Every way of deleting them is held to it: a form, code, drush, a config import. - One counter serves all three:
ReferencingRecords::countRecordsByEntityType(), the reference-following twin ofTenantRecords. It counts, per content entity type, the rows whose installed entity reference fields target the record, so a field another module adds is counted too. refuseDeletionWhileNamed()throws.- What only describes the record goes with it: a tariff's slot tariffs, and a preset's tariffs and allotments.
- The three delete forms say what uses the record and offer no button. The tenant, slot and resource delete forms now share that refusal (
OwnedRecordFormHooks::refuseDeletion()) instead of three copies, and use the class's existinggetFormEntity(). - The orchestra example's uninstall deletes its slots, with their bookings, before its tariffs, which a booking would otherwise keep.
docs/getting-started.mdstates the rules. French for every new string.
Cost: one COUNT per entity type with a field naming the record, only when a tariff, resource type, tariff class or preset is deleted or its delete form is shown. The booking rule is one state comparison on an entity already loaded, for delete only.
Tests, each checked against its part reverted:
InUseRecordDeletionTest::testCountingBookingIsNotDeleted(): refused to an administrator, skipped by the bulk delete; a canceled one can be deleted.testTariffInUseIsNotDeleted(): refused while a booking names it; an unused one takes its slot tariffs along.testResourceTypeInUseIsNotDeleted(),testTariffClassInUseIsNotDeleted(),testPresetGoesWithItsTariffsAndAllotments().testTheDeleteFormSaysWhatUsesIt(): lists "Bookings: 1", not the slot tariff that goes with it.ExampleUninstallTest::testBookedDemoUninstalls(): the orchestra example uninstalls with a demo slot booked at a demo tariff. It fails with the old purge order. Run on MySQL too; skipped on the Drupal 12 lane like the example's other kernel tests.BookingActionsTestandBookingsTabTestbulk-deleted a held booking to test the delete flow. They now delete a canceled one, andBookingActionsTestalso checks that a held one is refused ("You cannot delete the selected bookings.").PresetVenueBindingTestinstalls the tables a preset's and a tariff's deletions now ask.
AI-Generated: Yes (Claude Code was used to write this change, its tests and this description. I reviewed them before posting.)