Les moyens de paiement occupent une place centrale dans n’importe quel projet e-commerce. Aujourd’hui, de nombreuses solutions existent déjà sur le marché comme HiPay ou Lyra, et couvrent la majorité des besoins standards.
Cependant, certains projets nécessitent des comportements plus spécifiques :
- paiement sur place
- paiement différé
- paiement à la livraison
Dans ce genre de situation, les modules existants atteignent rapidement leurs limites et la création d’un mode de paiement personnalisé devient alors la meilleure solution.
L’objectif de cet article est de comprendre comment Magento structure son système de paiement et comment développer un moyen de paiement simple, propre et extensible.
Nous allons volontairement partir d’une architecture minimaliste afin de bien comprendre le rôle de chaque fichier.
1. Structure du module
Comme vu dans un précédent article, un module Magento 2 repose sur une structure standard.
Voici l’architecture finale de notre moyen de paiement :
app/code/My/Module/
├── registration.php
├── etc/
│ ├── adminhtml/
│ │ └── system.xml
│ ├── config.xml
│ ├── module.xml
│ └── payment.xml
├── Model/
│ └── Payment/
│ └── CustomPayment.php
└── view/
└── frontend/
├── layout/
│ └── checkout_index_index.xml
└── web/
├── js/
│ └── view/
│ └── payment/
│ ├── method-renderer/
│ │ └── custom-payment-method.js
│ └── custom-payment.js
└── template/
└── payment/
└── custom-payment.html
Même si cette structure peut sembler importante au premier abord, chaque fichier possède un rôle bien précis.
Nous allons maintenant détailler chacun de ces éléments.
2. Rôle des fichiers du module
system.xml — configuration back-office
Ce fichier permet d’exposer la configuration du paiement dans l’administration Magento.
<system>
<section id="payment">
<group id="custom_payment" translate="label" type="text" sortOrder="1" showInDefault="1" showInWebsite="1" showInStore="1">
<label>Custom Payment</label>
<field id="active" translate="label" type="select" sortOrder="1" showInDefault="1" showInWebsite="1" canRestore="1">
<label>Enabled</label>
<source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
</field>
<field id="order_status" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1">
<label>New Order Status</label>
<source_model>Magento\Sales\Model\Config\Source\Order\Status\NewStatus</source_model>
</field>
<field id="sort_order" translate="label" type="text" sortOrder="100" showInDefault="1" showInWebsite="1">
<label>Sort Order</label>
<frontend_class>validate-number</frontend_class>
</field>
<field id="title" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1">
<label>Title</label>
</field>
<field id="allowspecific" translate="label" type="allowspecific" sortOrder="50" showInDefault="1" showInWebsite="1" canRestore="1">
<label>Payment from Applicable Countries</label>
<source_model>Magento\Payment\Model\Config\Source\Allspecificcountries</source_model>
</field>
<field id="specificcountry" translate="label" type="multiselect" sortOrder="51" showInDefault="1" showInWebsite="1">
<label>Payment from Specific Countries</label>
<source_model>Magento\Directory\Model\Config\Source\Country</source_model>
<can_be_empty>1</can_be_empty>
</field>
<field id="min_order_total" translate="label" type="text" sortOrder="98" showInDefault="1" showInWebsite="1">
<label>Minimum Order Total</label>
<validate>validate-number validate-zero-or-greater</validate>
</field>
<field id="max_order_total" translate="label" type="text" sortOrder="99" showInDefault="1" showInWebsite="1">
<label>Maximum Order Total</label>
<validate>validate-number validate-zero-or-greater</validate>
</field>
<field id="model"></field>
</group>
</section>
</system>
Champs principaux
- active : active ou désactive le paiement
- order_status : statut de commande après paiement
- sort_order : ordre d’affichage dans le checkout
- title : libellé affiché au client
- allowspecific : restriction par pays
- specificcountry : liste des pays autorisés
- min_order_total : montant minimum
- max_order_total : montant maximum
config.xml — valeurs par défaut
Ce fichier définit les valeurs initiales du paiement :
<default>
<payment>
<sample_gateway>
<active>0</active>
<model>My\Module\Model\Payment\CustomPayment</model>
<order_status>pending</order_status>
<title>Custom Payment</title>
<can_use_checkout>1</can_use_checkout>
<group>offline</group>
</sample_gateway>
</payment>
</default>
Champs principaux
- can_use_checkout : visibilité dans le checkout
- group : catégorie du paiement (offline / online)
- model : classe PHP associée au paiement
payment.xml — déclaration du moyen de paiement
Ce fichier permet d’ajouter des comportements spécifiques à une méthode de paiement :
<payment>
<methods>
<method name="custom_payment">
<allow_multiple_address>1</allow_multiple_address>
</method>
</methods>
</payment>
Rôle de allow_multiple_address
Permet d’utiliser le paiement avec des commandes multi-adresses, chacun des produits de la commande peut être livré à une adresse différente.
CustomPayment.php — logique backend
Cette classe, déclarée dans le etc/config.xml, représente la logique backend du moyen de paiement.
Elle hérite du moteur de paiement natif de Magento et permet de centraliser le comportement du paiement.
Dans notre cas, nous y définissons principalement le code unique de la méthode de paiement afin de pouvoir le réutiliser facilement dans l’ensemble du module.
use Magento\Payment\Model\Method\Adapter as PaymentAdapter;
class CustomPayment extends PaymentAdapter
{
public const CODE = 'custom_payment';
}
Rôle
- définit le code unique du paiement
- centralise la logique backend
- permet d’étendre :
- authorize()
- capture()
- refund()
- isAvailable()
3. Intégration dans le checkout
Magento 2 utilise un checkout entièrement basé sur :
- UI Components,
- KnockoutJS,
- RequireJS.
Le paiement doit donc être enregistré côté frontend.
checkout_index_index.xml
Ce fichier injecte le paiement dans le checkout Magento via jsLayout.
<referenceBlock name="checkout.root">
<arguments>
<argument name="jsLayout" xsi:type="array">
<item name="components" xsi:type="array">
<item name="checkout" xsi:type="array">
<item name="children" xsi:type="array">
<item name="steps" xsi:type="array">
<item name="children" xsi:type="array">
<item name="billing-step" xsi:type="array">
<item name="children" xsi:type="array">
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="renders" xsi:type="array">
<!-- merge payment method renders here -->
<item name="children" xsi:type="array">
<item name="custom-payments" xsi:type="array">
<item name="component" xsi:type="string">My_Module/js/view/payment/custom-payment</item>
<item name="methods" xsi:type="array">
<item name="custom_payment" xsi:type="array">
<item name="isBillingAddressRequired" xsi:type="boolean">true</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</item>
</argument>
</arguments>
</referenceBlock>
Il permet :
- d’enregistrer le composant JS
- de définir les options du paiement
- de contrôler certains comportements (ex: adresse obligatoire)
custom-payment.js — enregistrement du paiement
Ce fichier ajoute la méthode dans la liste des paiements disponibles :
define([
'uiComponent',
'Magento_Checkout/js/model/payment/renderer-list'
], function (Component, rendererList) {
'use strict';
rendererList.push(
{
type: 'custom_payment',
component: 'My_Module/js/view/payment/method-renderer/custom-payment-method'
}
);
/** Add view logic here if needed */
return Component.extend({});
});
Magento utilise une liste globale appelée rendererList.
Elle contient tous les moyens de paiement disponibles dans le checkout.
En ajoutant notre méthode à cette liste, nous indiquons à Magento :
- le type : identifiant unique du paiement, qui doit être identique partout, représentant quel paiement afficher,
- le component : quel composant KnockoutJS utiliser pour le rendre.
custom-payment-method.js — composant principal
Ce composant représente la logique frontend du paiement.
define([
'Magento_Checkout/js/view/payment/default'
], function (Component) {
'use strict';
return Component.extend({
defaults: {
template: 'My_Module/payment/custom-payment'
},
});
});
Rôle
- hérite du système de paiement Magento
- définit le template utilisé
- peut ajouter logique métier frontend
custom-payment.html — affichage frontend
C’est le template KnockoutJS affiché dans le checkout.
<div class="payment-method" data-bind="css: {'_active': (getCode() == isChecked())}">
<div class="payment-method-title field choice">
<input type="radio"
name="payment[method]"
class="radio"
data-bind="attr: {'id': getCode()}, value: getCode(), checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()"/>
<label data-bind="attr: {'for': getCode()}" class="label"><span data-bind="text: getTitle()"></span></label>
</div>
<div class="payment-method-content">
<!-- ko foreach: getRegion('messages') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<div class="payment-method-billing-address">
<!-- ko foreach: $parent.getRegion(getBillingAddressFormName()) -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>
<p data-bind="html: getInstructions()"></p>
<div class="checkout-agreements-block">
<!-- ko foreach: $parent.getRegion('before-place-order') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
</div>
<div class="actions-toolbar">
<div class="primary">
<button class="action primary checkout"
type="submit"
data-bind="
click: placeOrder,
attr: {title: $t('Place Order')},
enable: (getCode() == isChecked()),
css: {disabled: !isPlaceOrderActionAllowed()}
"
disabled>
<span data-bind="i18n: 'Place Order'"></span>
</button>
</div>
</div>
</div>
</div>
Ce qu’il gère
- sélection du moyen de paiement
- affichage du titre
- bloc adresse de facturation
- bouton “Place Order”
- validation checkout

