msp3PaymentSkeleton — шаблон для создания платёжных дополнений MiniShop3

Сделал открытый шаблон для разработки платёжных дополнений MiniShop3 под MODX 3 github.com/Ibochkarev/msp3PaymentSkeleton

Если вы хотя бы раз писали интеграцию с платёжным API, то знаете этот сценарий.

Сначала нужно разобраться с API провайдера.

Потом появляются webhook, подписи, попытки оплаты, внешний ID, возвраты, статусы, обработка ошибок, 54-ФЗ, настройки, вкладка в заказе, resolver, сборка…

И только после этого можно наконец заняться самой интеграцией с платёжной системой.

При этом значительная часть этой работы каждый раз практически одинаковая.

Поэтому я собрал msp3PaymentSkeleton — готовую основу, от которой можно начинать разработку нового payment Extra для MiniShop3.

Идея простая: не писать инфраструктуру платёжного дополнения заново, а сразу работать с API конкретного провайдера.
Что это

msp3PaymentSkeleton — это не готовая интеграция с каким-то банком или агрегатором.

Это developer skeleton для создания таких интеграций.

Внутри уже подготовлены:
  • Payment handler;
  • API client;
  • подписи запросов;
  • webhook;
  • payment lifecycle;
  • payment attempts;
  • payment link;
  • refund;
  • cancel;
  • вкладка платежа в заказе;
  • 54-ФЗ;
  • настройки;
  • логи;
  • тестовый и production режимы;
  • resolver;
  • тесты;
  • сборка пакета;
  • документация;
  • чек-лист перед релизом.
То есть это уже каркас полноценного платёжного Extra, а не пустой пример класса оплаты.

Почему вообще понадобился skeleton

В MiniShop3 постепенно появляется всё больше отдельных платёжных дополнений.

У каждого провайдера своё API:
  • свои endpoint;
  • свои способы авторизации;
  • свои подписи;
  • свои статусы;
  • свой формат webhook;
  • свои идентификаторы платежей;
  • свои правила возврата.
Но вокруг этого API есть общая инфраструктура MiniShop3.

Например:

Заказ
  ↓
Payment
  ↓
Payment Attempt
  ↓
Provider API
  ↓
Webhook
  ↓
Payment Lifecycle
  ↓
Статус платежа
  ↓
Статус заказа

И нет особого смысла реализовывать этот слой заново в каждом дополнении.

Главное изменение — lifecycle

В основе skeleton используется новый payment lifecycle MiniShop3.

Поэтому платёжное дополнение не должно самостоятельно управлять статусом заказа:

$order->set('status_id', 3);
$order->save();

Вместо этого Extra сообщает lifecycle о событии:

paid
failed
cancelled
refunded
partially_refunded

А MiniShop3 уже сам применяет соответствующую бизнес-логику.

Например:

paid
  ↓
ms3_status_paid

failed
  ↓
ms3_payment_on_failed_status

refunded
  ↓
ms3_payment_on_refunded_status

Это важный момент.

Платёжный Extra отвечает за интеграцию с провайдером, а MiniShop3 — за жизненный цикл платежа и связанный с ним заказ.

Такой подход значительно проще поддерживать, когда количество платёжных интеграций растёт.

Что реально нужно написать под нового провайдера

После клонирования skeleton основная работа находится в нескольких местах.

В проекте они специально отмечены:

// PROVIDER:

В первую очередь:

src/Api/ApiClient.php
src/Api/Signature.php
src/Api/WebhookParser.php
src/Payment/SkeletonPayment.php
src/Service/Settings.php

ApiClient — API провайдера.

Здесь находятся:
  • host;
  • endpoint создания платежа;
  • refund;
  • cancel;
  • HTTP-запросы;
  • headers;
  • body;
  • обработка ответа.
Signature — авторизация и подпись запросов.

Если у провайдера HMAC-SHA256 — уже есть базовый вариант.

Если другая схема — меняется этот слой.

WebhookParser — перевод событий провайдера в события MiniShop3.

Например:

SUCCESS
    ↓
paid

FAIL
    ↓
failed

REFUND
    ↓
refunded

