Comment créer un module de commandes programmées pour les achats B2B récurrents dans Magento 2
Introduction
If your Magento 2 store handles B2B clients who buy regularly — think office supplies, chemicals, or food distribution — building a custom "Scheduled Order" module can save time, lower churn, and improve retention. In this post I’ll walk you through a practical approche to implement a recurring scheduled-commande system in Magento 2: architecture, tâches cron, payment automation, admin UI, and client experience. I’ll keep it relaxed and practical, with étape-by-étape code exemples you can adapt.
What this module does (brief)
- Create and store subscription-like scheduled commandes (not a subscription for contenu, but scheduled B2B purchases).
- Run tâches cron to generate actual commandes at the right time.
- Charge clients automatically using saved payment tokens / vault.
- Provide admin UI to manage schedules and a commerçant-friendly commande log.
- Provide client-facing controls to change frequency, pause or cancel schedules.
High-level architecture
Think of the module as four layers:
- Data model: entities to store scheduled commande templates and related metadata.
- Scheduler: tâches cron that inspect the schedule table and generate real sales commandes.
- Payment integration: using Magento payment tokens (vault) or secure payment provider APIs for automatic capture.
- Interfaces: Admin panel for commerçants, and compte client UI for buyers to manage frequency and items.
Entities
Key database entities:
- scheduled_commande_template — the blueprint for creating commandes (client, items, frequency, next_run_at, status)
- scheduled_commande_history — record of generated commandes (template_id, commande_id, executed_at, result, payment_reference)
Keep the model small and query-friendly so the cron can run efficiently on large volumes.
Module skeleton and registration
Create a standard module namespace e.g. MageFine/ScheduledOrder. Minimal fichiers:
app/code/MageFine/ScheduledOrder/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(
ComponentRegistrar::MODULE,
'MageFine_ScheduledOrder',
__DIR__
);
app/code/MageFine/ScheduledOrder/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_ScheduledOrder" setup_version="1.0.0" />
</config>
Run bin/magento setup:mise à jour after adding fichiers.
Database schema (install schema / declarative)
Use declarative schema (db_schema.xml). Minimal schema sketch:
app/code/MageFine/ScheduledOrder/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_scheduled_order_template" resource="default" engine="innodb" comment="Scheduled order templates">
<column xsi:type="int" name="template_id" nullable="false" identity="true" unsigned="true" comment="Template ID"/>
<column xsi:type="int" name="customer_id" nullable="false" unsigned="true" comment="Customer ID"/>
<column xsi:type="text" name="items_json" nullable="false" comment="Items JSON"/>
<column xsi:type="varchar" name="frequency" nullable="false" length="50" comment="Frequency (e.g. weekly, monthly)"/>
<column xsi:type="timestamp" name="next_run_at" nullable="true" on_update="false" comment="Next run"/>
<column xsi:type="varchar" name="status" nullable="false" length="50" default="active" comment="active|paused|cancelled"/>
<column xsi:type="varchar" name="payment_token" nullable="true" length="255" comment="Vault token or gateway reference"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="template_id"/>
</constraint>
</table>
<table name="magefine_scheduled_order_history" resource="default" engine="innodb" comment="Scheduled order history">
<column xsi:type="int" name="history_id" nullable="false" identity="true" unsigned="true"/>
<column xsi:type="int" name="template_id" nullable="false" unsigned="true"/>
<column xsi:type="int" name="order_id" nullable="true" unsigned="true"/>
<column xsi:type="timestamp" name="executed_at" nullable="false" on_update="false"/>
<column xsi:type="varchar" name="status" length="50" nullable="false"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="history_id"/>
</constraint>
</table>
</schema>
Design note: store items as JSON or a separate child table. JSON is quick to prototype; a child table is better for complex queries.
Cron architecture — scheduling and processing
Replace naive loops with batched processing and carefully handle failures. Cron responsibilities:
- Find templates where next_run_at <= now and status = active.
- Lock each template to avoid double-processing (use DB flag or optimistic locking).
- Create a cart/commande programmatically for the client.
- Charge using vault token or create payment intent with stored instrument.
- Record history and compute next_run_at basé sur frequency.
Define tâche cron
app/code/MageFine/ScheduledOrder/etc/crontab.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Cron:etc/crontab.xsd">
<group id="default">
<job name="magefine_scheduledorder_run" instance="MageFine\ScheduledOrder\Cron\ProcessScheduledOrders" method="execute">
<schedule>*/5 * * * *</schedule>
</job>
</group>
</config>
Cron implémentation—clé points
Keep the cron class small and delegate work to service classes:
app/code/MageFine/ScheduledOrder/Cron/ProcessScheduledOrders.php
<?php
namespace MageFine\ScheduledOrder\Cron;
class ProcessScheduledOrders
{
private $templateRepository;
private $processor;
public function __construct(
\MageFine\ScheduledOrder\Api\TemplateRepositoryInterface $templateRepository,
\MageFine\ScheduledOrder\Model\Processor $processor
) {
$this->templateRepository = $templateRepository;
$this->processor = $processor;
}
public function execute()
{
$templates = $this->templateRepository->getDueTemplates(100); // batch size
foreach ($templates as $template) {
try {
$this->processor->processTemplate($template);
} catch (\Exception $e) {
// log, mark history as failed, notify admin if repeated errors
}
}
}
}
Processor flow
Processor responsibilities:
- Acquire lock for the template (for concurrency safety).
- Build quote for client (set client, items, addresses, méthode de livraison).
- Process payment using saved token via Payment Gateway / Magento Vault.
- Create commande, factures, expéditions if needed.
- Update next_run_at and create history record.
// Pseudocode for Model/Processor
public function processTemplate($template) {
if (!$this->lock($template)) return; // another process got it
$quote = $this->quoteService->createQuoteForCustomer($template->getCustomerId());
$this->quoteService->addItemsFromTemplate($quote, $template->getItemsJson());
$this->quoteService->setShippingAndBilling($quote, $template);
// Use payment token if available
$paymentResult = $this->paymentService->chargeUsingToken($quote, $template->getPaymentToken());
if ($paymentResult->isSuccess()) {
$order = $this->orderService->submitQuote($quote);
$this->historyRepo->createSuccess($template->getId(), $order->getId(), $paymentResult->getReference());
$this->templateRepo->updateNextRunAt($template, $this->calculateNextRun($template));
} else {
$this->historyRepo->createFailure($template->getId(), $paymentResult->getMessage());
// Optionally try to notify customer and admin
}
$this->unlock($template);
}
Note: keep payment capture and commande creation atomic where possible. If capture happens via gateway webhooks you may create a pending commande and finalize it on successful capture callback.
Payment integration and secure automatic payments
Security and PCI compliance are essential. The recommended approche is:
- Use the Magento Vault (Payment tokenization) so you don’t store card data in your DB.
- If using a gateway that supports tokenization and recurring billing (Stripe, Adyen, Authorize.net, Braintree), use the gateway’s token and charge APIs via the payment token saved in vault.
How to use vault tokens
On paiement, enable saving méthode de paiement to vault for eligible clients. Save the vault token or payment instrument id in scheduled_commande_template.payment_token. At cron time, call the passerelle de paiement charge API using the token.
// Simplified PaymentService::chargeUsingToken
public function chargeUsingToken(\Magento\Quote\Model\Quote $quote, $vaultToken) {
// Build payment request according to gateway SDK
// For example: $gatewayClient->charge(['token' => $vaultToken, 'amount' => $quote->getGrandTotal(), 'currency' => $quote->getCurrencyCode()]);
// Handle responses and translate into local result object
}
Two strategies for commande creation & capture:
- Capture first, then create commande: call gateway charge; on success, create a paid commande in Magento and mark as facture created.
- Create commande then capture: place commande as pending_payment, attempt capture; update statut de commande basé sur gateway response and webhook callbacks.
Which to choose dépend de your gateway and webhook reliability. For B2B, capturing first is often easier to reconcile because you only create an commande when payment succeeded.
Failure handling and retry stratégie
If a payment fails:
- Record the failure and increment an attempt counter.
- Try a configurable retry policy: e.g. retry after 1 day, 3 days, then pause and notify admin/client.
- Consider dunning flow: automated e-mails to update méthode de paiement or notify about failed charge.
Admin Interface: managing scheduled commandes
Admin should have a clear UI to manage templates and avis execution history. Key screens:
- Grid listing scheduled templates (filtres: client, status, next_run_at).
- Edit form: change frequency, next_run_at, payment token selection, pause/cancel actions.
- History tab to view generated commandes and statuses.
Admin menu and ACL
app/code/MageFine/ScheduledOrder/etc/adminhtml/menu.xml
<menu xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
<add id="MageFine_ScheduledOrder::main" title="Scheduled Orders" module="MageFine_ScheduledOrder" sortOrder="300" parent="Magento_Backend::sales" action="magefine_scheduledorder/template/index" resource="MageFine_ScheduledOrder::manage" />
</menu>
app/code/MageFine/ScheduledOrder/etc/acl.xml
<acl>
<resources>
<resource id="Magento_Backend::admin">
<resource id="MageFine_ScheduledOrder::manage" title="Manage Scheduled Orders" />
</resource>
</resources>
</acl>
Admin grid and edit form
Use UI composants for the grid and form. Columns: template_id, client_name, frequency, next_run_at, payment_méthode, status, actions. The edit form should allow commerçants to directly trigger a manual run or cancel future runs.
// Minimal UI component xml snippets omitted for brevity — implement a standard uiComponent grid and form.
Customer experience: frontend management
Allow B2B clients to:
- Create a schedule when placing an commande (checkbox “Schedule this commande”), pick frequency, and next run date.
- Edit schedule in the account tableau de bord: change frequency, items, pause/cancel or change méthode de paiement.
- View upcoming and past scheduled expéditions and factures.
Creating schedules at paiement
Add a simple form champ at paiement or in a quick-recommande flow. Example quick UI:
<div class="scheduled-order-option">
<input type="checkbox" name="scheduled" id="scheduled" />
<label for="scheduled">Create a scheduled order</label>
<select name="frequency">
<option value="weekly">Weekly</option>
<option value="biweekly">Bi-weekly</option>
<option value="monthly">Monthly</option>
</select>
<input type="date" name="next_run_at" />
</div>
On submit, create a scheduled template for the client au lieu de (or in addition to) creating a normal commande depending on entreprise rules. For B2B repeat buys you often want both: create an immediate commande and create a schedule for future commandes.
Editing schedules in My Account
Create a simple grid in My Account that lists templates and offers actions: Edit, Pause, Resume, Cancel, Run Now. When editing items, you can either let the change apply to next runs only or optionally update existing future expéditions.
Frequency rules and next run calculation
Keep frequency logic extensible. Implement frequency strategies:
- Fixed interval (e.g., every 7 days)
- One-month increments (handle month lengths carefully)
- Custom cron-like expressions for advanced clients
// Example simple next-run calculator
switch ($frequency) {
case 'weekly':
$next = (new \DateTime($current))->modify('+7 days');
break;
case 'biweekly':
$next = (new \DateTime($current))->modify('+14 days');
break;
case 'monthly':
$next = (new \DateTime($current))->modify('+1 month');
break;
}
Edge cases: end-of-month behavior, timezones (store timestamps as UTC) and daylight saving. Always store runs in UTC and convert for display.
Concurrency and locking strategies
If crons run on mulconseille servers you must avoid double-processing. Options:
- DB-level lock: update a processing flag with WHERE status='active' AND next_run_at <= now AND processing=0; then set processing=1 in one atomic update and check affected lignes.
- Use SELECT ... FOR UPDATE in a transaction.
- Leverage Redis or another distributed lock mechanism if you already have one in your stack.
// Example atomic update technique
UPDATE magefine_scheduled_order_template
SET processing = 1
WHERE template_id = :id AND processing = 0 AND status = 'active' AND next_run_at <= :now
Logging, notifications and observability
Visibility is clé for B2B operations. Implement:
- History records for each execution.
- Admin e-mail alerts for repeated failures per client/template.
- Dashboards or counters (failed attempts, upcoming runs) in admin.
- Structured logs (JSON) for cron runs so you can ingest them into log aggregation tools.
Testing and déploiement conseils
Test carefully — scheduled systems can create a lot of financial impact if broken.
- Unit tests for frequency logic and template creation.
- Integration tests for cron behavior (simulate time and token responses).
- Use a sandbox gateway for payment tests and never test real card data in dev environments.
- Deploy with fonctionnalité flags so you can enable the scheduler per environment and gradually turn it on.
Practical code snippets: create quote & place commande programmatically
Here’s a compact exemple showing the main étapes to create a quote for a client and convert it to an commande. This assumes DI and proper service objets — consider this pseudocode to adapt.
// 1) Create quote for customer
$quote = $this->quoteFactory->create();
$quote->setStore($store);
$quote->assignCustomer($customer); // sets group, addresses
// 2) Add items
foreach ($items as $itemData) {
$product = $this->productRepository->getById($itemData['product_id']);
$quote->addProduct($product, intval($itemData['qty']));
}
// 3) Set addresses, shipping method
$quote->getBillingAddress()->addData($billingData);
$quote->getShippingAddress()->addData($shippingData);
$quote->getShippingAddress()->setCollectShippingRates(true)->collectShippingRates();
$quote->getShippingAddress()->setShippingMethod('freeshipping_freeshipping');
// 4) Set payment
$quote->getPayment()->setMethod('vault'); // your custom method that uses token
$quote->setTotalsCollectedFlag(false)->collectTotals();
// 5) Submit quote
$order = $this->cartManagement->submit($quote);
Real implémentation must adapt to règle fiscales, custom prix adjustments, tier tarification, and negotiated B2B tarification.
Security and compliance notes
Handle payment tokens and client data carefully:
- Never store raw card numbers.
- Use Magento Vault to keep PCI surface minimal.
- Review GDPR and local data retention laws for storing client commande history.
- Audit admin actions on schedules (who changed frequency or payment token).
Performance considerations
When dozens of thousands of scheduled templates exist, performance matters:
- Process in batches and paginate queries.
- Precompute criteria where possible (e.g., indexes on next_run_at and status).
- Use asynchronous jobs for non-critical post-processing (e.g., genenote PDF factures).
Example end-to-end flow (concrete)
Let’s walk through one real flow for clarity:
- Customer places a one-off commande and checks "Create scheduled commande" with frequency monthly.
- At paiement the system creates a normal commande and also creates a scheduled_commande_template with next_run_at = 30 days from now and stores the vault token.
- Cron runs every 5 minutes and picks up templates with next_run_at <= now.
- Processor builds a quote basé sur template and tries to charge via token. It attempts capture via the gateway SDK.
- If capture succeeds, the processor creates a paid commande, records history, and sets next_run_at += 1 month. If it fails, history records the erreur and the template's attempt_count increments.
- Après 3 failed attempts, the template is paused and an e-mail to the client and the site admin is sent to update méthode de paiement.
Extensibility ideas
Une fois you have a basic system, you can add:
- Advanced frequency rules (N-th entreprise day of month).
- Support for bundled expéditions and partial expéditions.
- Integration with ERP for automatic PO creation.
- Bulk import of templates from CSV for onboarding large B2B clients.
Common pitfalls and comment avoid them
- Double charging due to concurrent cron — use locks.
- Payment tokens expired — implement graceful retries and notifications.
- Time zone problèmes — store UTC, display local time.
- Missing taxes / incorrect tarification on regenerated commandes — always collect totals and load current catalog prixs if desired.
Résumé and next étapes
Building a custom Scheduled Order module in Magento 2 is a rewarding project for B2B commerçants. The core fonctionnalités are straightforward: modèle de données, cron-driven processor, payment token usage, admin and client UIs. The hard parts are payment reliability, concurrency, and edge-case frequency rules.
If you’re implementing this in production, start small: implement templates, a basic cron, and integrate with a sandbox gateway. Add admin and client UIs next and iterate on failure handling and observability.
Si vous want, I can help with a starter repository layout, scaffold the UI composants, or draft the processor class and payment integration for your specific gateway (Stripe, Adyen, Braintree, etc.).
Thanks for reading — hope this vous aide à get the module running quickly for your B2B flows. Ping me if you want code tailored to your store’s setup.
Keywords: Magento 2 Scheduled Order, recurring purchases, B2B scheduled commandes, MageFine, Magento Vault, tâches cron