> For the complete documentation index, see [llms.txt](https://docs.mmoweb.biz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.mmoweb.biz/gaidy/dobavlenie-sposobov-oplaty.md).

# Добавление способов оплаты

### Введение

Этот гайд описывает как интегрировать новый способ оплаты в личный кабинет. Процесс включает настройку [Application Token](/rest-api/autentifikaciya.md), создание обработчика вебхуков, регистрацию в конфигурации и настройку платежной системы.

### Архитектура платежей

```
Платежная система (вебхук)
    ↓
PaymentController::webhook($paymentSystem)
    ↓
PaymentHandler (ваш класс обработчика)
    ├─ Проверка IP адреса
    ├─ Проверка статуса платежной системы
    └─ Обработка вебхука (webhook())
    ↓
API: POST /v2/payment/order/complete
    ├─ payment_system: "cstm:your_system"
    ├─ order_id: "Order:XXXXXXXX"
    └─ payment_id: "внешний_id"
    ↓
Пополнение баланса пользователя
```

***

### Требования

**Обязательно:**

* Понимание REST API и PHP (версия 7.4+)
* Учетные данные платежной системы (merchant ID, API ключи и т.д.)
* Документация платежной системы (формат вебхуков, способ подписи)

**Рекомендуется:**

* Знакомство с примером FreeKassa в `Modules/Globals/Donations/Integrations/Handlers/FreeKassa.php`

***

### Шаг 1: Выпуск Application Token

**Это критичный шаг — без токена платежи не будут обрабатываться!**

Application Token требуется для аутентификации при вызове API endpoint'а `/v2/payment/order/complete`.

#### Создание токена

1. Перейдите в панель администратора: `https://mmoweb.biz/panel/settings/Globals.ApiKeyApp/add`
2. Укажите название
3. В разделе **"Доступы ключа"** включите разрешение: **Финализация платежного ордера**
4. **Скопируйте токен** — он больше не будет отображён
5. Нажмите "Создать"

#### Установка токена

Откройте файл `Modules/Globals/Donations/Integrations/PaymentHandler.php` и найдите строку:

```php
const APPLICATION_TOKEN = '';
```

Вставьте ваш токен:

```php
const APPLICATION_TOKEN = 'ваш_64_символьный_токен';
```

> **ВАЖНО:** Храните токен в безопасности. Не коммитьте в публичные репозитории. Если токен скомпрометирован — создайте новый и отключите старый.

***

### Шаг 2: Создание класса обработчика

Создайте файл

&#x20;`Modules/Globals/Donations/Integrations/Handlers/YourPaymentSystem.php`:

```php
<?php

namespace Modules\Globals\Donations\Integrations\Handlers;

use Modules\Globals\Donations\Integrations\PaymentHandler;
use Modules\Globals\Donations\Integrations\HandlerInterface;
use Modules\Globals\Donations\Integrations\DTO\CheckoutResponse;
use Modules\Globals\Donations\Integrations\Exceptions\PaymentWebhookException;

class YourPaymentSystem extends PaymentHandler implements HandlerInterface
{
    // Содержимое класса (см. ниже)
}
```

**Требования:**

* Класс должен наследоваться от `PaymentHandler`
* Класс должен реализовывать `HandlerInterface`
* Имя класса должно совпадать с именем файла

***

### Шаг 3: Определение конфигурации

Внутри класса определите статические свойства:

```php
private static bool $status = false;  // Включить после настройки
private static string $identifier = 'your_payment_system';  // идентификатор платежной системы
private static array $orderDescription = [
    'ru' => 'Пополнение баланса',
    'en' => 'Balance topup',
];
private static string $currency = 'RUB';  // RUB, USD, EUR и т.д.
private static int $sortOrder = 1;

private static array $config = [
    'merchant_id'   => '',  // ID магазина
    'api_key'       => '',  // API ключ
    'secret_key'    => '',  // Секретный ключ для подписи
];

private static array $allowedIps = [
    // '1.2.3.4',  // разрешенные IP адреса для обработки вебхука (опционально)
];
```

#### Конструктор

```php
public function __construct()
{
    $this->setStatus(self::$status);
    $this->setIdentifier(self::$identifier);
    $this->setSortOrder(self::$sortOrder);
    $this->setDescription(self::$orderDescription);
    $this->setCurrency(self::$currency);
    $this->setConfig(self::$config);
    $this->setAllowedIps(self::$allowedIps);
}
```

***

### Шаг 4: Реализация метода checkout()

Этот метод подготавливает платеж и возвращает URL для перенаправления пользователя.

{% hint style="info" %}
Реализуйте метод checkout() для получения URL перенаправления пользователя, в соответствии с документацией платежной системы.
{% endhint %}

```php
public function checkout(array $orderData): CheckoutResponse
{
    // 1. Проверяем статус
    if (!$this->getStatus()) {
        throw new \Exception('Payment method is unavailable');
    }

    // 2. Валидируем параметры
    if (empty($orderData['order_id']) || empty($orderData['sum']) || $orderData['sum'] <= 0) {
        throw new \Exception('Invalid order data');
    }

    // 3. Формируем параметры для платежной системы
    $params = [
        'merchant_id'  => $this->getConfig('merchant_id'),
        'amount'       => $orderData['sum'],
        'order_id'     => $orderData['order_id'],
        'currency'     => $this->getCurrency(),
        'description'  => $this->getDescription()['ru'] ?? 'Payment',
    ];

    // 4. Вычисляем подпись (если требуется)
    $signature = md5(
        $this->getConfig('merchant_id') . ':' .
        $orderData['sum'] . ':' .
        $this->getConfig('secret_key') . ':' .
        $orderData['order_id']
    );
    $params['signature'] = $signature;

    // 5. Формируем URL
    $redirectUrl = 'https://payment-system.example.com/pay?' . http_build_query($params);

    // 6. Возвращаем ответ
    return new CheckoutResponse($redirectUrl);
}
```

***

### Шаг 5: Реализация метода webhook()

Этот метод обрабатывает входящий вебхук от платежной системы.

{% hint style="info" %}
Реализуйте метод webhook() в соответствии с документацией обработки успешных платежей вашей платежной системы.
{% endhint %}

```php
public function webhook(): bool
{
    try {
        // 1. Получаем данные вебхука
        // Формат зависит от системы: $_POST, $_GET или JSON
        $payload = $_POST;  // или json_decode(file_get_contents('php://input'), true)

        if (empty($payload)) {
            throw new PaymentWebhookException('No data in webhook');
        }

        // 2. Проверяем обязательные поля
        if (empty($payload['order_id']) || empty($payload['signature'])) {
            throw new PaymentWebhookException('Missing required fields');
        }

        // 3. Валидируем подпись (КРИТИЧНО!)
        $expectedSignature = $this->getWebhookSignature($payload);
        if (!hash_equals($expectedSignature, $payload['signature'])) {
            throw new PaymentWebhookException('Invalid signature');
        }

        // 4. Проверяем статус платежа
        if ($payload['status'] !== 'completed' && $payload['status'] !== 'success') {
            throw new PaymentWebhookException('Payment not completed');
        }

        // 5. Извлекаем данные
        // order_id должен быть в формате "Order:XXXXXXXX"
        $this->setOrderId($payload['order_id']);
        $this->setPaymentId($payload['payment_id'] ?? '');

        return true;

    } catch (PaymentWebhookException $e) {
        error_log("Webhook error: " . $e->getMessage());
        log_write('payment_errors', $e->getMessage());
        return false;
    }
}
```

***

### Шаг 6: Реализация метода getWebhookSignature()

Вычисляет ожидаемую подпись вебхука для верификации.

```php
public function getWebhookSignature(array $data): string
{
    // Формула зависит от платежной системы!
    // Пример для MD5:
    return md5(
        $this->getConfig('merchant_id') . ':' .
        $data['order_id'] . ':' .
        $data['amount'] . ':' .
        $this->getConfig('secret_key')
    );
    
}
```

> **ВАЖНО:** Формула подписи должна **точно совпадать** с документацией платежной системы!

***

### Шаг 7: Регистрация в Config.php

Откройте файл `Config.php` и добавьте вашу систему:

```php
define('PAYMENT_INTEGRATIONS', [
    'freekassa'      => \Modules\Globals\Donations\Integrations\Handlers\FreeKassa::class,
    'your_payment_system' => \Modules\Globals\Donations\Integrations\Handlers\YourPaymentSystem::class,
]);
```

**Важно:** Ключ в массиве должен совпадать с `$identifier` в созданном ранее классе.

***

### Шаг 8: Добавление изображения платежной системы

Добавьте логотип платежной системы:

**Путь:**

```
template/panel/assets/media/payment/your_identifier.png
```

**Требования:**

* **Формат:** PNG или SVG
* **Фон:** прозрачный или белый
* **Имя файла:** должно совпадать с `$identifier`

***

### Шаг 9: Настройка URL вебхука в платежной системе

URL вебхука формируется автоматически:

```
https://ваш-сайт.ru/payment/webhook/{identifier}
```

**Примеры:**

* `$identifier = 'stripe'` → `https://ваш-сайт.ru/payment/webhook/stripe`
* `$identifier = 'paypal'` → `https://ваш-сайт.ru/payment/webhook/paypal`

#### Что делать:

1. Войдите в панель управления платежной системой
2. Найдите раздел "Webhooks" / "Notifications" / "Integration"
3. Добавьте новый вебхук
4. Вставьте URL: `https://ваш-сайт.ru/payment/webhook/your_identifier`
5. Выберите события: обычно "Payment completed" или "Payment success"
6. Сохраните

***

### Шаг 10: Включение платежной системы

Откройте ваш класс обработчика и измените:

```php
private static bool $status = true;  // Включаем платежную систему
```

После этого способ оплаты **автоматически появится** на странице:

```
https://ваш-сайт.ru/panel/donations
```

***

### Чек-лист перед тестированием

Проверьте что выполнены все шаги:

* Application Token выпущен и установлен в `PaymentHandler::APPLICATION_TOKEN`
* Класс обработчика создан в `Modules/Globals/Donations/Integrations/Handlers/`
* Класс зарегистрирован в `PAYMENT_INTEGRATIONS` в `Config.php`
* Изображение добавлено в `template/panel/assets/media/payment/your_identifier.png`
* URL вебхука настроен в платежной системе
* `$status = true` установлен в классе
* `checkout()` возвращает объект `checkoutResponse($redirectUrl)`
* `webhook()` возвращает `boolean`

***

### Тестирование

#### Проверка отображения

1. Перейдите на `https://ваш-сайт.ru/panel/donations`
2. Убедитесь что ваша платежная система отображается в списке

#### Тестовый платеж

1. Выберите вашу платежную систему
2. Укажите сумму пополнения
3. Нажмите "Пополнить"
4. Убедитесь что произошел редирект на платежный шлюз
5. Выполните тестовый платеж
6. Проверьте что баланс пополнился

***

### Частые ошибки

| Ошибка                              | Причина              | Решение                                           |
| ----------------------------------- | -------------------- | ------------------------------------------------- |
| `ORDER_NOT_FOUND`                   | Ордер не найден      | Проверьте формат order\_id: `Order:XXXXXXXX`      |
| `Invalid webhook signature`         | Подпись не совпадает | Проверьте формулу подписи в документации          |
| `Payment system not supported`      | Не зарегистрирована  | Проверьте PAYMENT\_INTEGRATIONS в Config.php      |
| `Forbidden: IP address not allowed` | IP не разрешен       | Добавьте IP в $allowedIps или используйте подпись |
| `ORDER_ALREADY_COMPLETED`           | Ордер уже обработан  | Нормально при повторной отправке вебхука          |

***

### Рекомендации по безопасности

1. **Всегда проверяйте статус платежа** в методе `webhook()`

   ```php
   if ($payload['status'] !== 'completed') {
       throw new PaymentWebhookException('Payment not completed');
   }
   ```
2. **Всегда проверяйте подпись вебхука** используя `hash_equals()`

   ```php
   if (!hash_equals($expectedSignature, $payload['signature'])) {
       throw new PaymentWebhookException('Invalid signature');
   }
   ```
3. **Альтернативно: проверяйте IP адрес** если платежная система не использует подписи

   ```php
   private static array $allowedIps = [
       '1.2.3.4',  // IP платежной системы
   ];
   ```

**Дополнительно:**

* Проверяйте сумму платежа если она приходит в вебхуке
* Используйте HTTPS для всех запросов
* Логируйте все платежи для аудита
* Не доверяйте клиентским редиректам — только вебхукам

***

### Полный пример класса

```php
<?php

namespace Modules\Globals\Donations\Integrations\Handlers;

use Modules\Globals\Donations\Integrations\PaymentHandler;
use Modules\Globals\Donations\Integrations\HandlerInterface;
use Modules\Globals\Donations\Integrations\DTO\CheckoutResponse;
use Modules\Globals\Donations\Integrations\Exceptions\PaymentWebhookException;

class MyPaymentSystem extends PaymentHandler implements HandlerInterface
{
    private static bool $status = true;
    private static string $identifier = 'mypayment';
    private static array $orderDescription = [
        'ru' => 'Оплата через MyPayment',
        'en' => 'Payment via MyPayment',
    ];
    private static string $currency = 'USD';
    private static int $sortOrder = 2;
    private static array $config = [
        'merchant_id' => 'your_merchant_id',
        'secret_key'  => 'your_secret_key',
    ];
    private static array $allowedIps = [];

    public function __construct()
    {
        $this->setStatus(self::$status);
        $this->setIdentifier(self::$identifier);
        $this->setSortOrder(self::$sortOrder);
        $this->setDescription(self::$orderDescription);
        $this->setCurrency(self::$currency);
        $this->setConfig(self::$config);
        $this->setAllowedIps(self::$allowedIps);
    }

    public function checkout(array $orderData): CheckoutResponse
    {
        if (!$this->getStatus()) {
            throw new \Exception('Payment unavailable');
        }
        if (empty($orderData['order_id']) || empty($orderData['sum'])) {
            throw new \Exception('Invalid data');
        }

        $params = [
            'merchant' => $this->getConfig('merchant_id'),
            'amount'   => $orderData['sum'],
            'order'    => $orderData['order_id'],
            'sign'     => md5($this->getConfig('merchant_id') . $orderData['sum'] . $this->getConfig('secret_key'))
        ];

        $url = 'https://pay.example.com?' . http_build_query($params);
        return new CheckoutResponse($url);
    }

    public function webhook(): bool
    {
        try {
            $payload = $_POST;

            if (empty($payload['order_id']) || empty($payload['signature'])) {
                throw new PaymentWebhookException('Missing fields');
            }

            if (!hash_equals($this->getWebhookSignature($payload), $payload['signature'])) {
                throw new PaymentWebhookException('Invalid signature');
            }

            if ($payload['status'] !== 'success') {
                throw new PaymentWebhookException('Payment not completed');
            }

            $this->setOrderId($payload['order_id']);
            $this->setPaymentId($payload['payment_id'] ?? '');

            return true;

        } catch (PaymentWebhookException $e) {
            error_log("Webhook error: " . $e->getMessage());
            return false;
        }
    }

    public function getWebhookSignature(array $data): string
    {
        return md5($data['order_id'] . $data['amount'] . $this->getConfig('secret_key'));
    }
}
```
