12 min readAug 10, 2026by jakub

Changelog

Current version: 1.0.0

All notable changes to this project are documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

1.0.0 - 2026-06-02

Hardened (release-candidate review rounds, rc2-rc5)

Four external review rounds on the release candidate; every confirmed finding fixed:

  • Failure lifecycle: the booking recorded before order persistence is released when placement fails - conditionally: bookings of committed orders (a submit_success observer or post-placement quote save throwing, duplicate concurrent submissions reusing the reserved increment id) survive. An hourly reconciliation cron removes aged booking rows no order accounts for (killed worker / fatal paths that skip the failure event), batched over separate connections so it also works on split-database installations.
  • Store-safe booking identity: store_id on the ledger with a composite (order_increment_id, store_id) unique key (increment ids are only unique per store); the upgrade backfill only resolves increment ids with exactly one known-store order and leaves ambiguous rows NULL with a warning.
  • Concurrency: the slot lock keys on method+date, so the daily-cap check-then-write is atomic across different slots of the same day; verified live with two-session same-slot and different-slot placement races.
  • Transactionality: reschedule and cancel run release + guards + record + order save in one DB transaction; same-day moves are judged on their net effect (the moved order's own booking is excluded).
  • Staleness: order placement re-validates schedule rules (past / newly-blacked-out / removed slots); reservations made for one delivery method can no longer attach to another (guarded in checkout persistence and order fallback); disabled slot booking discards stale quote slot data.
  • Store scoping: reminders, change-policy reads and cut-off calculations all resolve config and "today"/slot-start in the ORDER store's scope and timezone; Google distance lookup failures fail closed for max-distance methods.
  • Hyvä parity: the checkout picker follows the selected shipping method, honours per-date capacity overrides, the daily cap and the remaining-capacity toggle, and hides for non-Qoliber carriers; unit-tested against the real magewirephp/magewire Component (RC-labelled pending live verification - see docs/HYVA_VERIFICATION.md).
  • Packaging: scripts/package-release.sh builds the three modules as independent archives and fails if the base module is not self-contained; setup:di:compile verified both base-only and combined-with-Magewire.

Added

  • Initial Qoliber release, rebranded from the legacy private extension to Qoliber_DeliverySlots (composer package qoliber/delivery-slots).
  • Storefront delivery slot booking from a header icon, with a 30-minute sliding reservation hold that is prolonged on every customer action (Qoliber\DeliverySlots\Model\SlotReservation).
  • "Qoliber Delivery Slots" shipping method that appears as a selectable shipping option at checkout, with an inline slot picker rendered in the checkout shipping step (checkout-delivery-slots UI component) that books the chosen date and time when the method is selected.
  • Shipping carrier (qoliber_delivery_slots) with a flat method price by default and optional Google Distance Matrix per-kilometre pricing when an API key is configured. Product weight / length / volume limits are enforced only when both the method limit and the product attribute are set, so the method is available out of the box.
  • Admin-managed delivery methods with semicolon-separated time slots (Delivery Slots menu).
  • Per-slot capacity: each delivery method defines how many deliveries it can fulfil per time slot per day (capacity_per_slot, 0 = unlimited). Confirmed orders consume capacity; full slots are shown as "Fully booked" and cannot be selected or reserved, preventing overbooking conflicts.
  • Enable/disable slot booking (carriers/qoliber_delivery_slots/enable_slot_booking): when off, the carrier still works as a plain shipping method but the calendar, header icon and checkout slot picker are hidden.
  • Per-method availability rules (store-timezone aware): minimum lead time (days), same-day cut-off time (push one lead day after it), maximum days-ahead booking window, weekly days off (multiselect), and blackout dates. The calendar greys out any date that fails a rule.
  • Fully Qoliber-branded identifiers throughout: carrier code qoliber_delivery_slots, tables qoliber_delivery_method / qoliber_delivery_slot_booking, order/quote columns qoliber_delivery_date / qoliber_delivery_time, REST GET /V1/qoliber-delivery-slots/slots (no legacy names remain).
  • Slot persistence onto the quote at checkout (SaveDeliverySlotToQuote plugin) and onto the order at submission (OrderObserver), plus order and shipment e-mail template variables (EmailVariables).
  • The booked slot is shown back to the customer on the order success page and in "My Account → Orders".
  • The booked slot is shown in the checkout order summary (right column), next to the shipping method, on every step.
  • The booked slot is exposed in the admin order grid (filterable/sortable columns), the REST order API (extension_attributes on order + order list) and GraphQL (CustomerOrder.qoliber_delivery_date / qoliber_delivery_time).
  • Admin Bookings report / daily delivery manifest (Delivery Slots → Bookings): a filterable/sortable grid of confirmed bookings joined with order data - recipient, shipping address, phone and order status - so a date-filtered CSV / Excel export doubles as the driver's manifest.
  • Admin Capacity Overview (Delivery Slots → Capacity Overview): booked (confirmed + active holds) vs capacity per slot per bookable day for the next two weeks, per delivery method, with full slots highlighted.
  • Weekday-specific slots: each delivery method can override its time slots per weekday ("N=slots" lines, e.g. Saturday-only mornings or no Sunday delivery); the calendar, validation, capacity overview, reschedule UIs and Hyva picker are all weekday-aware, and days without slots render as unavailable.
  • Per-date capacity overrides ("Y-m-d=N" lines, e.g. Christmas Eve = 2) and a daily order cap per method (max deliveries per day across all slots); enforced in the calendar, at reservation/checkout validation and - authoritatively - at order placement and reschedule under the per-slot lock.
  • Richer pricing model per method: base fee + per-km (or flat) price, minimum charge, free-above cart total, and a maximum delivery distance (km, with Google distance); plus postcode restrictions (patterns with * wildcard) - all exposed as proper TransportInterface getters so the REST service contract carries them too.
  • Per-slot surcharges ("slot=amount" lines, e.g. an evening premium): shown as "+X.XX" next to the slot in the picker and added to the shipping price when that slot is booked (order totals verified end-to-end).
  • Delivery reminder e-mail: a daily cron sends customers a reminder N days before their delivery (configurable toggle, days-before and e-mail template), including the slot, their note and the add-to-calendar link; each order is reminded once (send failures retry the next day).
  • Holiday auto-blackout: per method, national public holidays of selected countries (PL, DE, US, GB, FR, NL, AT, CZ, SK, ES, IT; fixed-date + Easter-derived) are blocked automatically for this and next year - no more typing blackout dates by hand. Plus a slot generator shorthand: type 10:00-18:00/2h in the Slots field and it expands to the explicit windows on save.
  • Merchant display & policy settings: show/hide the header booking widget, show/hide the "N left" remaining-capacity hint, configurable customer-facing date format, and customer change policy - allow/disallow reschedule, allow/disallow cancel, and a "latest change N hours before the slot" cut-off (Qoliber\DeliverySlots\Model\ChangePolicy).
  • Delivery comment: optional customer note at checkout ("leave at the gate"), toggleable per store; sanitised server-side (255 chars, tags stripped), stored on the quote/order and shown in the admin order view, customer order view/success page and as the {{var qoliber_delivery_comment}} e-mail variable.
  • Customer self-service (My Account → order view): cancel the booked slot or move it to another date/time while the order is still changeable (future date, not yet shipped). Rescheduling is capacity-safe (re-validated under the per-slot lock) and CSRF-protected; ownership is enforced.
  • Admin slot management (order view): change or cancel an order's delivery slot, sharing the same capacity-safe Qoliber\DeliverySlots\Model\BookingManager.
  • Calendar invite (.ics) for the booked slot: an "Add to calendar" link on the order success page and in "My Account → Orders", plus a {{var qoliber_delivery_ics_url}} for the order e-mail. The download is token-protected (the order's protect_code), so it is safe to link from e-mail and never exposes another customer's order.
  • GET /V1/qoliber-delivery-slots/slots REST endpoint exposing current and next month availability, returning a typed SlotResultInterface so it is fully described in the generated Swagger / OpenAPI schema.
  • PHP 8.1 through 8.5 support.
  • PHPStan level 8, PHP_CodeSniffer (Magento2 standard) and PHPMD (Magento standard ruleset) configuration, wired as Composer quality scripts; all gates pass with zero findings.
  • PHPUnit unit tests for the core scheduling/capacity/booking logic, plus an integration test (Test/Integration) covering the DB-backed slot-capacity ledger (confirmed counts, capacity enforcement, release).
  • Playwright end-to-end test suite for the slot booking and checkout flow, plus a REST API contract test for the public slots endpoint.
  • Per-method availability rules (lead time, same-day cut-off, max days-ahead window, weekly days off, blackout dates), store-timezone aware.
  • Per-weekday cut-off overrides (cutoff_rules, e.g. 5=12:00,2): a different cut-off time - and optionally lead days - for orders placed on a given weekday.
  • Checkout slot picker now opens in a popup via a "Choose / Change delivery slot" button instead of an always-open inline calendar.
  • Calendar day indicators now reflect real availability: one dot per slot, green when the slot has capacity, red when it is fully booked.
  • The reserved slot is exposed as a customer-data section (qoliber-delivery-slot), so the header badge renders instantly from localStorage on page refresh (no network round-trip) while the markup stays full-page-cache safe.
  • Configurable number of calendar months to display (calendar_months, 1-12, default 2).
  • Slot dropdown opens leftwards on right-hand calendar columns so it no longer overflows the calendar/modal.
  • "Require slot selection" option that blocks checkout (server-side) when the carrier is chosen without a slot.
  • Admin order-view "Delivery Slot" panel and a "%1 left" remaining-capacity hint on each slot.
  • Persistent soft-hold reservations: an active reservation consumes slot capacity (alongside confirmed orders) until it expires, closing the concurrent-checkout overbooking race. A visitor's own hold never counts against them. Expired holds are purged by a cron (*/15).
  • Polish (pl_PL) translations for the full storefront and admin UI.

Security

  • The header widget markup is now full-page-cache safe: it carries no session data (form key / reservation). Reservation state is hydrated at runtime from the uncacheable slot/info endpoint and the form-key cookie.
  • All calendar HTML is escaped and the calendar controller ignores unknown methodCode request values (prevents reflected XSS).
  • Reservations are validated server-side against the method's configured slots, availability rules and capacity, so a tampered request cannot book an invalid date/slot.
  • The admin delivery-method controllers now declare the Qoliber_DeliverySlots::list ACL resource (including the list/Index action, which extended the base action directly and was not covered before), so access follows the configured admin role; Save and Delete are restricted to POST and the grid submits deletes via POST with the form key.
  • The admin save action no longer mass-assigns the posted payload onto the model's primary key (entity_id is taken from the loaded record).
  • The slot sent from checkout (setShippingInformation) is now re-validated server-side (Qoliber\DeliverySlots\Model\SlotValidator: configured slots, availability rules and capacity), so a tampered request can no longer persist an invalid slot onto the quote.
  • The per-slot lock now fails closed: when the lock cannot be acquired (contention or backend failure) the capacity check-and-write is not run unlocked - the customer gets a "booking system is busy, try again" message instead of a potential overbooking. Storefront reserve and reschedule surface it as a normal error.
  • Order placement is blocked (instead of silently proceeding) when the order carries slot data but its delivery method can no longer be resolved, so an order can never consume a slot without a matching booking ledger row.
  • The Hyvä Checkout slot picker now reserves under the same per-slot lock as the base checkout, closing the concurrent-overbooking window on Hyvä stores.
  • The public calendar endpoint no longer returns raw exception messages; failures are logged and a generic message is shown.
  • The admin edit-page Delete button submits via POST with the form key (it previously issued a GET against the POST-only delete action and was broken).

Fixed

  • The header widget now sits at the far right of the top utility bar (right of the account links) with a delivery-truck icon and a subtle pill style; the booked state is a green pill with the same white icon.
  • The header "Book a delivery slot" widget now sits in the top utility bar next to the Sign In / Create an Account links (header.panel), with light text and a filled-green "booked" pill so it reads on Luma's dark bar.
  • Redesigned the calendar picker: a compact popup showing one month at a time with prev/next arrows (instead of several months sprawling across a large modal), bookable days are light clickable tiles, and the day's slots open as a clean centered panel (no overflow, no horizontal scrollbar, no full-page dim) with a filled, properly inset "Confirm date" button; click-outside dismisses it.
  • Added loading feedback: a spinner while the calendar loads and a loading state on the "Confirm date" button while the reservation is saved (header widget and checkout picker).
  • The delivery slot is now strictly tied to the Qoliber Delivery Slots carrier: it is hidden in the summary/picker and is no longer saved to the quote/order when a different shipping method (e.g. Flat Rate) is selected.
  • The checkout slot picker now pre-selects a slot already booked from the header widget (or a previous visit), hydrating from the qoliber-delivery-slot customer-data section instead of showing an empty "Choose delivery slot".
  • The carrier now rejects a method when the total cart weight/volume exceeds its limit (previously only checked per line item).
  • Composer requirements use proper Magento version ranges instead of *; dev/test artifacts (Test/, .phpqa/, node_modules/) are excluded from the distributable package via .gitattributes.
  • Delivery method deletion now loads the entity by its real primary key (entity_id) instead of a never-populated transport_id, so removing a method/slot from the admin works as intended.
  • Corrected the product volume calculation in the carrier, which due to PHP operator precedence (?? vs *) previously evaluated to the product height only instead of height × width × length.
  • Cancelling an order now frees the booked slot's capacity: the confirmed booking is removed on order_cancel_after (Qoliber\DeliverySlots\Observer\ReleaseBookingOnCancel), so the slot becomes bookable again instead of staying permanently consumed.
  • A full refund (credit memo for the whole order) likewise frees the slot's capacity (ReleaseBookingOnRefund); partial refunds leave the booking in place.
  • The per-method title shown at checkout is now passed through __(), so a multilingual store can localise method names per store view.
  • Unit tests no longer depend on a prior setup:di:compile: the test bootstrap synthesises generated factory / extension-attribute classes on demand (Magento's own unit autoloader), so the suite runs on a clean checkout / in CI.
  • Overbooking is now prevented under concurrency. Slot reservation and the confirmed booking at order placement run under a per-slot lock (Qoliber\DeliverySlots\Model\SlotLock, backed by Magento\Framework\Lock\LockManagerInterface); capacity is re-validated against confirmed orders inside the lock at submit, so two simultaneous checkouts can no longer both take the last slot. The booking is recorded before the hold is released, and an over-capacity slot aborts placement (instead of silently overbooking) rather than being swallowed.
  • The carrier now honours its "Ship to Applicable Countries" restriction in collectRates(), and distance-based pricing reads the destination from the shipping rate request instead of the checkout session (so it also works in admin/API contexts).
  • Slot availability now derives "today" from the store timezone (was the server/UTC date), fixing availability/count drift near midnight in non-UTC stores.
  • The isolated quality tooling was moved out of the module directory so bin/magento setup:di:compile no longer scans it; the raw module source tree is production-installable, not only the packaged archive.
  • The sliding reservation hold now has an absolute maximum lifetime (4× the configured window), so an idle open tab can no longer hold a slot - and starve other shoppers - indefinitely.
  • Fixed the admin delivery-method grid: text/number columns no longer use a date filter (and dropped the updated_at field class), and the actions column no longer references a foreign module's edit route - edit/delete use the module's own routes and entity_id.
  • Centralised configuration access: the reservation hold time is read only through Model\Config (the duplicated path/default in SlotReservation was removed).
  • Storefront/checkout slot pickers now show an error when a reserve request fails (added .fail() handlers), the header countdown/heartbeat timers are no longer torn down and rebuilt on every customer-data tick, and the global activity listener is namespaced and removed on widget destroy.
  • The booking calendar day cells and the "Confirm date" control are keyboard-operable (role="button", tabindex, Enter/Space) with an accessible label.
  • The carrier is no longer offered for an empty cart, and uses ScopeInterface::SCOPE_STORE for the carrier title.
  • The public GET /V1/qoliber-delivery-slots/slots endpoint takes typed parameters (miniCartId, methodCode) instead of reading the request directly, and ignores an unknown method code.
  • Shipped a source-locale i18n/en_US.csv; removed dead block methods that implied the (intentionally session-free) header markup carried reservation state.
Changelog — Delivery Slots — Checkout & Shipping — Extensions | qoliber Docs