SkeletonPayment — адаптация самого платежа к контракту MiniShop3.

В частности, здесь собирается запрос:

buildPaymentRequest()

А send() возвращает:

payment_link
payment_id
external_id
currency

Быстрый старт

Например, нужно сделать msp3MyPay.

Клонируем skeleton:

cp -R msp3PaymentSkeleton ../msp3MyPay
cd ../msp3MyPay

Запускаем генератор:

php bin/init.php \
  --name=msp3MyPay \
  --provider=MyPay

И получаем уже свой проект.

Дополнительные части можно оставить через --keep:

php bin/init.php \
  --name=msp3MyPay \
  --provider=MyPay \
  --keep=second-method,bindings,receipts-extra

Есть также:

--no-encrypt — для локальной сборки без шифрования.
--self-destruct — удалить bin/init.php после успешной инициализации.

Webhook — уже не «прикрутим потом»

В платёжных интеграциях webhook часто становится самым проблемным местом.

Платёж создан — это только начало.

Дальше провайдер может:
  • прислать подтверждение оплаты;
  • прислать ошибку;
  • прислать отмену;
  • прислать возврат;
  • повторить одно и то же событие несколько раз.
Поэтому в skeleton сразу заложена работа с payment lifecycle.

Стандартный endpoint:

POST /assets/components/minishop3/api.php/api/v1/payment/webhook/{payment_method_id}

Дальше:

raw body
   ↓
verifyWebhook()
   ↓
parseWebhook()
   ↓
PaymentWebhookEvent
   ↓
ms3_payment_lifecycle
   ↓
payment attempt

При этом verifyWebhook() должен действительно проверять подпись.

Заглушка: return true; не является реализацией webhook security.

Идемпотентность

Повтор webhook — обычная ситуация.

Например:

Provider
   │
   ├── webhook #123
   │
   ├── timeout
   │
   └── webhook #123 повторно

Повторная обработка одного и того же providerEventId не должна ломать заказ.

Поэтому идемпотентность вынесена в общий сценарий lifecycle, а при разработке конкретного провайдера необходимо корректно сопоставить его идентификатор события.

А если у провайдера только один webhook URL?

Такое тоже встречается.

Некоторые API дают один webhook URL на весь магазин, а некоторые отправляют вообще не JSON, а form POST.

Для этого в skeleton предусмотрен отдельный: webhook.php

Он позволяет адаптировать нестандартный входящий webhook, не ломая основной payment API MiniShop3.

Возвраты

Возврат также проходит через lifecycle.

Вместо:

$order->set('status_id', ...);

используется:

lifecycle->refund()

И учитывается состояние payment attempt.

Например, нельзя считать заказ оплаченным только потому, что был создан платёж.

Сначала:

pending
   ↓
paid
   ↓
refund

А не:

payment created
   ↓
refund

Это особенно важно для API, где создание платежа и фактическое списание денег — разные операции.

Дополнительные ID провайдера

У разных платёжных систем могут быть разные идентификаторы:

operationId
mdOrder
invoice_id
paymentId
transactionId

MiniShop3 использует external_id, но иногда одного идентификатора недостаточно для последующих API-вызовов.

Поэтому skeleton предусматривает сохранение дополнительных данных через:

lifecycle->initiate()

Это позволяет не потерять идентификатор, который понадобится для refund, capture, sync или других операций.

Вкладка платежа в заказе

Ещё одна вещь, которую обычно приходится делать отдельно в каждом Extra, — интерфейс менеджера.

В skeleton уже подготовлена интеграция через: MS3OrderTabsRegistry

Вкладка может содержать:

  • статус платежа;
  • external ID;
  • сумму;
  • информацию от провайдера;
  • операции возврата;
  • отмену;
  • синхронизацию.
Также учтена регистрация вкладки до загрузки order.min.js и проблема повторного вызова register().

То есть здесь тоже не нужно начинать с исследования внутренностей менеджера.

54-ФЗ

Если провайдер работает с чеками, skeleton уже содержит необходимую основу.

Предусмотрены:
  • включение/отключение чека;
  • НДС;
  • признак способа расчёта;
  • предмет расчёта доставки.
Отдельно проверяется наличие email покупателя перед отправкой чека.

