A partially used order can be canceled and purged, lock counters use MySQL-only SQL, and four more defects in the engine and its screens

Closes #3631977.

Lock anchor counters used MySQL-only SQL, and so did filling a slot's seats. LockAnchors::addTo() moved a counter with GREATEST(0, CAST(taken AS SIGNED) + :delta), and SIGNED is a MySQL type name: on PostgreSQL every counter move failed, while the README and three doc pages say PostgreSQL should work. Running this merge request's pipeline on PostgreSQL also showed SlotSeats filling seat rows with an INSERT … SELECT whose state was a CASE over bound values, which PostgreSQL types as text and refuses for an integer column.

  • The counter is moved by an expression chosen by the sign of the move: taken + :delta to add, CASE WHEN taken < :give THEN 0 ELSE taken - :give END to give back. No cast, still floored at nought.
  • The seat fill writes its integers into the SQL (the slot id and the enum's values, all integers) instead of binding them.
  • The test suite then passed on PostgreSQL 16, kernel and functional alike, in pipelines run on purpose for it (1009949, and 1010438 after the rebase); the commit that switched the pipeline to PostgreSQL is not part of this merge request. The next-major lanes cannot run there: Drupal 12 requires PostgreSQL 18. Tests that count queries skip PostgreSQL, whose driver adds statements of its own, as core's performance tests do: they ask ComparableQueryCountsTrait rather than each checking the database type; a deadlock assertion accepts PostgreSQL's code 40P01, which Contention already treats as one; a test fixture no longer names an alias in a HAVING. The docs now say the suite passes on PostgreSQL, while CI runs on MySQL.

A partially used order could be canceled, and so voided. An order whose bookings settled to a mix of used and no-show is partial, and cancelTransaction() refused only completed, unused and canceled orders. Canceling a partial order made it a void one: deletable at once, purged after the canceled retention, its used bookings deleted with it. It could not be archived either, and Confirm was offered on it with nothing to confirm.

  • BookingTransactionInterface::SETTLED_STATES (canceled, completed, unused, partial) is the one list of finished orders; OrderInterface inherits it instead of keeping its own. cancelTransaction() refuses a settled order, before and under the row lock. Confirm is offered only on a pending order. STATE_PARTIAL's description said "some bookings confirmed, others not", which an order with a confirmed booking never is.

Deleting slots with their bookings was a switch left on the shared booking manager. setSlotDeleteCascade(TRUE) made every later slot delete in the request take its bookings along, confirmed ones included, until a finally switched it off. It is replaced by one call, BookingManagerInterface::deleteSlotsWithBookings(), which deletes the slots' bookings and slot tariffs through the entity API and then the slots. BookingSlot::preDelete() only refuses now. The two example modules' uninstalls and docs/api.md use it.

Every entity form's references to another module's weighted type were reordered. WeightedOptionsHooks::formAlter() runs on every entity form of the site and put any reference select whose target type has a weight in weight order, loading every record it offers: an article's taxonomy select was reordered. It now orders only the types yoyaku and its submodules provide.

Two admin screens showed the engine's own English message. BookingOperationController and PlaceBookingForm printed $e->getMessage(), the sentence meant for the log, and the controller named the operation by its machine name. Both give the refusal through BookerRefusal::getRefusalMessage(), as every other booking surface does, and the controller says "Booking 12 confirmed." in words.

The cart's Remove buttons all read "Remove". Each ticket row and event card had a button whose only name was "Remove", so a screen reader user heard it repeated with nothing saying what it removes. Each now carries an accessible name starting with the word shown: "Remove Adult, Row A, seat 3", or the quantity for a ticket that names nothing of its own, and "Remove all tickets for Gala, Saturday 20:00" for an event.

Cost. None of these adds work to a path that ran before: the counter expression is one statement as before, the seat fill the same insert, the weighted options hook now stops earlier on other modules' types, and the rest are messages and attributes.

Tests (each fails without its part of the fix):

  • AnchorCounterTest::testCounterMovesAndIsFlooredAtNought: a counter moves up and down and is floored at nought; it fails if the give-back is not floored. The PostgreSQL pipeline above is the evidence for portability.
  • OrderTest::testPartialOrder (a partial order is settled, canceling it changes nothing and keeps its used booking) and OrderOperationLinksTest (no Cancel and no Confirm on a partial order).
  • BookingManagerTest::testSlotsAreDeletedWithTheirBookingsOnlyWhenAsked: a booked slot is refused, deleted with its bookings when asked, and the next booked slot is refused again.
  • WeightedOptionsScopeTest: a taxonomy select keeps its order, a yoyaku select is weighted.
  • PlaceBookingFormTest::testRefusalsAndOperationsAreReportedInWords: both screens give the refusal in words, not the engine's message.
  • CartRemoveButtonNamesTest: each button is named after what it removes.

AI-Generated: Yes (Claude Code was used to write the fix, its tests and this description. I reviewed them, and each new test was confirmed to fail without its part of the fix and to pass with it.)

Edited by Frank Mably

Merge request reports

Loading
Loading