How to Build a Custom Delivery Date Selection Module in Magento 2

In this post I’ll walk you through how to build a custom “Delivery Date Selection” module for Magento 2. We’ll cover architecture, creating custom attributes and DB tables, integrating a checkout UI component, handling constraints (holidays, lead times, time slots), UX tips for both the storefront and the admin backoffice, and how to keep it compatible with stock management and other Magefine modules. I’ll keep things practical with step-by-step code examples so you can copy, adapt and ship.
Why build a Delivery Date Selection module?
Delivery date selection improves conversion and reduces support enquiries by letting customers choose a convenient delivery day and time. Most merchants need business rules: exclude holidays, enforce preparation time, allow only certain time windows and keep the module compatible with inventory/fulfillment extensions. Building your own module gives full control and tight integration with Magento 2 checkout and admin.
High-level architecture
At a glance, the module is composed of:
- Database: custom tables to store configurable time slots, holidays and order delivery info.
- Config: system configuration to control lead times, available days, and time slot behavior.
- Checkout front-end: a UI component (Knockout JS) that integrates into the Magento 2 checkout and collects selected date and slot.
- Back-end: admin UI to manage holidays, time slots and order delivery meta; observers to persist data on order placement.
- Integration points: observers/plugins to keep compatibility with inventory, shipping and other Magefine extensions.
Module skeleton
We’ll call the module Magefine_DeliveryDate. Create the basic module files:
app/code/Magefine/DeliveryDate/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'Magefine_DeliveryDate',
__DIR__
);
app/code/Magefine/DeliveryDate/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="Magefine_DeliveryDate" setup_version="1.0.0" />
</config>
Database: schema and sample table structure
You need tables for:
- delivery_date_order — store the selected date and slot per order
- delivery_date_slots — available time slots and rules
- delivery_date_holidays — blocked dates
Example declarative schema (db_schema.xml):
app/code/Magefine/DeliveryDate/etc/db_schema.xml
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="magefine_delivery_slots" resource="default" engine="innodb" comment="Magefine delivery slots">
<column xsi:type="int" name="slot_id" padding="10" unsigned="true" nullable="false" identity="true" />
<column xsi:type="varchar" name="label" length="255" nullable="false" />
<column xsi:type="time" name="from" nullable="false" />
<column xsi:type="time" name="to" nullable="false" />
<column xsi:type="tinyint" name="is_active" nullable="false" default="1" />
<column xsi:type="int" name="max_orders" nullable="true" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="slot_id" />
</constraint>
</table>
<table name="magefine_delivery_holidays" resource="default" engine="innodb" comment="Magefine delivery holidays">
<column xsi:type="int" name="holiday_id" nullable="false" identity="true" unsigned="true" />
<column xsi:type="date" name="date" nullable="false" />
<column xsi:type="varchar" name="label" length="255" nullable="true" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="holiday_id" />
</constraint>
</table>
<table name="magefine_order_delivery" resource="default" engine="innodb" comment="Magefine order delivery info">
<column xsi:type="int" name="entity_id" nullable="false" identity="true" unsigned="true" />
<column xsi:type="int" name="order_id" nullable="false" unsigned="true" />
<column xsi:type="date" name="delivery_date" nullable="true" />
<column xsi:type="varchar" name="time_slot_label" length="255" nullable="true" />
<column xsi:type="int" name="slot_id" nullable="true" unsigned="true" />
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="entity_id" />
</constraint>
</table>
</schema>
System configuration
Use system.xml to expose settings such as lead time (in days), allowed weekdays, default time slots per day, and whether to show in shipping step or review step.
app/code/Magefine/DeliveryDate/etc/adminhtml/system.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
<system>
<section id="delivery_date" translate="label" sortOrder="200" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Delivery Date Settings</label>
<group id="general" translate="label" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1">
<label>General</label>
<field id="lead_time" translate="label" type="text" sortOrder="10" showInDefault="1">
<label>Preparation time (days)</label>
<comment>Minimum number of days between order and delivery</comment>
</field>
<field id="allowed_weekdays" translate="label" type="multiselect" sortOrder="20" showInDefault="1">
<label>Allowed weekdays</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
</group>
</section>
</system>
</config>
Creating a custom attribute or saving delivery data to order
We have two common approaches: add a custom order attribute (EAV or extension attributes) or create your own order delivery table linking to order_id. Using a separate table (magefine_order_delivery) keeps order schema simple and is easier to maintain. But sometimes you want to show delivery info in order grid and export; in that case use order extension attributes or add columns via extension attributes and plugins.
Example: observer to save delivery selection when order is placed. We’ll capture the selected values from the quote and persist them on order success.
app/code/Magefine/DeliveryDate/etc/events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
<event name="checkout_submit_all_after">
<observer name="magefine_delivery_save" instance="Magefine\DeliveryDate\Observer\SaveDeliveryToOrder" />
</event>
</config>
app/code/Magefine/DeliveryDate/Observer/SaveDeliveryToOrder.php
<?php
namespace Magefine\DeliveryDate\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class SaveDeliveryToOrder implements ObserverInterface
{
protected $deliveryFactory;
public function __construct(\Magefine\DeliveryDate\Model\DeliveryFactory $deliveryFactory)
{
$this->deliveryFactory = $deliveryFactory;
}
public function execute(Observer $observer)
{
$order = $observer->getEvent()->getOrder();
$quote = $observer->getEvent()->getQuote();
// Retrieve data stored on quote (we’ll set it from the checkout JS)
$deliveryDate = $quote->getData('magefine_delivery_date');
$timeSlotId = $quote->getData('magefine_delivery_slot_id');
$timeSlotLabel = $quote->getData('magefine_delivery_slot_label');
if ($deliveryDate) {
$model = $this->deliveryFactory->create();
$model->setData([
'order_id' => $order->getEntityId(),
'delivery_date' => $deliveryDate,
'slot_id' => $timeSlotId,
'time_slot_label' => $timeSlotLabel
]);
$model->save();
}
}
}
Checkout UI: where to put the component
You have two common options:
- Integrate into the shipping step (shipping-address) so customer selects date before payment.
- Place it in order review only (checkout_index_index layout) — less ideal for shipping-dependent rules.
Example: adding a custom UI component to the shipping step via layout and requirejs mixin or a custom checkout layout update.
app/code/Magefine/DeliveryDate/view/frontend/layout/checkout_index_index.xml
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="checkout.container">
<block class="Magento\Checkout\Block\Onepage" name="magefine.delivery.checkout" template="Magefine_DeliveryDate::checkout/delivery.phtml" before="-"/>
</referenceContainer>
</body>
</page>
But probably the best approach is to register a UI component as part of the shipping step’s layout JS configuration. Use mixins to extend Magento_Checkout/js/view/shipping or create your own component and insert it into checkoutSteps.
Front-end JS component (simplified)
Create a UI component with KnockoutJS to show a date picker and time slots. Use Magento’s customerData/quote to save values to the quote via an AJAX call to a controller that adds them as quote attributes.
app/code/Magefine/DeliveryDate/view/frontend/web/js/view/delivery-date.js
define([
'uiComponent',
'ko',
'mage/storage',
'Magento_Checkout/js/model/quote',
'Magento_Ui/js/modal/alert'
], function (Component, ko, storage, quote, alert) {
'use strict';
return Component.extend({
defaults: {
template: 'Magefine_DeliveryDate/delivery-date'
},
selectedDate: ko.observable(null),
selectedSlot: ko.observable(null),
slots: ko.observableArray([]),
initialize: function () {
this._super();
// Load available slots via AJAX
storage.get('deliverydate/ajax/slots')
.done(function (response) {
this.slots(response.slots);
}.bind(this));
},
save: function () {
var payload = {
date: this.selectedDate(),
slot_id: this.selectedSlot() ? this.selectedSlot().id : null,
slot_label: this.selectedSlot() ? this.selectedSlot().label : null
};
storage.post('deliverydate/ajax/saveQuote', JSON.stringify(payload))
.done(function () {
// update quote data or show confirmation
})
.fail(function (){
alert({content: 'Unable to save delivery date.'});
});
}
});
});
KO template (simple)
app/code/Magefine/DeliveryDate/view/frontend/web/templates/delivery-date.html
<div class="magefine-delivery-date">
<label>Choose delivery date</label>
<input data-bind="value: selectedDate, event: {change: save}" type="date" />
<div data-bind="foreach: slots">
<label>
<input type="radio" name="delivery_slot" data-bind="value: $data, checked: $parent.selectedSlot, click: $parent.save" />
<span data-bind="text: label"></span>
</label>
</div>
</div>
Server-side controller to accept quote changes
app/code/Magefine/DeliveryDate/Controller/Ajax/SaveQuote.php
<?php
namespace Magefine\DeliveryDate\Controller\Ajax;
use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Quote\Model\QuoteRepository;
use Magento\Checkout\Model\Session as CheckoutSession;
class SaveQuote extends Action
{
protected $checkoutSession;
protected $quoteRepository;
public function __construct(Context $context, CheckoutSession $checkoutSession, QuoteRepository $quoteRepository)
{
parent::__construct($context);
$this->checkoutSession = $checkoutSession;
$this->quoteRepository = $quoteRepository;
}
public function execute()
{
$data = json_decode($this->getRequest()->getContent(), true);
$quote = $this->checkoutSession->getQuote();
if (!empty($data['date'])) {
$quote->setData('magefine_delivery_date', $data['date']);
$quote->setData('magefine_delivery_slot_id', $data['slot_id']);
$quote->setData('magefine_delivery_slot_label', $data['slot_label']);
$this->quoteRepository->save($quote);
}
$this->getResponse()->setBody(json_encode(['success' => true]));
}
}
Handling constraints: holidays, lead time, and time slots
This is the core logic. On the server side provide an endpoint that returns valid dates and slots taking account of:
- Configured lead time: now + lead_time days should be earliest selectable date.
- Holidays table: excluded dates.
- Allowed weekdays: merchant might disable specific weekdays.
- Slot capacity: optional limit of orders per slot per day.
- Time windows: compute available slots per selected date.
Example: slot provider logic (simplified):
app/code/Magefine/DeliveryDate/Model/AvailabilityProvider.php
<?php
namespace Magefine\DeliveryDate\Model;
use Magento\Framework\Stdlib\DateTime\DateTime;
class AvailabilityProvider
{
protected $holidayCollectionFactory;
protected $slotCollectionFactory;
protected $scopeConfig;
protected $date;
public function __construct(
\Magefine\DeliveryDate\Model\ResourceModel\Holiday\CollectionFactory $holidayCollectionFactory,
\Magefine\DeliveryDate\Model\ResourceModel\Slot\CollectionFactory $slotCollectionFactory,
\Magento\Store\Model\ScopeInterface $scopeConfig,
DateTime $date
) {
$this->holidayCollectionFactory = $holidayCollectionFactory;
$this->slotCollectionFactory = $slotCollectionFactory;
$this->scopeConfig = $scopeConfig;
$this->date = $date;
}
public function getAvailableDates($rangeDays = 30)
{
// compute from today + lead_time
$leadTime = (int)$this->scopeConfig->getValue('delivery_date/general/lead_time');
$start = strtotime('+' . $leadTime . ' days');
$holidays = $this->holidayCollectionFactory->create()->getColumnValues('date');
$available = [];
for ($i = 0; $i < $rangeDays; $i++) {
$ts = strtotime('+' . ($i + $leadTime) . ' days');
$date = date('Y-m-d', $ts);
$weekday = date('N', $ts); // 1 (Mon) - 7 (Sun)
// check weekday allowed - example: store config contains allowed weekdays as array
$allowedWeekdays = explode(',', $this->scopeConfig->getValue('delivery_date/general/allowed_weekdays') ?: '1,2,3,4,5');
if (!in_array($weekday, $allowedWeekdays)) continue;
if (in_array($date, $holidays)) continue;
// optionally check slot capacity
$available[] = $date;
}
return $available;
}
}
Slot capacity and concurrency
When your store is busy you may want to limit how many orders can be scheduled per slot per day. Typical design:
- Count existing orders for date+slot (from magefine_order_delivery table).
- Compare to slot.max_orders and disable if full.
- Use transactions or optimistic locking to avoid race conditions at high concurrency — e.g. reserve on checkout submit and confirm on order placement.
Example SQL-ish pseudo logic:
SELECT COUNT(*) FROM magefine_order_delivery WHERE delivery_date = :date AND slot_id = :slot_id
IF count >= slot.max_orders THEN slot not available
Admin backoffice: manage holidays and time slots
Create admin grids for slots and holidays using standard Magento UI components (ui_component grids and forms). This provides UX for administrators to add/remove holidays and manage slots. Also expose system settings in Stores > Configuration for global settings.
Example admin menu item and ACL to access the grid:
app/code/Magefine/DeliveryDate/etc/adminhtml/menu.xml
<menu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Menu/etc/menu.xsd">
<add id="Magefine_DeliveryDate::delivery" title="Delivery Date" module="Magefine_DeliveryDate" sortOrder="90" parent="Magento_Sales::sales" action="deliverydate/slot/index" resource="Magefine_DeliveryDate::delivery"/>
</menu>
Displaying delivery info in admin order view
You’ll want to add a panel in the admin order view showing the selected delivery date & slot. Use layout xml to add a block to the order view or use an observer to append data to order view blocks. Also provide the ability to change delivery date from admin (respecting constraints).
Integration with inventory, shipping and other Magefine modules
Compatibility is key. Here are recommended practices to keep it harmonious:
- Do not rewrite core classes. Use plugins or observers.
- Use extension attributes or a dedicated table rather than adding fragile changes to sales_order table. That reduces conflicts with inventory or other extensions that alter orders.
- Listen to events rather than hard-coding flows. For example, observe checkout_submit_all_after instead of forcing a custom checkout pipeline.
- If you need to reserve inventory for a slot, integrate with MSI (Multi Source Inventory) APIs or your stock module: create a reservation entry with source selection or notify the fulfillment module about the scheduled shipment.
- Provide extension points: preferences, interfaces and events so other Magefine extensions can read/write delivery data. For example, dispatch a custom event magefine_delivery_reserve to let the stock/reservation module act.
Example: dispatch event for reservation:
// In SaveDeliveryToOrder::execute after saving delivery info
$this->_eventManager->dispatch('magefine_delivery_reserve', ['order' => $order, 'delivery' => $model]);
UX: front-end and admin best practices
UX matters. Here are practical tips to reduce friction:
- Keep the date picker simple: show earliest selectable date and disabled blocked dates (holidays, weekends if disabled).
- Show time slot capacity and “few slots left” hints if capacity is enabled — creates urgency and transparency.
- Validate server-side and client-side. Never rely only on client validation for blocked dates or capacity.
- Keep checkout flow minimal: if you need an extra click, make it clear why (e.g. choose delivery date)
- In admin, provide bulk actions for shifting many orders to another delivery date (helpful in case of closures).
- Expose delivery data on order emails and PDF invoices (so warehouse & customer have the info).
Testing strategy
Test the module thoroughly with:
- Unit tests for availability provider and business rules.
- Integration tests for quote-to-order flow.
- Manual QA of checkout flow with multiple payment/shipping methods and with and without logged in customers.
- Concurrency tests to simulate many customers booking the same slot.
Performance considerations
Computing available dates for many customers concurrently must be efficient. Strategies:
- Cache static results (e.g. list of holidays, slot definitions) and only refresh when admin changes them.
- Keep DB indexes on delivery_date, slot_id and order_id for fast counts.
- Limit server-side computation to necessary ranges (e.g. next 30 days).
Example: show delivery date on order emails
Add a small template variable populated from the order delivery table or extension attribute. Use a plugin on \Magento\Sales\Model\Order\Email\SenderBuilder or attach a variable via email templates.
Admin UX: change delivery date from order view
Provide an admin form in Sales > Orders to change the delivery date. When changing, re-run constraint checks (holidays, slot capacity) and, if needed, notify fulfillment. Consider logging changes and optionally notifying the customer.
Handling special cases
Common edge cases and how to handle them:
- Order edited after placement: keep original delivery date but mark it changed, or charge a rescheduling fee — implement as business logic.
- Shipping method affects availability: e.g. express courier delivers on different days. Use shipping method code when computing availability.
- Multi-shipping checkout: you may need per-address delivery dates. Save selections per quote address.
Compatibility with Magefine extensions
If you or your client uses other Magefine modules (for example advanced shipping, checkout improvements or hosting integrations) follow these guidelines:
- Check for conflicts in observers and events. Avoid listening to generic events that many extensions use; use your own event names for extension points.
- If other Magefine modules offer shipping or reservation features, provide a small integration doc or helper class so they can read the delivery selection via API (e.g. DeliveryRepositoryInterface::getByOrderId($orderId)).
- Test the module with Magefine’s common extensions (e.g. advanced shipping, payment modules) and with the hosting environment to ensure caching layers don’t block dynamic date calculation. Cache dynamic endpoints via AJAX and bypass full page cache for per-customer available slots calls.
Security and privacy
Delivery dates are not sensitive personal data, but keep the usual good practices:
- Sanitize any admin inputs (dates, labels).
- Protect admin endpoints with ACL.
- Only expose delivery info to authorized users (order view, customer area).
Extensibility: how to let other modules hook in
Expose interfaces and events so other modules can extend behavior without editing your code. Example services to provide:
- DeliveryRepositoryInterface: read/write delivery data for a given order.
- AvailabilityProviderInterface: given a date range and context (shipping method, address), return available slots.
- Events: magefine_delivery_before_save_quote, magefine_delivery_after_save_quote, magefine_delivery_reserve
Real-world tips from shipping teams
- Keep a default buffer between order and delivery to account for picking/packing.
- Consider setting different lead times per product type or warehouse — integrate with MSI sources or product attributes.
- Allow cut-off time for same-day delivery if you support it (e.g. if now < 2pm allow same-day otherwise start tomorrow).
Step-by-step recap to build the module
- Create module skeleton and register it in Magento.
- Add db_schema.xml for slots, holidays and order delivery table.
- Add system.xml to expose lead time and allowed weekdays in admin config.
- Create admin UI grids for slots and holidays (ui_component).
- Create front-end Knockout UI component to pick date and slot, and a controller to persist to quote.
- Create observer to persist quote delivery data into your order delivery table on order placement.
- Implement AvailabilityProvider that respects lead time, holidays and slot capacity.
- Add order view block in admin to display/change delivery date and dispatch reservation events.
- Test thoroughly and add unit/integration tests; ensure compatibility with other Magefine modules and MSI.
Example: DeliveryRepositoryInterface and a simple implementation
app/code/Magefine/DeliveryDate/Api/DeliveryRepositoryInterface.php
<?php
namespace Magefine\DeliveryDate\Api;
interface DeliveryRepositoryInterface
{
public function getByOrderId($orderId);
public function save(\Magefine\DeliveryDate\Model\Delivery $delivery);
}
app/code/Magefine/DeliveryDate/Model/DeliveryRepository.php
<?php
namespace Magefine\DeliveryDate\Model;
use Magefine\DeliveryDate\Api\DeliveryRepositoryInterface;
class DeliveryRepository implements DeliveryRepositoryInterface
{
protected $resource;
protected $factory;
protected $collectionFactory;
public function __construct(
\Magefine\DeliveryDate\Model\ResourceModel\Delivery $resource,
\Magefine\DeliveryDate\Model\DeliveryFactory $factory,
\Magefine\DeliveryDate\Model\ResourceModel\Delivery\CollectionFactory $collectionFactory
) {
$this->resource = $resource;
$this->factory = $factory;
$this->collectionFactory = $collectionFactory;
}
public function getByOrderId($orderId)
{
$collection = $this->collectionFactory->create()->addFieldToFilter('order_id', $orderId);
return $collection->getFirstItem();
}
public function save(\Magefine\DeliveryDate\Model\Delivery $delivery)
{
$this->resource->save($delivery);
return $delivery;
}
}
Examples of corner-case code snippets
Cut-off time logic for same-day delivery:
// in AvailabilityProvider
$cutoff = $this->scopeConfig->getValue('delivery_date/general/cutoff_time') ?: '14:00';
if ($leadTime === 0) {
$currentTime = date('H:i');
if ($currentTime >= $cutoff) {
// same-day no longer available
$start = strtotime('+1 day');
}
}
Deployment notes
On production:
- Run setup:upgrade to create schema.
- Run di:compile and static content deploy if necessary.
- Warm cache for typical AJAX endpoints or ensure they bypass FPC correctly.
- Add database indexes for fast queries on delivery_date & slot_id.
Summary
Building a Delivery Date Selection module in Magento 2 is a practical and highly valuable improvement for many stores. Keep separation of concerns: use a dedicated table for order delivery data, keep front-end logic light and rely on server-side availability checks that respect lead time, holidays, and slot capacities. Provide admin UX for managing slots and holidays and expose extension points so other Magefine modules — or your own integrations — can read/write delivery information.
If you follow the structure above (db schema, system config, front-end KO component, quote persistence, order save observer, availability provider and admin grids) you’ll have a robust solution compatible with MSI and other extensions. For Magefine customers, ensure you test with the Magefine shipping and checkout extensions and provide a short integration guide so teams can hook into the delivery reservation event.
Want a compact checklist to move to production? Here it is:
- Schema & DB indexes
- Admin grids & system config
- Front-end component & quote persistence
- Order observer & repository
- Availability rules (lead time, holidays, allowed weekdays)
- Slot capacity checks & concurrency handling
- Integration events for inventory & fulfillment
- Tests and QA with Magefine extensions
I hope this gives you a solid roadmap and practical code snippets to get started. If you want, I can generate a minimal working Git tree with the files above and a short installation script so you can spin it up locally and try the flow end-to-end.