Конкретный формат данных, конечно, адаптируется под API провайдера.

Безопасность

Секреты можно хранить через: msPayment.properties

Поддерживаются:

secret
secret_key
webhook_secret

При этом секреты не должны попадать в payload событий.

В частности:

secret
token
api_key
password

не должны сохраняться как данные платёжной попытки.

Документация внутри самого skeleton

Помимо кода, в репозитории есть отдельная документация:
Последний особенно полезен, когда дополнение уже вроде бы работает.

Потому что «платёж создаётся» и «платёжное дополнение готово к публикации» — немного разные вещи.

Чек-лист перед релизом

Перед публикацией проверяем:
  • send() возвращает необходимые данные;
  • реализован PaymentWebhookHandlerInterface;
  • webhook действительно проверяет подпись;
  • секрет находится в msPayment.properties;
  • нет ручного изменения status_id;
  • webhook переводится в PaymentAttemptStatus;
  • повторный webhook безопасен;
  • корректно обрабатываются рубли/копейки;
  • 54-ФЗ не отправляется без email;
  • дополнительные ID провайдера сохраняются;
  • возврат выполняется через lifecycle;
  • вкладка заказа регистрируется один раз;
  • проверяются права доступа;
  • проходят тесты;
  • проходит PHP syntax check;
  • пакет собирается;
  • после генерации нигде не остаётся имя skeleton.
Совместимость

  • MODX Revolution 3.0+
  • MiniShop3 >= 1.14.0-beta1
  • PHP 8.2+
И что в итоге?

Получается довольно простая модель.

Было:

Новый платёжный Extra
        │
        ├── Payment
        ├── API
        ├── Webhook
        ├── Attempts
        ├── Lifecycle
        ├── Refund
        ├── Manager UI
        ├── 54-ФЗ
        ├── Settings
        ├── Resolver
        └── Build

Стало:

msp3PaymentSkeleton
        │
        ├── Payment ✓
        ├── Attempts ✓
        ├── Lifecycle ✓
        ├── Webhook infrastructure ✓
        ├── Refund ✓
        ├── Manager UI ✓
        ├── 54-ФЗ ✓
        ├── Settings ✓
        ├── Resolver ✓
        └── Build ✓
                  │
                  ▼
             PROVIDER
                  │
        ├── API endpoints
        ├── Signature
        ├── Request format
        └── Webhook format

Именно последняя часть и должна отличаться между платёжными дополнениями.

Кому это пригодится

Если вы собираетесь сделать новое платёжное дополнение для MiniShop3 — попробуйте начать с skeleton.

Особенно если это:
  • банк;
  • платёжный агрегатор;
  • СБП;
  • BNPL-сервис;
  • корпоративный платёжный шлюз;
  • локальная платёжная система;
  • внутренний API компании.
Если API поддерживает создание платежа, получение статуса через webhook и операции с платежом — большая часть инфраструктуры уже подготовлена.

Что дальше

Это не попытка сделать ещё один «универсальный платёжный модуль».

Наоборот.

Идея в том, чтобы сделать небольшую и понятную стандартную основу для отдельных платёжных Extras.

Тогда:

MiniShop3
    │
    ├── msp3TBank
    ├── msp3Sberbank
    ├── msp3PayKeeper
    ├── msp3CloudPayments
    ├── msp3CDEKPay
    └── ...
             ▲
             │
      единый подход
             │
      msp3PaymentSkeleton

Каждое дополнение остаётся самостоятельным и адаптируется под API своего провайдера, но общая архитектура становится гораздо более предсказуемой.

Репозиторий

→ GitHub: Ibochkarev/msp3PaymentSkeleton

Если делаете платёжный Extra для MiniShop3 — можно не начинать с нуля.

Если в процессе использования skeleton обнаружится сценарий, который приходится реализовывать одинаково в нескольких платёжных дополнениях, это как раз хороший кандидат для следующей версии шаблона.

Меньше копипаста между платёжными Extras — больше времени на нормальную интеграцию с самим API.
Иван Бочкарев
Иван Бочкарев
1 час назад
modx.pro
17

Комментарии: 0

Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
0