Comment créer un module de réservation en magasin / Click & Collect personnalisé dans Magento 2
In this post I’ll show you comment build a custom "Reserve in Store" (Click & Collect) module for Magento 2. Think of this as a practical walk-through you can follow étape-by-étape while sipping coffee — I’ll explain the architecture, the clé fichiers and dossiers, comment integrate with paiement and stock, admin UI for managing stores and reservations, notifications, and a few performance conseils to keep your site snappy. Examples include concrete XML, PHP and JavaScript snippets you can copy and adapt for your project.
Why build a custom Click & Collect module?
Built‑in Magento fonctionnalités might not fit every commerçant’s needs: different pickup flux de travails, special store rules, varying stock handling with MSI, or bespoke e-mails and reminders. Building your own module means you control UX and logic — and you can integrate closely with your inventaire stratégie and your paiement.
High-level fonctionnalités we’ll cover
- Store selection during paiement (pickup point chooser)
- Persisting pickup selection on quote/commande
- Admin UI to manage participating stores and reservations
- Reservation lifecycle: creation, confirmation e-mail, pickup reminder
- Stock management integration (MSI-aware suggestions)
- Performance considerations (cache, debounce, async checks)
Module architecture
For clarity, we’ll use a conventional module name: Magefine_ReserveInStore. Keep fichiers organized and follow Magento conventions so the module is maintainable and easy to extend later.
Key répertoires and responsibilities:
- etc/: module.xml, adminhtml/frontend routes, di.xml and events
- registration.php and composer.json
- Controller/ (frontend & adminhtml) for actions
- Model/ResourceModel/ for reservation and store models
- view/frontend/layout and view/frontend/web/js for paiement UI
- view/adminhtml/ui_composant for grid and form UI
- etc/db_schema.xml to create tables for stores and reservations
- Observer/Plugin for paiement integration and commande convert
Basic module registration
<?php
// app/code/Magefine/ReserveInStore/registration.php
\Magento\Framework\Component\ComponentRegistrar::register(
\Magento\Framework\Component\ComponentRegistrar::MODULE,
'Magefine_ReserveInStore',
__DIR__
);
<?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_ReserveInStore" setup_version="1.0.0" />
</config>
Database schema (db_schema.xml)
We’ll create two tables: magefine_reserve_store (stores list) and magefine_reservation (reservation records tied to a quote/commande). Using db_schema.xml is the modern way (no install/mise à jour scripts).
<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="magefine_reserve_store" resource="default" engine="innodb" comment="Reserve In Store - Stores">
<colonne xsi:type="int" name="store_id" nullable="false" unsigned="true" identity="true" />
<colonne xsi:type="varchar" name="name" nullable="false" length="255" />
<colonne xsi:type="varchar" name="code" nullable="false" length="64" />
<colonne xsi:type="text" name="address" nullable="true" />
<colonne xsi:type="varchar" name="phone" nullable="true" length="64" />
<colonne xsi:type="smallint" name="active" nullable="false" default="1" />
<constraint referenceId="PRIMARY" type="primary" colonneList="store_id" />
</table>
<table name="magefine_reservation" resource="default" engine="innodb" comment="Reserve In Store - Reservations">
<colonne xsi:type="int" name="reservation_id" nullable="false" unsigned="true" identity="true" />
<colonne xsi:type="int" name="quote_id" nullable="true" unsigned="true" />
<colonne xsi:type="int" name="commande_id" nullable="true" unsigned="true" />
<colonne xsi:type="int" name="store_id" nullable="false" unsigned="true" />
<colonne xsi:type="varchar" name="status" nullable="false" length="32" default="pending" />
<colonne xsi:type="timestamp" name="created_at" nullable="false" on_update="false" default="CURRENT_TIMESTAMP" />
<colonne xsi:type="timestamp" name="pickup_by" nullable="true" />
<constraint referenceId="PK_RESERVATION" type="primary" colonneList="reservation_id" />
</table>
</schema>
This structure keeps reservation separate from quotes/commandes, which avoids changing core tables and makes the logic portable.
Models and Resource Models
Create simple Model/ResourceModel classes for Store and Reservation. Example for Reservation model skeleton:
<?php
namespace Magefine\ReserveInStore\Model;
use Magento\Framework\Model\AbstractModel;
class Reservation extends AbstractModel
{
protected fonction _construct()
{
$this->_init(\Magefine\ReserveInStore\Model\ResourceModel\Reservation::class);
}
}
<?php
namespace Magefine\ReserveInStore\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Reservation extends AbstractDb
{
protected fonction _construct()
{
$this->_init('magefine_reservation', 'reservation_id');
}
}
Admin interface: managing stores and reservations
Use UI Components for grille d'administrations and forms. Create ui_composant XML for the store grid (view/adminhtml/ui_composant/magefine_reserve_store_listing.xml) and for reservation listing. The admin needs to:
- Create and edit pickup stores (name, address, code, active)
- See reservations, filtre by status, export, and change status (e.g. confirmed, picked_up, cancelled)
Quick exemple: a form for store entity (a simplified UI Component snippet):
<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<dataSource name="store_form_data_source">
<argument name="dataProvider" xsi:type="configurableObject">
<argument name="class" xsi:type="chaîne">\Magefine\ReserveInStore\Ui\Component\Store\DataProvider
</argument>
</dataSource>
<champset name="general" triOrder="10">
<champ name="name"><settings><dataType>text</dataType><label>Store Name</label></settings></champ>
<champ name="code"><settings><dataType>text</dataType><label>Code</label></settings></champ>
</champset>
</form>
Integnote into paiement
C'est the trickiest part. You must let clients choose pickup in paiement and persist it to quote so the back-office can create a reservation when the commande is placed.
Two common UX patterns:
- Add a méthode de livraison called "Pick up in store" (simpler if you already have a méthode de livraison plugin system); or
- Add a paiement étape or a small UI composant on the shipping étape that shows a "Pickup" toggle and store selector (modern and less invasive).
I prefer adding a small UI composant on the shipping étape using Knockout (Magento paiement is powered by KO). Steps:
- Add layout: app/code/Magefine/ReserveInStore/view/frontend/layout/paiement_index_index.xml
- Provide JS composant and template in view/frontend/web/js/view
- Persist selection to quote via an AJAX call to a frontend contrôleur or via quote API
<?xml version="1.0"?>
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
<body>
<referenceContainer name="paiement.root">
<block name="reserve.in.store" template="Magento_Theme::html/blank.phtml">
<arguments>
<argument name="jsLayout" xsi:type="tableau">
<item name="composants" xsi:type="tableau">
<item name="paiement" xsi:type="tableau">
<item name="children" xsi:type="tableau">
<item name="étapes" xsi:type="tableau">
<item name="children" xsi:type="tableau">
<item name="shipping-étape" xsi:type="tableau">
<item name="children" xsi:type="tableau">
<item name="reserve-in-store" xsi:type="tableau">
<item name="composant" xsi:type="chaîne">Magefine_ReserveInStore/js/view/pickupbefore-shipping-méthode-form
The JS composant (very simplified) might look like this:
define([
'uiComponent',
'ko',
'mage/storage',
'Magento_Checkout/js/model/quote'
], fonction (Component, ko, storage, quote) {
'use strict';
return Component.extend({
defaults: { template: 'Magefine_ReserveInStore/pickup' },
isPickup: ko.observable(false),
selectedStore: ko.observable(null),
stores: ko.observableArray([]),
initialize: fonction () {
this._super();
var self = this;
// load available stores via REST or contrôleur
storage.get('reserveinstore/index/stores').done(fonction (response) {
self.stores(response);
});
self.isPickup.subscribe(fonction (valeur) {
// when toggled, persist to server (quote meta table)
storage.post('reserveinstore/quote/save', JSON.chaîneify({is_pickup: valeur}));
});
self.selectedStore.subscribe(fonction (storeId) {
storage.post('reserveinstore/quote/save', JSON.chaîneify({store_id: storeId}));
});
}
});
});
The frontend contrôleur endpoints (reserveinstore/index/stores and reserveinstore/quote/save) will return the list of active stores and persist the chosen option to a helper table linked by quote_id. In the back-office we’ll read that on commande place and create a reservation record.
Persisting data from quote to commande
We want to create a reservation record when an commande is placed. Use an observateur on sales_model_service_quote_submit_success (or the commande_place_after) event. Read the quote ID, find the stored pickup metadata, then create Magefine\ReserveInStore\Model\Reservation with commande_id and change status to confirmed.
<?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="paiement_submit_all_after">
<observateur name="magefine_create_reservation" instance="Magefine\ReserveInStore\Observer\CreateReservation" />
</event>
</config>
<?php
namespace Magefine\ReserveInStore\Observer;
use Magento\Framework\Event\Observer;
use Magento\Framework\Event\ObserverInterface;
class CreateReservation implements ObserverInterface
{
protected $reservationFactory;
protected $quoteReservationRepository; // a model that reads the quote temp table
public fonction __construct(
\Magefine\ReserveInStore\Model\ReservationFactory $reservationFactory,
\Magefine\ReserveInStore\Model\QuoteReservationRepository $quoteReservationRepository
) {
$this->reservationFactory = $reservationFactory;
$this->quoteReservationRepository = $quoteReservationRepository;
}
public fonction execute(Observer $observateur)
{
$commande = $observateur->getEvent()->getOrder();
if (!$commande) {
return;
}
$quoteId = $commande->getQuoteId();
$data = $this->quoteReservationRepository->getByQuoteId($quoteId);
if (!empty($data) && !empty($data['store_id'])) {
$reservation = $this->reservationFactory->create();
$reservation->setData([
'quote_id' => $quoteId,
'commande_id' => $commande->getId(),
'store_id' => $data['store_id'],
'status' => 'confirmed'
]);
$reservation->save();
}
}
}
Stock management and MSI
Handling stock is critical: if you allow clients to reserve items for pickup they shouldn’t be available for other buyers, otherwise you’ll oversell. Il y a two options:
- Perform a source deduction / create a reservation in Magento MSI when the commande is placed (preferred); or
- Create an internal reservation and periodically sync with stock (less safe).
With Magento 2.3+ MSI: the recommended approche when an commande is placed is to create a reservation (inventaire reservation system) or directly deduct from the source for the store if you manage per-store source stocks. Si vous don’t use MSI, you can update cataloginventaire_stock_item.
Example idea for MSI-aware approche (pseudo-code):
// In the CreateReservation observateur, after saving reservation
// 1) determine source for the chosen store
// 2) call a safe service that creates a reservation or source deduction
$sourceCode = $this->storeSourceResolver->getSourceCodeByStoreId($data['store_id']);
$items = $commande->getAllVisibleItems();
foreach ($items as $item) {
$sku = $item->getSku();
$qty = (int) $item->getQtyOrdered();
// Use Inventory APIs (InventoryReservationInterface or SourceDeductionService)
$this->inventaireService->createReservation($sku, $sourceCode, $qty, 'reserve_in_store', $commande->getIncrementId());
}
Note: the MSI API has InventoryReservationInterface and commands like InventoryUploadReservation. Exact class names may vary by Magento version; check your installed core for correct interfaces. Si vous are unsure, use the SourceDeductionService for direct source deductions. Always wrap deductions in try/catch and fail gracefully (set reservation status to failed and notify admin).
Admin actions & notifications
Admins devrait être able to confirm reservations, mark them as picked up, or cancel them. For notifications we’ll implement:
- An e-mail on reservation creation (confirmation) to the client
- A reminder e-mail a configurable number of hours before pickup_by
Create e-mail templates (view/frontend/e-mail) and use Magento\Framework\Mail\Template\TransportBuilder to send them. Example snippet to send a simple confirmation e-mail:
/** @var \Magento\Framework\Mail\Template\TransportBuilder $transportBuilder */
$transport = $this->transportBuilder
->setTemplateIdentifier('reserveinstore_confirmation_template')
->setTemplateOptions(['area' => 'frontend', 'store' => $commande->getStoreId()])
->setTemplateVars(['commande' => $commande, 'reservation' => $reservation])
->setFromByScope('general')
->addTo($commande->getCustomerEmail())
->getTransport();
$transport->sendMessage();
For reminders, create a tâche cron that runs hourly and sends reminders for reservations where pickup_by is approcheing and status is still confirmed. Cron is defined in etc/crontab.xml and the cron méthode should batch process reservations and mark when reminder is sent to avoid duplicates.
Admin UI: exemple flow
- Admin creates stores (name, address, phone, code, active)
- Orders with pickup are visible in the Reservation grid; filtres show store, status, created_at
- Admin can manually reserve stock (or check reservation failed logs) and mark reservation as ready or picked_up
- When marked picked_up, the reservation becomes complete and any temporary stock holds are finalized
Performance and caching
Click & Collect often requires real-time stock checks per store and per SKU, which peut être expensive. A poorly implemented real-time check can create DB and API bloat. Voici pragmatic recommendations:
- Debounce front-end calls. When clients type or change quantities, wait 300-500ms before hitting the server.
- Batch SKU checks. If your cart has 5 items, query availability in one request, not five separate ones.
- Use Redis cache for availability snapshots. Cache per (store, sku) with a short TTL (e.g. 30s). That gives near real-time accuracy and reduces load spikes.
- Avoid full joins on large tables for every request. Use indexed colonnes and proper resource model queries. Create small, focused read tables if necessary for fast lookups.
- When using MSI: avoid calling heavy inventaire indexeurs during client flows. Use precomputed salable_qty valeurs served via fast queries or caching layers.
- Rate-limit endpoints to avoid abuse (e.g., 10 calls per second per IP). Vous pouvez implement a simple token bucket in Redis for this.
- Use asynchronous processing for non-blocking tasks: sending e-mails or creating complex stock deductions peut être queued (Message Queue or custom tâches cron) so paiement is snappy.
Edge cases and pitfalls
- Partial expéditions: if only part of the commande est disponible at the pickup store, decide how you’ll notify the client and whether to allow partial pickup.
- Cancelled commandes and stock reconciliation: ensure cancellations release any holds or reservations.
- Mulconseille stores/one SKU: clients should only choose stores that have the items available, or you should show an estimated availability. Don’t allow selecting a store with zero stock unless you have a backcommande flow.
- Cart changes after selecting pickup: revalidate availability if quantities change and prompt client to confirm.
Monitoring and logs
Log all reservation attempts and failures. Monitor these logs and create simple tableau de bords that tell you how many reservations are pending, which stores have frequent failures, and if MSI deductions are failing. This vous aide à react fast when a source goes offline or a process fails.
Example: simple flow résumé and code mapping
Mapping from concept to fichiers (quick reference):
- Frontend UI composant: view/frontend/web/js/view/pickup.js + template knockout fichier
- Frontend contrôleurs: Controller/Index/Stores.php (return JSON), Controller/Quote/Save.php (persist pick-up selection)
- Admin UI: view/adminhtml/ui_composant/magefine_reserve_store_listing.xml, forms in view/adminhtml/ui_composant/***
- Models: Model/Store, Model/Reservation + ResourceModel
- Observer: CreateReservation (hooked to paiement_submit_all_after)
- Stock service: Service/Inventory/SourceDeductionService.php that talks with MSI interfaces
- Email templates: view/frontend/e-mail/reservation_confirmation.html
- Cron: etc/crontab.xml -> Cron/ReminderSender.php
SEO and contenu conseils for magefine.com
Parce que this contenu should attract Magento développeurs and propriétaire de boutiques, sprinkle the post with the right cléwords — "Magento 2 click and collect", "Reserve in Store Magento 2", "MSI store reservations", "Magento module development" — but keep readability first. Use code blocks for clarity (as above) and include an architecture diagram if possible in your CMS. Assurez-vous you include internal links to related Magefine pages (your extension listing or hosting pages) on the final published page, but do not link to non-existent extensions.
Testing checklist
- End-to-end: select pickup, place commande, confirm reservation created and stock adjusted
- Edge: change cart after selecting pickup — verify availability recheck
- Failure: simulate MSI API failure and verify reservation status 'failed' and admin notification
- Performance: load test the availability endpoint with cached and uncached scenarios
Tips for packaging and releasing
- Keep module composer-friendly (composer.json). Make installation with composer require magefine/reserveinstore easy.
- Provide config options in admin (Stores > Configuration > Magefine > ReserveInStore) for enabling, default pickup lead time, reminder offset, default active stores and caches TTL.
- Create DB mise à jours safely if you later change schema; keep db_schema.xml consistent for new installations and use declarative schema for mise à jours.
Final thoughts
Building a robust Reserve in Store module is an excellent way to add real entreprise valeur to a Magento store; the clé is to keep concerns separated (quote meta, reservations table), respect MSI if you use it, and design the paiement UX so it’s non-disruptive. Focus on fast lookups and caching to avoid adding load to peak times. Si vous keep these points in mind the module sera reliable and easy to support.
Si vous want, I can generate the full fichier tree and more exact code (complete contrôleurs, grille d'administrations, templates and full MSI integration) tailored to your Magento version (2.3.x, 2.4.x), or prepare a composer-ready starter repo matching this architecture.