17 min readOct 11, 2026by jakub

Changelog

Current version: 1.0.4

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.4] - 2026-10-11

Fixed

  • Delivery method form accepts 0 in Minimal Cart Value, Maximum Weight, Maximum Length, Maximum Volume and Price per kilometer. The carrier has always treated 0 as no minimum, no limit or no per-km charge, but the form demanded a value greater than 0. Methods that hold zeros, for example ones created by an import, the API or a setup script, could not be saved in the admin at all; they now save normally. Each of these fields now explains what 0 means (also in Polish).

[1.0.3] - 2026-10-10

Fixed

  • Capacity at order placement and rescheduling uses locking reads, also outside a transaction: a booking or hold another request inserted but has not committed yet is waited for and counted, and an older repeatable-read snapshot cannot hide committed rows. Calendar and validation reads stay non-locking.
  • Rescheduling retries once when the database picks it as a deadlock victim (two moves into nearby empty dates), then reports "busy" instead of an error page. Order placement reports the same "please try again" message on a deadlock.
  • Cancellation releases the booking on sales_order_save_after, inside the order-save transaction, for canceled orders and for closed ones (a partly invoiced order whose invoiced part was refunded). A failed save keeps the booking. This also covers cancellations that only save the order (payment deny, PayPal/IPN, Payflow).
  • Rescheduling a canceled or closed order is refused instead of reporting success while leaving the order with no booking.
  • Hold heartbeat judges expiry on the application clock, like every other hold check, so a skewed database clock cannot revive a hold another shopper already took.
  • Luma checkout keeps a saved slot tied to its delivery method: switching to another method and back, or a failed rate request, no longer hides a slot the server keeps for the order. The reservation response includes expires_at, so a changed slot is shown and submitted immediately, and the delivery note is restored from the quote.
  • Store timezones for quote slot validation: cross-store cart requests use the quote store's day and time.
  • Calendar export (.ics): lines longer than 75 octets are folded (RFC 5545); the event UID no longer derives from the guest-order access code; an unparseable stored date redirects instead of returning an error.

Upgrade notes

  • The booking-release observer qoliber_delivery_slots_release_on_cancel moved from order_cancel_after to sales_order_save_after. A customization that disabled it under the old event must disable it under the new one.
  • Calendar event UIDs change once: an event imported from a 1.0.2 download is not merged with a fresh 1.0.3 download.
  • Public signatures that gained optional parameters or changed return types (BC for callers; preference subclasses overriding them must match): SlotAvailability::getConfirmedCount/getBookedCount/getConfirmedDayCount/getDayCount ($lockRows), ReservationLedger::countActive/countActiveForDate ($lockRows), SlotValidator::validate/validateMethod (store id) and, since 1.0.2, Api\Google\Maps::getDistance (?int $storeId), IcsGenerator::generate ($timezone), ReservationLedger::touch() (now returns bool). The calendar controller Controller\Slot\Ics gained a constructor dependency.

[1.0.2] - 2026-10-09

Fixed

  • Lock a delivery method before checking for upcoming bookings and deleting it; concurrent bookings cannot be silently removed, including when an outer transaction already holds an older database snapshot.
  • Keep delivery-method codes immutable after creation so existing orders and quotes continue to resolve their shipping method; names and other settings remain editable.
  • Use the rated quote's store for the shipping origin and Google Maps API key.
  • Keep ambiguous legacy bookings with no store as single rows in the admin grid, without joining customer details from another store.
  • Include Polish Christmas Eve from 2025 and observed US/GB holiday weekdays, including year-end observations and separate Christmas/Boxing Day substitutes.
  • Export timed calendar events as UTC instants derived from the order store's timezone, and give each order a stable UID that differs across orders and installations.
  • Add delivery-slot variables to normal shipment confirmation emails.
  • Preserve unresolved legacy bookings on cancellation, refund, retry and rescheduling; a NULL-store booking is released only when its order uniquely resolves to the requested store.
  • Release capacity after a rolled-back order save even when Magento leaves its allocated entity ID on the in-memory order; committed orders retain their booking.
  • Renew only active database holds, without reviving expired or purged rows or shortening a concurrent renewal; update the session only after renewal succeeds.
  • Restore Luma's saved delivery selection in the picker and totals after hold expiry and checkout reloads. Share the selection state, persist the displayed date/time, and discard saved selections when the shipping method changes.

[1.0.1] - 2026-10-08

Fixed

  • Checkout pricing: the slot is stored on the quote before shipping rates are collected, so a changed slot's surcharge is priced into the totals the checkout returns (and quote totals are re-collected whenever a stored slot changes).
  • Luma checkout: the calendar follows the selected delivery method (no longer the default method's slots); a hold for another method, or a hold that ended, is no longer shown as selected; the order summary only shows the selected method's slot.
  • Stale bookings after ordering: placing an order refreshes the customer-data section, so the header and the next checkout stop showing the used slot.
  • Capacity: order placement and rescheduling count other shoppers' active holds under the slot lock; a reschedule also counts the customer's own hold for a new cart.
  • Method switch: an optional-slot checkout no longer carries the previous delivery method's slot over to the newly selected one.
  • Same-day windows that have already started are neither offered nor accepted.
  • Store timezones: schedule rules (lead time, cut-off, "today") use the ORDER's store for rescheduling, admin-created orders and both reschedule pickers; the storefront and REST calendars start at the store-local month, and the REST calendar no longer skips a month after the 30th/31st.
  • Carrier limits: maximum length and volume apply again (dimension attributes are loaded on cart items); configurable and bundle items are weighed and measured once; no product is loaded per cart item; multishipping items are supported.
  • Carrier money: minimum cart value and "free above" compare the base-currency subtotal, whatever currency the shopper displays; cart price rules granting free shipping make the delivery-slot methods free.
  • Require slot selection no longer blocks orders created in the admin or placed via GraphQL (neither has a slot input); the admin can set the slot from the order view.
  • Deleting a delivery method that still has upcoming deliveries is refused - its bookings would have been deleted with it. "Upcoming" is judged per booking in its order store's timezone (bookings without a known store against the earliest store day).
  • Long generated slot lists (e.g. 08:00-20:00/30m) are stored in full instead of being cut off at 255 characters.
  • Bookings grid shows the delivery day as a date, without shifting it into the admin's timezone (west of UTC it showed the previous day).
  • Robustness: delivery-method rows with empty optional columns no longer break the storefront; slot dates and times are validated strictly (impossible dates and padded values are rejected) and trimmed where they arrive.

Security

  • New setting Max Active Holds per Visitor Network (default 10, 0 = no limit): caps the soft holds one IP address can own, enforced atomically under concurrent requests. Only a keyed hash of the address is stored, purged with the hold.
  • The header widget, calendar and reserve endpoint follow the carrier's enabled flag.
  • Admin: the delivery-method grid data requires the Delivery Methods permission; the order-view slot form requires the Bookings permission.

Changed

  • Development tooling (PHPUnit bootstrap, PHPStan config) no longer hard-codes a workstation path; the bootstrap honours MAGENTO_ROOT.

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