Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A payment module

A payment method in PrestaShop is a separate module. It does two things: shows its option at the payment step and, once the customer has chosen it, turns the cart into an order. A payment module extends not Module but PaymentModule — it already has the method that creates an order.

Reviews have nothing to do with this, so the example is a separate acmepay module with payment on delivery.

Structure

modules/acmepay/
├── acmepay.php
└── controllers/
    └── front/
        └── validation.php

The main class

<?php
// modules/acmepay/acmepay.php
use PrestaShop\PrestaShop\Core\Payment\PaymentOption;
 
if (!defined('_PS_VERSION_')) {
    exit;
}
 
class AcmePay extends PaymentModule
{
    public function __construct()
    {
        $this->name = 'acmepay';
        $this->tab = 'payments_gateways';
        $this->version = '1.0.0';
        $this->author = 'Acme';
        $this->currencies = true;
        $this->currencies_mode = 'checkbox';
 
        parent::__construct();
 
        $this->displayName = $this->trans('Payment on delivery', [], 'Modules.Acmepay.Admin');
    }
 
    public function install(): bool
    {
        return parent::install() && $this->registerHook('paymentOptions');
    }
}

What sets the class apart from an ordinary module is the PaymentModule parent, the payments_gateways section and two currency properties: they say the payment method can be enabled for each currency separately.

The payment option

At the payment step the engine calls the paymentOptions hook and collects a list of options from modules:

// modules/acmepay/acmepay.php
public function hookPaymentOptions(array $params): array
{
    $option = new PaymentOption();
    $option->setCallToActionText($this->trans('Pay on delivery', [], 'Modules.Acmepay.Shop'));
    $option->setAction($this->context->link->getModuleLink($this->name, 'validation', [], true));
 
    return [$option];
}

setCallToActionText() sets the option caption, setAction() sets the address the "Place order" button sends the customer to. Here it is the module's own validation controller.

Install the module and go to the payment step in the front office: the "Pay on delivery" option is in the list.

php bin/console prestashop:module install acmepay

Creating the order

<?php
// modules/acmepay/controllers/front/validation.php
class AcmepayValidationModuleFrontController extends ModuleFrontController
{
    public function postProcess(): void
    {
        $cart = $this->context->cart;
        $customer = new Customer((int) $cart->id_customer);
 
        if (!$cart->id || !Validate::isLoadedObject($customer) || !$this->module->active) {
            Tools::redirect($this->context->link->getPageLink('order'));
        }
 
        $this->module->validateOrder(
            (int) $cart->id,
            (int) Configuration::get('PS_OS_PREPARATION'),
            (float) $cart->getOrderTotal(true, Cart::BOTH),
            $this->module->displayName,
            null,
            [],
            (int) $this->context->currency->id,
            false,
            $customer->secure_key
        );
 
        Tools::redirect($this->context->link->getPageLink('order-confirmation', true, null, [
            'id_cart' => (int) $cart->id,
            'id_module' => (int) $this->module->id,
            'id_order' => (int) $this->module->currentOrder,
            'key' => $customer->secure_key,
        ]));
    }
}

validateOrder() creates an order from the cart, decreases stock, sends an email to the customer and calls the actionValidateOrder hook. Then the customer is redirected to the standard "Order confirmed" page. The order appears in the "Orders" section of the back office.

Arguments of validateOrder

ArgumentWhat is passed
Cart id$cart->id
Order statusAn id from the settings: PS_OS_PAYMENT — paid, PS_OS_PREPARATION — processing, PS_OS_ERROR — payment error
AmountHow much was paid. If it differs from the cart total, the order gets the error status
Method nameA string saved in the order
Currency idThe payment currency
Customer keysecure_key — protection against cart substitution

Paying on an external service

With card payment the customer is sent to the bank's page, and the flow gets longer:

  1. setAction() leads to a controller that creates a payment at the bank and redirects the customer there.
  2. The bank reports the result with a request to a separate module controller — a notification.
  3. The notification controller checks the request signature and only then calls validateOrder() with the PS_OS_PAYMENT status.

The page after the order

To show its own instructions on the confirmation page — bank details, for example — a module subscribes to the displayPaymentReturn hook and returns a template like any display hook.

Common mistake

The order is created in the controller the customer returns to from the bank, trusting a parameter like status=paid in the address. Such an address can be opened by hand to get a "paid" order. An order is confirmed only on the bank's notification with a verified signature, and the amount is taken from the notification and compared with the cart.

Thanks for reading!

Donate to the site — we work hard for you.