Как создать расширение OpenCart для отправки SMS
Представим ситуацию. Где-то на просторах нашей страны есть благородный негоциант XXI века. У него много светлых идей, но мало золотых дублонов в электронном кошельке. Наш предприниматель выбрал для себя один из самых популярных бесплатных движков интернет-магазина в России OpenCart.
И вроде всё идет хорошо, но хочется добавить новых функций, например, возможность отправки SMS пользователям после оформления заказа, да так, чтобы не покупать платные модули.
Наш герой начинает искать информацию о том, как доработать свой интернет-магазин. Мы с вами не можем оставить человека в беде. Поэтому я написал туториал, как создать собственное расширение для OpenCart 4 и реализовать в нём вызов стороннего API.
Разберу пример отправки SMS через API МТС Exolve, но в принципе материалы статьи с небольшими доработками подойдут для вызова любого другого REST API.
Оглавление:
Ставим задачу
Сегодня мы напишем расширение для следующего сценария:
Пользователь на странице checkout успешно создал заказ;
Магазин вызвал API МТС Exolve;
МТС Exolve отправил SMS уведомление пользователю.
Ориентироваться будем на самую последнюю на момент подготовки статьи версию OpenCart 4.0.2.3.
Начиная с версии 2.2+ в OpenCart применяется система событий, которая позволяет дополнить или заменить логику обработки действий пользователя или администратора интернет-магазина. Проблема в том, что на мой субъективный взгляд, для новичка в разработке под OpenCart официальная документация по данной теме оставляет больше вопросов, чем ответов. Именно поэтому я решил набросать простой пример разработки своего собственного модуля.
Модуль получился вполне работоспособным. Вы можете его скачать с GitHub, установить свои настройки МТС Exolve и смело использовать.
Готовимся
OpenCart
Мы не будем рассматривать процесс установки OpenCart. Я использовал предложенный хостинг-провайдером OpenCart 4.0.2.3, на PHP 8.1, без установки русификатора и других расширений.
МТС Exolve API
Для того, чтобы отправлять SMS, выполните следующие действия:
Зарегистрируйтесь на сайте МТС Exolve. И получите бесплатно 300 рублей, для тестирования. Для проверки и отладки расширения нам хватит «за глаза»
Подтвердите номер телефона.
Создайте приложение.
Получите API Token.
Приобретите стартовый номер.
Я уже кратко разбирал флоу настройки МТС Exolve в статье про отправку SMS в процессе сборки приложения в GitHub. Детально с процессом настройки ЛК и созданием приложения можно ознакомится в официальной документации.
Пишем код
Пришло время традиционного дисклеймера. Я не программист, и с OpenCart я знаком поверхностно. Многие решения я позаимствовал из чужих материалов и по аналогии адаптировал под себя. Это базовый концепт, который вы можете доработать для решения других задач.
Структура расширения (модуля)
В общем случае структура расширения для Opencart 4 основана на парадигме Model View Controller (MVC). В некоторых публикациях еще добавляют букву l – Language.
Обычно функции расширения делятся на две составляющие:
Админ панель – папка admin;
Пользовательская часть интернет-магазина – папка catalog.
В некоторых случаях в расширении могут встретиться ещё разделы system и image.
Если говорить упрощённо, то в каждом из этих разделов могут быть свои ключевые папки:
controller – основная бизнес-логика;
model – слой работы с данными (запросы к базе данных);
view – слой представления, шаблоны отображения страниц и блоков.
language – перевод на разные языки.
Отдельно отметим лежащий в корне расширения файл install.json, в котором хранится информация о приложении и его авторах.
В нашем случае набор папок будет неполным.
Во-первых, мы нигде не работаем напрямую с базой данных, поэтому у нас не будет папок model.
Во-вторых, в пользовательской части нам не надо ничего отображать, мы только отправляем запрос к API МТС Exolve. Поэтому нам нужно только реализовать controller.
Вот как выглядит структура нашего расширения:
Код для админ-панели
Пришло время писать код. Но прежде, уточню, что я не погружался глубоко в особенности разработки для OpenCart 4. Некоторые вещи я позаимствовал по аналогии и не на 100% понимаю, как они работают.
Ознакомится с исходным кодом можно на GitHub.
Наш модуль будет называться opc send_sms. В данном случае это родительская корневая папка, в которой лежит весь код. Включать её в структуру расширения не надо. Но название папки мы используем, когда будем формировать архив с расширением opc_send_sms.ocmod.zip.
Контроллер
Создадим файл контроллера админ-панели по адресу:
/admin/controller/module/send_sms.php
Полный код файла под спойлером
Развернуть код
<?php
/**
* Extension name: Send SMS
* Descrption: Using this extension we will a send sms via MTS Exolve API after make order.
* Author: BosonBeard.
*
*/
namespace Opencart\Admin\Controller\Extension\OpcSendSms\Module;
use \Opencart\System\Helper AS Helper;
class SendSMS extends \Opencart\System\Engine\Controller {
/**
* index
*
* @return void
*/
public function index(): void {
$this->load->language('extension/opc_send_sms/module/send_sms');
$this->document->setTitle($this->language->get('heading_title'));
$data['breadcrumbs'] = [];
$data['breadcrumbs'][] = [
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard','user_token='
.$this->session->data['user_token'])
];
$data['breadcrumbs'][] = [
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension','user_token='
.$this->session->data['user_token'] . '&type=module')
];
if (!isset($this->request->get['module_id'])) {
$data['breadcrumbs'][] = [
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/opc_send_sms/module/send_sms','user_token='.
$this->session->data['user_token'])
];
} else {
$data['breadcrumbs'][] = [
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/opc_send_sms/module/send_sms','user_token='.
$this->session->data['user_token'] . '&module_id=' . $this->request->get['module_id'])
];
}
// configuration save URL
$data['save'] = $this->url->link('extension/opc_send_sms/module/send_sms.save', 'user_token=' . $this->session->data['user_token']);
// back to previous page URL
$data['back'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=module');
// getting settings fields from extension configuration
$data['module_opc_send_sms_status'] = $this->config->get('module_opc_send_sms_status');
$data['module_opc_send_sms_token'] = $this->config->get('module_opc_send_sms_token');
$data['module_opc_send_sms_phone'] = $this->config->get('module_opc_send_sms_phone');
$data['module_opc_send_sms_text'] = $this->config->get('module_opc_send_sms_text');
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/opc_send_sms/module/send_sms', $data));
}
/**
* save method
*
* @return void
*/
public function save(): void {
$this->load->language('extension/opc_send_sms/module/send_sms');
$json = [];
if (!$this->user->hasPermission('modify', 'extension/opc_send_sms/module/send_sms')) {
$json['error']['warning'] = $this->language->get('error_permission');
}
if (!isset($this->request->post['module_opc_send_sms_token'])) {
$json['error']['api-key'] = $this->language->get('error_api_token');
}
if (!isset($this->request->post['module_opc_send_sms_phone'])) {
$json['error']['api-key'] = $this->language->get('error_api_phone');
}
if (!$json) {
$this->load->model('setting/setting');
// saving configuration
$this->model_setting_setting->editSetting('module_opc_send_sms_status', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_token', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_phone', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_text', $this->request->post);
$json['success'] = $this->language->get('text_success');
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}
/**
* install method
*
* @return void
*/
public function install() {
// registering events to show menu
$this->__registerEvents();
}
/**
* __registerEvents
*
* @return void
*/
protected function __registerEvents() {
// check_event
$this->load->model('setting/event');
if ($this->model_setting_event->getEventByCode('SendCheckoutSmsMtsExolve')) {
// The event exists, delete older version.
$this->model_setting_event->deleteEventByCode('SendCheckoutSmsMtsExolve');
}
// events array
$events = array();
$events[] = array(
'code' => "SendCheckoutSmsMtsExolve",
'trigger' => "catalog/model/checkout/order/addHistory/before",
'action' => "extension/opc_send_sms/event/event",
'description' => "Send SMS after checkout via MTS Exolve",
'status' => 1,
'sort_order' => 0,
);
// loading event model
$this->load->model('setting/event');
foreach($events as $event){
// registering events in DB
$this->model_setting_event->addEvent($event);
}
}
}
Подробнее остановимся на некоторых моментах.
namespace Opencart\Admin\Controller\Extension\OpcSendSms\Module;
Название namespace. Я точно не нашел информации почему так, но корректно расширение работает, только если указать всё в PascalCase. В нашем случае название модуля opc_send_sms, чтобы перевести его в PascalCase уберем все символы «_» и каждое новое слово начнем с большой буквы. Получим OpcSendSms.
Фрагмент кода от начала функции public function index(): void до «// getting settings fields from extension configuration»
— типовая логика расширения. Она отвечает за формирование заголовка, «хлебных крошек», логики возврата на прошлую страницу и т.п. Если вы будете писать свой модуль, вам скорее всего будет достаточно поменять адреса на свои, без глубокого погружения в логику.
Отдельно рассмотрим блок:
// getting settings fields from extension configuration
$data['module_opc_send_sms_status'] = $this->config->get('module_opc_send_sms_status');
$data['module_opc_send_sms_token'] = $this->config->get('module_opc_send_sms_token');
$data['module_opc_send_sms_phone'] = $this->config->get('module_opc_send_sms_phone');
$data['module_opc_send_sms_text'] = $this->config->get('module_opc_send_sms_text');
Он отвечает за заполнение этих полей на странице редактирования настроек модуля.
В методе public function save(): void
, который отвечает за обработку нажатия на кнопку «Сохранить» рассмотрим блок обработки ошибок:
if (!$this->user->hasPermission('modify', 'extension/opc_send_sms/module/send_sms')) {
$json['error']['warning'] = $this->language->get('error_permission');
}
if (!isset($this->request->post['module_opc_send_sms_token'])) {
$json['error']['api-key'] = $this->language->get('error_api_token');
}
if (!isset($this->request->post['module_opc_send_sms_phone'])) {
$json['error']['api-key'] = $this->language->get('error_api_phone');
}
В первом блоке if проверяем, есть ли у пользователя права на изменение настроек расширения.
Во втором блоке проверяем заполнение поля API key.
В третьем проверяем, заполнен ли телефон МТС Exolve, с которого будет отправлено SMS.
В случае ошибки мы показываем сообщение из файла в папке language.php. Но об этом немного позже.
Также если вы будете писать своё расширение, то не забудьте по аналогии реализовать сохранение заполненных полей.
$this->model_setting_setting->editSetting('module_opc_send_sms_status', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_token', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_phone', $this->request->post);
$this->model_setting_setting->editSetting('module_opc_send_sms_text', $this->request->post);
И последнее, что стоит рассмотреть в коде контроллера – это регистрация события с отправкой SMS.
По-хорошему, вы можете зарегистрировать события, вызвав метод addEvent
из любого файла в контексте OpenCart 4. Но само собой, так делать не стоит, как минимум, чтобы не плодить дубли сообщений в системе.
Поэтому мы будем регистрировать новое сообщение в момент установки расширения при вызове метода __registerEvents()
.
Вначале мы проверяем, есть ли уже в системе событие SendCheckoutSmsMtsExolve
. Если есть, то удаляем, устраняя дубли.
protected function __registerEvents() {
// check_event
$this->load->model('setting/event');
if ($this->model_setting_event->getEventByCode('SendCheckoutSmsMtsExolve')) {
// The event exists, delete older version.
$this->model_setting_event->deleteEventByCode('mSendCheckoutSmsMtsExolve');
}
Далее регистрируем массив событий. Но в нашем случае он состоит всего из одного элемента.
$events[] = array(
'code' => "SendCheckoutSmsMtsExolve_",
'trigger' => "catalog/model/checkout/order/addHistory/before",
'action' => "extension/opc_send_sms/event/event",
'description' => "Send SMS after checkout via MTS Exolve",
'status' => 1,
'sort_order' => 0,
);
Он отвечает за то, что мы увидим на странице событий.
Перевод
Теперь перейдем к языковому файлу.
admin/language/en-gb/module/send_sms.php
Полный код под спойлером.
Развернуть код
<?php
// Heading
$_['heading_title'] = 'MTS Exolve send SMS';
// Text
$_['text_extension'] = 'Extensions';
$_['text_success'] = 'Success: You have modified show menu module!';
$_['text_edit'] = 'Edit Show Menu Module';
// Entry
$_['entry_status'] = 'Status';
$_['entry_api_token'] = 'MTS Exolve API Key';
$_['entry_api_phone'] = 'MTS Exolve sender phone';
$_['entry_text'] = 'SMS text template';
// Error
$_['error_permission'] = 'Warning: You do not have permission to modify this module!';
$_['error_api_token'] = 'MTS Exolve API Key Required';
$_['error_api_phone'] = 'MTS Exolve sender phone Required';
Помните в методе save()
контроллера было сообщение об ошибке $this->language->get('error_api_phone')
. Текст для него мы возьмем как раз из этого файла. В данном случае: «MTS Exolve sender phone Required».
Осталось рассмотреть файл представления, или по-другому шаблона страницы.
Представление
admin/view/template/module/send_sms.twig
Полный код под спойлером.
Развернуть код
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="float-end">
<button type="submit" form="form-module" data-bs-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary">
<i class="fa-solid fa-save"></i>
</button>
<a href="{{ back }}" data-bs-toggle="tooltip" title="{{ button_back }}" class="btn btn-light">
<i class="fa-solid fa-reply"></i>
</a>
</div>
<h1>{{ heading_title }}</h1>
<ol class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li class="breadcrumb-item">
<a href="{{ breadcrumb.href }}">{{ breadcrumb.text }}</a>
</li>
{% endfor %}
</ol>
</div>
</div>
<div class="container-fluid">
<div class="card">
<div class="card-header">
<i class="fa-solid fa-pencil"></i> {{ text_edit }}
</div>
<div class="card-body">
<form id="form-module" action="{{ save }}" method="post" data-oc-toggle="ajax">
<div class="row mb-3">
<label for="input-status" class="col-sm-2 col-form-label">
{{ entry_status }}
</label>
<div class="col-sm-10">
<div class="form-check form-switch form-switch-lg">
<input type="hidden" name="module_opc_send_sms_status" value="0"/>
<input type="checkbox" name="module_opc_send_sms_status" value="1"
id="input-status" class="form-check-input"{% if module_opc_send_sms_status %} checked{% endif %}/>
</div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-api_token">{{ entry_api_token }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_api_token }}"
name="module_opc_send_sms_token" value="{{ module_opc_send_sms_token }}">
<div id="error-api-token" class="invalid-feedback"></div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-api_phone">{{ entry_api_phone }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_api_phone }}"
name="module_opc_send_sms_phone" value="{{ module_opc_send_sms_phone }}">
<div id="error-api-phone" class="invalid-feedback"></div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-text">{{ entry_text }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_text }}"
name="module_opc_send_sms_text" value="{{ module_opc_send_sms_text }}">
<p>Use %order_id% to place varible. <br> E.g. Your order %order_id% created.</p>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{ footer }}
Этот файл отвечает за то, как выглядит страница настроек модуля в админ-панели. Мы уже видели её, когда исследовали контроллер.
<form id="form-module" action="{{ save }}" method="post" data-oc-toggle="ajax">
<div class="row mb-3">
<label for="input-status" class="col-sm-2 col-form-label">
{{ entry_status }}
</label>
<div class="col-sm-10">
<div class="form-check form-switch form-switch-lg">
<input type="hidden" name="module_opc_send_sms_status" value="0"/>
<input type="checkbox" name="module_opc_send_sms_status" value="1"
id="input-status" class="form-check-input"{% if module_opc_send_sms_status %} checked{% endif %}/>
</div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-api_token">{{ entry_api_token }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_api_token }}"
name="module_opc_send_sms_token" value="{{ module_opc_send_sms_token }}">
<div id="error-api-token" class="invalid-feedback"></div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-api_phone">{{ entry_api_phone }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_api_phone }}"
name="module_opc_send_sms_phone" value="{{ module_opc_send_sms_phone }}">
<div id="error-api-phone" class="invalid-feedback"></div>
</div>
</div>
<div class="row mb-3 required">
<label class="col-sm-2 col-form-label" for="input-text">{{ entry_text }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" placeholder="{{ entry_text }}"
name="module_opc_send_sms_text" value="{{ module_opc_send_sms_text }}">
<p>Use %order_id% to place varible. <br> E.g. Your order %order_id% created.</p>
</div>
</div>
</form>
Этот фрагмент кода отвечает за поля, в которые вводим данные. Если вы решите писать свое расширение, то скорее всего именно этот блок вам предстоит изменить в первую очередь.
Код для пользовательской части
Как я уже говорил, непосредственно в пользовательской части интернет-магазина мы не реализуем каких-то компонентов, требующих графического отображения или изменения структуры базы данных. Поэтому всё, что нам нужно это реализовать в контроллере логику для обработки сообщения SendCheckoutSmsMtsExolve, которое мы ранее зарегистрировали в админ-панели.
Событие будет срабатывать после успешного оформления заказа.
Создайте файл:
catalog/controller/event/event.php
Полный код под спойлером.
Развернуть код
<?php
/**
* Extension name: Send SMS
* Descrption: Using this extension we will a send sms via MTS Exolve API after make order.
* Author: BosonBeard.
*
*/
namespace Opencart\Catalog\Controller\Extension\OpcSendSms\Event;
class Event extends \Opencart\System\Engine\Controller
{
/**
* index
* Event trigger: catalog/model/checkout/order/addHistory/before
* @param mixed $route
* @param mixed $data
* @param mixed $output
* @return void
*/
public function index(&$route = false, &$data = array(), &$output = array()): void {
// get data from OpenCart
$order_id = "none";
$this->load->model('setting/setting');
if (isset($this->session->data['order_id']))
{
$order_id = $this->session->data['order_id'];
}
// get data from OpenCart
$customer_phone = $this->customer->getTelephone();
// get data from extension opc_send_sms
$sender_phone = $this->config->get('module_opc_send_sms_phone');
$token = $this->config->get('module_opc_send_sms_token');
$text_raw = $this->config->get('module_opc_send_sms_text');
if (!$text_raw)
{
$text_raw = "Order $order_id created.";
}
$text = str_replace('%order_id%',$order_id,$text_raw);
// send request to MTS Exolve API
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.exolve.ru/messaging/v1/SendSMS',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{
"number": "'.$sender_phone.'",
"destination": "'.$customer_phone.'",
"text": "'.$text.'"
}
',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
"Authorization: Bearer $token"
),
));
$response = curl_exec($curl);
curl_close($curl);
}
/**
* getTemplateBuffer
*
* @param mixed $route
* @param mixed $event_template_buffer
* @return string
*/
protected function getTemplateBuffer($route, $event_template_buffer) {
// if there already is a modified template from view/*/before events use that one
if ($event_template_buffer) {
return $event_template_buffer;
}
}
}
Мы рассмотрим два блока бизнес-логики.
$order_id = "none";
$this->load->model('setting/setting');
if (isset($this->session->data['order_id']))
{
$order_id = $this->session->data['order_id'];
}
// get data from OpenCart
$customer_phone = $this->customer->getTelephone();
// get data from extension opc_send_sms
$sender_phone = $this->config->get('module_opc_send_sms_phone');
$token = $this->config->get('module_opc_send_sms_token');
$text_raw = $this->config->get('module_opc_send_sms_text');
if (!$text_raw)
{
$text_raw = "Order $order_id created.";
}
$text = str_replace('%order_id%',$order_id,$text_raw);
В первом блоке мы получаем данные из интернет-магазина: номер текущего заказа и телефон покупателя.
Затем получаем переменные, которые создало разработанное нами расширение.
Второй блок еще проще, это непосредственно отправка запросов к методу https://api.exolve.ru/messaging/v1/SendSMS MTS Exolve API (ссылка на документацию).
Код автоматически сгенерирован Postman. Я только подставил переменные в запрос.
// send request to MTS Exolve API
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => 'https://api.exolve.ru/messaging/v1/SendSMS',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS =>'{
"number": "'.$sender_phone.'",
"destination": "'.$customer_phone.'",
"text": "'.$text.'"
}
',
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
"Authorization: Bearer $token"
),
));
$response = curl_exec($curl);
curl_close($curl);
}
Сборка расширения для установки
Прежде чем собирать приложение, нам необходимо заполнить файл install.json.
{
"name": "MTS Exolve checkout SMS notification ",
"version": "0.1",
"author": "bosonbeard",
"link": "hhttps://github.com/bosonbeard/mts-habr"
}
Процесс сборки расширения очень простой. Необходимо просто запаковать все папки в архив с название {имя модуля}.ocmod.zip.
В нашем случае opc_send_sms.ocmod.zip. Если вы ошибетесь в названии архива, то после установки модуля могут быть ошибки.
Пример архива для расширения:
Проверяем работу
Установка
Установка расширений производится в соответствующем разделе админ-панели:
После загрузки расширения его необходимо установить.
Далее необходимо перейти в список установленных расширений. Мы разработали расширение с типом «модуль», поэтому будем искать в одноименном разделе.
Нам необходимо установить модуль.
А затем включить его и заполнить остальные настройки.
Обратите внимание на %order_id%. В обработчике отправки сообщения реализована замена данного шаблона на переменную OpenCart с номером текущего заказа order_id. Вы можете дополнительно реализовать обработку других шаблонов по аналогии.
Важный момент: во время тестового периода SMS можно отправить только на номер телефона, с которым вы зарегистрировались в MTС Exolve. Ограничение снимается после заключения договора.
Не забудьте сохранить результат.
Проверка
Осталось только сделать заказ и убедится, что SMS пришло.
Заказ успешно сформирован.
О чем нам пришло SMS уведомление.
Теперь вы можете самостоятельно разработать простое расширение для OpenCart 4 с использованием событий. Но если вам не требуются доработки, можете просто скачать архив с GitHub, установить приложение и попробовать его на своём интернет-магазине.
Надеюсь, статья была для вас полезной. Буду рад почитать ваши комментарии.