Cómo crear un módulo de selección de fecha de entrega personalizado en Magento 2
In this post I’ll walk you through cómo build a custom “Delivery Date Selection” module for Magento 2. We’ll cover architecture, creating atributos personalizados and DB tables, integrating a pago UI component, handling constraints (holidays, lead times, time slots), UX tips for both the escaparate and the admin backoffice, and cómo 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 pago 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 pago 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 como 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 atributo personalizado 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="pago_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 pago 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: dónde 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 (pago_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 pago layout update.
app/code/Magefine/DeliveryDate/view/frontend/layout/pago_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="pago.container">
<block class="Magento\Checkout\Block\Onepage" name="magefine.delivery.pago" template="Magefine_DeliveryDate::pago/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 pagoSteps.
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 $pagoSession;
protected $quoteRepository;
public function __construct(Context $context, CheckoutSession $pagoSession, QuoteRepository $quoteRepository)
{
parent::__construct($context);
$this->pagoSession = $pagoSession;
$this->quoteRepository = $quoteRepository;
}
public function execute()
{
$data = json_decode($this->getRequest()->getContent(), true);
$quote = $this->pagoSession->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
Esto es 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 debería ser 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 puede ser 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 pago 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. Aquí están 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. Por ejemplo, observe pago_submit_all_after en lugar de forcing a custom pago pipeline.
- Si usted 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. Por ejemplo, disparche a custom event magefine_delivery_reserve to let the stock/reservation module act.
Example: disparche event for reservation:
// In SaveDeliveryToOrder::execute after saving delivery info
$this->_eventManager->disparche('magefine_delivery_reserve', ['order' => $order, 'delivery' => $model]);
UX: front-end and admin mejores prácticas
UX matters. Aquí están 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 está habilitado — creates urgency and transparency.
- Validate server-side and client-side. Never rely only on client validation for blocked dates or capacity.
- Keep pago 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 facturas (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 pago flow with multiple payment/método de envíos 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 debe ser 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 cómo 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 método de envío code when computing availability.
- Multi-shipping pago: you may need per-address delivery dates. Save selections per quote address.
Compatibility with Magefine extensions
Si usted or your client uses other Magefine modules (for example advanced shipping, pago 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 caché de página completa 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: cómo 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 (método de envío, 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 disparche 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:actualización 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.
Resumen
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.
Si usted 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 pago 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 le da a solid roadmap and practical code snippets to get started. Si usted 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.