Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1723177949 | ||
| d89a78bef2 |
25
README.md
25
README.md
@@ -64,8 +64,12 @@ php artisan vendor:publish --tag=alfa-bank-config
|
||||
ALFA_BAAS_ENVIRONMENT=sandbox # или production
|
||||
ALFA_BAAS_CLIENT_ID=your-client-id
|
||||
ALFA_BAAS_CLIENT_SECRET=your-client-secret
|
||||
# URL в вашем приложении, на который вернется пользователь после входа (должен совпадать с настройками в Портале)
|
||||
ALFA_BAAS_REDIRECT_URI=https://your-app.com/alfa/callback
|
||||
ALFA_BAAS_CERT_PATH=storage/certs/baas_cert.pem
|
||||
ALFA_BAAS_KEY_PATH=storage/certs/baas_key.pem
|
||||
# Путь к корневым сертификатам (или 'false' для отключения проверки SSL в sandbox)
|
||||
ALFA_BAAS_CA_BUNDLE=false
|
||||
|
||||
# Опционально: Ключи для CMS-подписи (если отличаются от транспортных)
|
||||
# ALFA_BAAS_SIGNING_CERT_PATH=storage/certs/sign_cert.pem
|
||||
@@ -76,6 +80,7 @@ ALFA_SBP_ENVIRONMENT=sandbox # или production
|
||||
ALFA_SBP_ALIAS=your-key-name-alias
|
||||
ALFA_SBP_CERT_PATH=storage/certs/sbp_cert.pem
|
||||
ALFA_SBP_KEY_PATH=storage/certs/sbp_key.pem
|
||||
ALFA_SBP_CA_BUNDLE=false
|
||||
|
||||
# --- Webhooks ---
|
||||
ALFA_WEBHOOK_USER=alfa
|
||||
@@ -94,8 +99,10 @@ return [
|
||||
'environment' => env('ALFA_BAAS_ENVIRONMENT', 'sandbox'),
|
||||
'client_id' => env('ALFA_BAAS_CLIENT_ID'),
|
||||
'client_secret' => env('ALFA_BAAS_CLIENT_SECRET'),
|
||||
'redirect_uri' => env('ALFA_BAAS_REDIRECT_URI'),
|
||||
'certificate_path' => env('ALFA_BAAS_CERT_PATH'),
|
||||
'private_key_path' => env('ALFA_BAAS_KEY_PATH'),
|
||||
'ca_bundle' => env('ALFA_BAAS_CA_BUNDLE'),
|
||||
// ...
|
||||
],
|
||||
'sbp' => [
|
||||
@@ -103,6 +110,7 @@ return [
|
||||
'key_name_alias' => env('ALFA_SBP_ALIAS'),
|
||||
'certificate_path' => env('ALFA_SBP_CERT_PATH'),
|
||||
'private_key_path' => env('ALFA_SBP_KEY_PATH'),
|
||||
'ca_bundle' => env('ALFA_SBP_CA_BUNDLE'),
|
||||
// ...
|
||||
],
|
||||
'webhook' => [
|
||||
@@ -193,6 +201,23 @@ $newAuth = $baas->refreshAccessToken($savedRefreshToken);
|
||||
$baas->authenticate($newAuth);
|
||||
```
|
||||
|
||||
**Пример обработки callback в Laravel:**
|
||||
```php
|
||||
// routes/web.php
|
||||
Route::get('/alfa/callback', function (Request $request, AlfaBankConnector $baas) {
|
||||
$code = $request->query('code');
|
||||
|
||||
// Обмениваем временный код на постоянные токены
|
||||
$authenticator = $baas->getAccessToken($code);
|
||||
|
||||
// Сохраняем refresh_token в БД для будущих обновлений сессии
|
||||
$refreshToken = $authenticator->getRefreshToken();
|
||||
auth()->user()->update(['alfa_refresh_token' => $refreshToken]);
|
||||
|
||||
return "Авторизация в Альфа-Банке прошла успешно!";
|
||||
});
|
||||
```
|
||||
|
||||
Полный пример: [examples/13_h2h_auth_service.php](examples/13_h2h_auth_service.php)
|
||||
|
||||
### 4. Генерация Client Secret (Утилита)
|
||||
|
||||
271
examples/20_sbp_qr_to_nominal_account.php
Normal file
271
examples/20_sbp_qr_to_nominal_account.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Пример: SBP QR → Номинальный счёт → Идентификация → Бенефициар
|
||||
*
|
||||
* Сценарий:
|
||||
* 1. Генерируем SBP QR код для пополнения номинального счёта
|
||||
* 2. Клиент оплачивает через QR (переводит деньги на номинальный счёт)
|
||||
* 3. Получаем список неидентифицированных пополнений
|
||||
* 4. Идентифицируем платёж (привязываем к конкретному бенефициару)
|
||||
* 5. Проверяем статус идентификации
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Apoloz\AlfaBank\AlfaBank;
|
||||
use Apoloz\AlfaBank\Enums\Environment;
|
||||
use Apoloz\AlfaBank\Sbp\Data\QrCodeRequestData;
|
||||
use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest;
|
||||
use Apoloz\AlfaBank\Core\AlfaConfig;
|
||||
use Apoloz\AlfaBank\Sbp\SbpConnector;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\GetUnidentifiedTransactionsRequest;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\IdentifyTransactionRequest;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\GetIdentifyStatusRequest;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Identification\IdentifyRequestData;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Identification\AmountDistributionData;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Payouts\DigestSignatureData;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
echo "=== SBP QR → Номинальный счёт → Идентификация ===\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// КОНФИГУРАЦИЯ
|
||||
// =============================================================================
|
||||
|
||||
// SBP QR конфигурация (mTLS + подпись)
|
||||
$sbpMtlsCert = __DIR__ . '/certs/sbp_mtls.crt';
|
||||
$sbpMtlsKey = __DIR__ . '/certs/sbp_mtls.key';
|
||||
$sbpSigningKey = __DIR__ . '/certs/sbp_signing.key'; // 4096-bit для подписи
|
||||
$sbpKeyAlias = 'your-alias-from-bank';
|
||||
$sbpTermNo = '30003970'; // Номер терминала
|
||||
$sbpCaBundle = __DIR__ . '/certs/russian_trusted_ca-bundle.crt';
|
||||
|
||||
// BaaS конфигурация (для номинальных счетов)
|
||||
$baasCert = __DIR__ . '/certs/baas_transport.crt';
|
||||
$baasKey = __DIR__ . '/certs/baas_transport.key';
|
||||
$baasClientId = getenv('ALFA_BAAS_CLIENT_ID');
|
||||
$baasClientSecret = getenv('ALFA_BAAS_CLIENT_SECRET');
|
||||
$baasCaBundle = __DIR__ . '/certs/russian_trusted_ca-bundle.crt';
|
||||
|
||||
// Данные для идентификации
|
||||
$nominalAccountNumber = '40702810XXXXXXXXXXX'; // Ваш номинальный счёт
|
||||
$beneficiaryId = '3fa85f64-5717-4562-b3fc-2c963f66afa6'; // ID бенефициара
|
||||
|
||||
echo "Конфигурация:\n";
|
||||
echo " Номинальный счёт: $nominalAccountNumber\n";
|
||||
echo " Бенефициар ID: $beneficiaryId\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ЭТАП 1: Генерация SBP QR кода для пополнения номинального счёта
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ЭТАП 1: Генерация SBP QR кода ===\n";
|
||||
|
||||
try {
|
||||
// Создаём SBP connector с раздельными ключами (mTLS + signing)
|
||||
$sbpConfig = new AlfaConfig(
|
||||
baseUrl: Environment::SBP_SANDBOX->value,
|
||||
certificatePath: $sbpMtlsCert,
|
||||
privateKeyPath: $sbpMtlsKey,
|
||||
caBundlePath: $sbpCaBundle,
|
||||
signingPrivateKeyPath: $sbpSigningKey // 4096-bit ключ для подписи
|
||||
);
|
||||
|
||||
$sbp = new SbpConnector($sbpConfig, $sbpKeyAlias);
|
||||
|
||||
// Создаём динамический QR код с уникальным OrderNumber для идентификации
|
||||
$orderNumber = 'ORDER-' . uniqid();
|
||||
|
||||
$qrPayload = new QrCodeRequestData(
|
||||
termNo: $sbpTermNo,
|
||||
qrcType: '02', // Dynamic QR
|
||||
amount: '50000', // 500.00 RUB
|
||||
currency: 'RUB',
|
||||
paymentPurpose: "Пополнение номинального счета {$nominalAccountNumber}",
|
||||
orderNumber: $orderNumber, // Уникальный номер заказа для отслеживания
|
||||
messageID: 'sbp-' . uniqid(),
|
||||
qrTtl: '60', // QR активен 60 минут
|
||||
notificationUrl: 'https://your-domain.com/webhooks/sbp', // Webhook для уведомлений
|
||||
width: '300',
|
||||
height: '300'
|
||||
);
|
||||
|
||||
$request = new GetQrCodeRequest($qrPayload);
|
||||
$response = $sbp->send($request);
|
||||
$qrData = $response->dto();
|
||||
|
||||
if ($qrData->ErrorCode !== 0) {
|
||||
throw new \RuntimeException("QR generation failed: {$qrData->message}");
|
||||
}
|
||||
|
||||
echo "✅ QR код сгенерирован!\n";
|
||||
echo " Order Number: $orderNumber\n";
|
||||
echo " QRC ID: {$qrData->qrcId}\n";
|
||||
echo " QR Payload: {$qrData->payload}\n";
|
||||
echo " Status: {$qrData->status}\n";
|
||||
echo "\n💡 Клиент должен отсканировать QR и оплатить {$qrData->payload}\n\n";
|
||||
|
||||
// Сохраняем QR изображение для показа клиенту
|
||||
if (!empty($qrData->image)) {
|
||||
$qrImagePath = __DIR__ . '/qr_payment.png';
|
||||
file_put_contents($qrImagePath, base64_decode($qrData->image));
|
||||
echo " 📁 QR изображение сохранено: $qrImagePath\n\n";
|
||||
}
|
||||
|
||||
echo "⏳ Ждём, пока клиент оплатит...\n";
|
||||
echo " (В тестовой среде используйте Sandbox для эмуляции оплаты)\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ЭТАП 2: Получение неидентифицированных пополнений
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ЭТАП 2: Поиск неидентифицированных пополнений ===\n";
|
||||
echo " (После оплаты через SBP деньги поступают на номинальный счёт\n";
|
||||
echo " как 'неидентифицированное пополнение')\n\n";
|
||||
|
||||
// Инициализируем BaaS connector (требуется H2H авторизация)
|
||||
$baas = AlfaBank::baas(
|
||||
certificatePath: $baasCert,
|
||||
privateKeyPath: $baasKey,
|
||||
environment: Environment::SANDBOX,
|
||||
clientId: $baasClientId,
|
||||
clientSecret: $baasClientSecret
|
||||
);
|
||||
|
||||
// Авторизация через Client Credentials Flow
|
||||
$baas->login($baasClientId, $baasClientSecret, ['beneficiary-payments']);
|
||||
|
||||
// Получаем список неидентифицированных пополнений
|
||||
$unidentifiedRequest = new GetUnidentifiedTransactionsRequest(
|
||||
accountNumber: $nominalAccountNumber,
|
||||
limit: 10,
|
||||
offset: 0
|
||||
);
|
||||
|
||||
$unidentifiedResponse = $baas->send($unidentifiedRequest);
|
||||
$unidentifiedCollection = $unidentifiedResponse->dto();
|
||||
|
||||
echo "Найдено неидентифицированных пополнений: " . count($unidentifiedCollection->items) . "\n\n";
|
||||
|
||||
if (count($unidentifiedCollection->items) === 0) {
|
||||
echo "⚠️ Нет неидентифицированных пополнений. Возможные причины:\n";
|
||||
echo " - Платёж ещё не поступил (нужно время на обработку)\n";
|
||||
echo " - Неправильный номер номинального счёта\n";
|
||||
echo " - Платёж уже идентифицирован\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// Ищем нужную транзакцию (обычно по сумме или назначению платежа)
|
||||
$targetTransaction = null;
|
||||
foreach ($unidentifiedCollection->items as $transaction) {
|
||||
echo " Транзакция ID: {$transaction->id}\n";
|
||||
echo " Сумма: {$transaction->amount} RUB\n";
|
||||
echo " Назначение: {$transaction->purpose}\n";
|
||||
echo " Дата: {$transaction->date}\n";
|
||||
echo " Плательщик: {$transaction->payer?->name}\n\n";
|
||||
|
||||
// В реальном сценарии ищем по сумме, назначению или другим критериям
|
||||
// Здесь берём первую для примера
|
||||
if ($targetTransaction === null) {
|
||||
$targetTransaction = $transaction;
|
||||
}
|
||||
}
|
||||
|
||||
if ($targetTransaction === null) {
|
||||
throw new \RuntimeException("Не найдена подходящая транзакция для идентификации");
|
||||
}
|
||||
|
||||
echo "✅ Выбрана транзакция для идентификации: {$targetTransaction->id}\n";
|
||||
echo " Сумма: {$targetTransaction->amount} RUB\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ЭТАП 3: Идентификация платежа (привязка к бенефициару)
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ЭТАП 3: Идентификация платежа ===\n";
|
||||
echo " Привязываем платёж к бенефициару: $beneficiaryId\n\n";
|
||||
|
||||
// Подготавливаем распределение суммы
|
||||
// Можно распределить между несколькими бенефициарами
|
||||
$amountDistribution = DataCollection::create([
|
||||
new AmountDistributionData(
|
||||
beneficiaryId: $beneficiaryId,
|
||||
amount: $targetTransaction->amount
|
||||
)
|
||||
], AmountDistributionData::class);
|
||||
|
||||
// Подготавливаем данные для идентификации
|
||||
$identifyData = new IdentifyRequestData(
|
||||
transactionId: $targetTransaction->id,
|
||||
accountNumber: $nominalAccountNumber,
|
||||
amountDistribution: $amountDistribution,
|
||||
// Подпись добавляется через sign() метод
|
||||
digestSignatures: DataCollection::create([
|
||||
new DigestSignatureData(
|
||||
base64Encoded: 'base64-encoded-signature-here', // Реальная подпись
|
||||
signatureType: 'rsa'
|
||||
)
|
||||
], DigestSignatureData::class)
|
||||
);
|
||||
|
||||
// Создаём запрос на идентификацию
|
||||
$identifyRequest = new IdentifyTransactionRequest(
|
||||
id: $targetTransaction->id,
|
||||
data: $identifyData
|
||||
);
|
||||
|
||||
$identifyResponse = $baas->send($identifyRequest);
|
||||
$identifyResult = $identifyResponse->dto();
|
||||
|
||||
echo "✅ Запрос на идентификацию создан!\n";
|
||||
echo " Identify ID: {$identifyResult->identifyId}\n";
|
||||
echo " Статус: {$identifyResult->status}\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ЭТАП 4: Проверка статуса идентификации
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ЭТАП 4: Проверка статуса ===\n\n";
|
||||
|
||||
$statusRequest = new GetIdentifyStatusRequest($identifyResult->identifyId);
|
||||
$statusResponse = $baas->send($statusRequest);
|
||||
$statusResult = $statusResponse->dto();
|
||||
|
||||
echo "Статус идентификации: {$statusResult->status}\n";
|
||||
echo "Возможные статусы:\n";
|
||||
echo " - PENDING: В обработке\n";
|
||||
echo " - VALIDATED: Провалидировано\n";
|
||||
echo " - COMPLETE: Успешно завершено\n";
|
||||
echo " - FAILED: Ошибка\n\n";
|
||||
|
||||
if ($statusResult->status === 'COMPLETE') {
|
||||
echo "🎉 ИДЕНТИФИКАЦИЯ УСПЕШНО ЗАВЕРШЕНА!\n";
|
||||
echo " Деньги {$targetTransaction->amount} RUB зачислены бенефициару $beneficiaryId\n\n";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// РЕЗЮМЕ
|
||||
// =============================================================================
|
||||
|
||||
echo "=== РЕЗЮМЕ ===\n";
|
||||
echo "✅ 1. Сгенерирован SBP QR код\n";
|
||||
echo "✅ 2. Клиент оплатил через SBP (эмитация в тесте)\n";
|
||||
echo "✅ 3. Деньги поступили на номинальный счёт\n";
|
||||
echo "✅ 4. Платёж идентифицирован и привязан к бенефициару\n";
|
||||
echo "\n💡 Теперь бенефициар может распоряжаться этими средствами\n";
|
||||
echo " (например, запросить выплату через CreatePayoffRequest)\n";
|
||||
|
||||
} catch (\Apoloz\AlfaBank\Exceptions\AlfaBankException $e) {
|
||||
echo "\n❌ AlfaBankException: {$e->getMessage()}\n";
|
||||
if ($e->getResponse()) {
|
||||
echo "Ответ API: {$e->getResponse()->body()}\n";
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
echo "\n❌ Ошибка: {$e->getMessage()}\n";
|
||||
echo "Файл: {$e->getFile()}:{$e->getLine()}\n";
|
||||
}
|
||||
|
||||
echo "\n=== Пример завершён ===\n";
|
||||
177
examples/21_laravel_tinker_sbp_identification.php
Normal file
177
examples/21_laravel_tinker_sbp_identification.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Laravel Tinker: SBP QR → Номинальный счёт → Идентификация
|
||||
*
|
||||
* Использование:
|
||||
* docker compose exec php-fpm php artisan tinker
|
||||
* >>> include 'examples/21_laravel_tinker_sbp_identification.php'
|
||||
*/
|
||||
|
||||
use Apoloz\AlfaBank\Core\AlfaBankConnector;
|
||||
use Apoloz\AlfaBank\Core\AlfaConfig;
|
||||
use Apoloz\AlfaBank\Enums\Environment;
|
||||
use Apoloz\AlfaBank\Sbp\SbpConnector;
|
||||
use Apoloz\AlfaBank\Sbp\Data\QrCodeRequestData;
|
||||
use Apoloz\AlfaBank\Sbp\Requests\GetQrCodeRequest;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\GetUnidentifiedTransactionsRequest;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\IdentifyTransactionRequest;
|
||||
use Apoloz\AlfaBank\Requests\NominalAccounts\Identification\GetIdentifyStatusRequest;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Identification\IdentifyRequestData;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Identification\AmountDistributionData;
|
||||
use Apoloz\AlfaBank\Data\NominalAccounts\Payouts\DigestSignatureData;
|
||||
use Spatie\LaravelData\DataCollection;
|
||||
|
||||
echo "=== SBP QR → Номинальный счёт → Идентификация (Tinker) ===\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// КОНФИГУРАЦИЯ
|
||||
// =============================================================================
|
||||
|
||||
$nominalAccountNumber = '40702810601880001584'; // Номинальный счёт из sandbox
|
||||
$beneficiaryId = 'your-beneficiary-id-here'; // ID бенефициара (получить из списка)
|
||||
$amount = '15000'; // 150.00 RUB в копейках
|
||||
|
||||
echo "Номинальный счёт: $nominalAccountNumber\n";
|
||||
echo "Сумма: " . number_format($amount / 100, 2) . " RUB\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ШАГ 1: Генерация SBP QR
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ШАГ 1: Генерация SBP QR кода ===\n";
|
||||
|
||||
try {
|
||||
// SBP Connector
|
||||
$sbpConfig = new AlfaConfig(
|
||||
baseUrl: Environment::SBP_SANDBOX->value,
|
||||
certificatePath: config('alfa-bank.sbp.certificate_path'),
|
||||
privateKeyPath: config('alfa-bank.sbp.private_key_path'),
|
||||
caBundlePath: config('alfa-bank.sbp.ca_bundle'),
|
||||
signingPrivateKeyPath: config('alfa-bank.sbp.signing_private_key_path')
|
||||
);
|
||||
|
||||
$sbp = new SbpConnector($sbpConfig, config('alfa-bank.sbp.key_name_alias'));
|
||||
|
||||
$orderNumber = 'ORDER-' . uniqid();
|
||||
|
||||
$qrResponse = $sbp->send(new GetQrCodeRequest(
|
||||
new QrCodeRequestData(
|
||||
termNo: config('alfa-bank.sbp.terminal_no'),
|
||||
qrcType: '02', // Dynamic QR
|
||||
amount: $amount,
|
||||
currency: 'RUB',
|
||||
paymentPurpose: "Пополнение счета $nominalAccountNumber",
|
||||
orderNumber: $orderNumber,
|
||||
messageID: 'sbp-' . uniqid(),
|
||||
qrTtl: '60'
|
||||
)
|
||||
));
|
||||
|
||||
$qrData = $qrResponse->dto();
|
||||
|
||||
if ($qrData->ErrorCode !== 0) {
|
||||
echo "❌ Ошибка генерации QR: {$qrData->message}\n";
|
||||
return;
|
||||
}
|
||||
|
||||
echo "✅ QR код сгенерирован!\n";
|
||||
echo " Order: $orderNumber\n";
|
||||
echo " QRC ID: {$qrData->qrcId}\n";
|
||||
echo " Payload: {$qrData->payload}\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ШАГ 2: Получение неидентифицированных пополнений
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ШАГ 2: Поиск неидентифицированных пополнений ===\n";
|
||||
echo "(После оплаты клиентом через SBP)\n\n";
|
||||
|
||||
$baas = app(AlfaBankConnector::class);
|
||||
|
||||
// Получаем список
|
||||
$unidentifiedResponse = $baas->send(
|
||||
new GetUnidentifiedTransactionsRequest($nominalAccountNumber, limit: 5)
|
||||
);
|
||||
|
||||
$transactions = $unidentifiedResponse->dto()->items;
|
||||
echo "Найдено транзакций: " . count($transactions) . "\n\n";
|
||||
|
||||
if (count($transactions) === 0) {
|
||||
echo "⚠️ Нет неидентифицированных пополнений\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем список
|
||||
foreach ($transactions as $i => $tx) {
|
||||
echo " [$i] ID: {$tx->id}\n";
|
||||
echo " Сумма: {$tx->amount} RUB\n";
|
||||
echo " Назначение: {$tx->purpose}\n";
|
||||
echo " Плательщик: {$tx->payer?->name}\n\n";
|
||||
}
|
||||
|
||||
// Берём первую для примера
|
||||
$transaction = $transactions[0];
|
||||
echo "✅ Выбрана транзакция: {$transaction->id} ({$transaction->amount} RUB)\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ШАГ 3: Идентификация (привязка к бенефициару)
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ШАГ 3: Идентификация платежа ===\n";
|
||||
|
||||
if (empty($beneficiaryId) || $beneficiaryId === 'your-beneficiary-id-here') {
|
||||
echo "⚠️ Укажите правильный beneficiaryId!\n";
|
||||
echo " Получите список бенефициаров через:\n";
|
||||
echo " >>> \$baas->send(new GetBeneficiariesRequest())->dto()\n";
|
||||
return;
|
||||
}
|
||||
|
||||
$identifyData = new IdentifyRequestData(
|
||||
transactionId: $transaction->id,
|
||||
accountNumber: $nominalAccountNumber,
|
||||
amountDistribution: DataCollection::create([
|
||||
new AmountDistributionData(
|
||||
beneficiaryId: $beneficiaryId,
|
||||
amount: $transaction->amount
|
||||
)
|
||||
], AmountDistributionData::class)
|
||||
);
|
||||
|
||||
$identifyResponse = $baas->send(
|
||||
new IdentifyTransactionRequest($transaction->id, $identifyData)
|
||||
);
|
||||
|
||||
$result = $identifyResponse->dto();
|
||||
echo "✅ Запрос создан!\n";
|
||||
echo " Identify ID: {$result->identifyId}\n";
|
||||
echo " Статус: {$result->status}\n\n";
|
||||
|
||||
// =============================================================================
|
||||
// ШАГ 4: Проверка статуса
|
||||
// =============================================================================
|
||||
|
||||
echo "=== ШАГ 4: Проверка статуса ===\n";
|
||||
|
||||
$statusResponse = $baas->send(
|
||||
new GetIdentifyStatusRequest($result->identifyId)
|
||||
);
|
||||
|
||||
$status = $statusResponse->dto();
|
||||
echo "Текущий статус: {$status->status}\n\n";
|
||||
|
||||
echo "=== РЕЗУЛЬТАТ ===\n";
|
||||
echo "✅ SBP QR: {$qrData->payload}\n";
|
||||
echo "✅ Транзакция: {$transaction->id}\n";
|
||||
echo "✅ Бенефициар: $beneficiaryId\n";
|
||||
echo "✅ Сумма: {$transaction->amount} RUB\n";
|
||||
echo "✅ Статус идентификации: {$status->status}\n";
|
||||
|
||||
} catch (\Exception $e) {
|
||||
echo "\n❌ Ошибка: {$e->getMessage()}\n";
|
||||
echo "Класс: " . get_class($e) . "\n";
|
||||
}
|
||||
|
||||
echo "\n=== Готово ===\n";
|
||||
@@ -75,7 +75,8 @@ class AlfaBankConnector extends Connector implements HasPagination
|
||||
{
|
||||
return [
|
||||
'Accept' => 'application/json',
|
||||
'Content-Type' => 'application/json',
|
||||
// Content-Type не устанавливаем здесь, каждый Request сам определяет его через трейты (HasJsonBody, HasFormBody и т.д.)
|
||||
// OAuth токен запросы требуют application/x-www-form-urlencoded
|
||||
];
|
||||
}
|
||||
|
||||
@@ -97,7 +98,7 @@ class AlfaBankConnector extends Connector implements HasPagination
|
||||
}
|
||||
|
||||
// login() kept for ClientCredentials compatibility (legacy/simple flow)
|
||||
public function login(?string $clientId = null, ?string $clientSecret = null): static
|
||||
public function login(?string $clientId = null, ?string $clientSecret = null, array $scopes = []): static
|
||||
{
|
||||
$oauthConfig = $this->oauthConfig();
|
||||
|
||||
@@ -113,7 +114,7 @@ class AlfaBankConnector extends Connector implements HasPagination
|
||||
throw new \RuntimeException("Client ID/Secret not configured.");
|
||||
}
|
||||
|
||||
$request = new \Saloon\Http\OAuth2\GetClientCredentialsTokenRequest($oauthConfig);
|
||||
$request = new \Saloon\Http\OAuth2\GetClientCredentialsTokenRequest($oauthConfig, $scopes);
|
||||
|
||||
$response = $this->send($request);
|
||||
|
||||
|
||||
@@ -15,18 +15,19 @@ namespace Apoloz\AlfaBank\Sbp\Requests;
|
||||
// <BELIEF_STATE "A Request class needs its specific data transfer objects (DTOs) for the request payload and the response. It also requires Saloon's base classes and traits to function within the framework.">
|
||||
use Apoloz\AlfaBank\Sbp\Data\QrCodeRequestData;
|
||||
use Apoloz\AlfaBank\Sbp\Data\QrCodeResponseData;
|
||||
use Apoloz\AlfaBank\Utils\CmsSigner;
|
||||
use Saloon\Contracts\Body\HasBody;
|
||||
use Saloon\Enums\Method;
|
||||
use Saloon\Http\Request;
|
||||
use Saloon\Http\Response;
|
||||
use Saloon\Traits\Body\HasJsonBody;
|
||||
use Saloon\Traits\Body\HasStringBody;
|
||||
// </SECTION>
|
||||
|
||||
// <SECTION name="class_definition" intent="Define a class that encapsulates all information for the 'Get QR Code' API call.">
|
||||
// <BELIEF_STATE "Using a dedicated class for each API request is a core Saloon pattern. It organizes the endpoint, method, body, and response handling into a single, reusable, and testable unit. Implementing `HasBody` and using `HasJsonBody` signals its intent to send a JSON payload.">
|
||||
class GetQrCodeRequest extends Request implements HasBody
|
||||
{
|
||||
use HasJsonBody;
|
||||
use HasStringBody;
|
||||
|
||||
// <SECTION name="http_method_declaration" intent="Declare the HTTP verb for this API operation.">
|
||||
// <BELIEF_STATE "The SBP specification requires a POST request to generate a QR code, as the operation creates a new resource on the server.">
|
||||
@@ -44,15 +45,19 @@ class GetQrCodeRequest extends Request implements HasBody
|
||||
// <BELIEF_STATE "Centralizing the endpoint path in this method makes the SDK resilient to API changes. If the endpoint moves from '/qrcs' to '/v2/qrcs', this is the only line that needs to be updated for this specific request.">
|
||||
public function resolveEndpoint(): string
|
||||
{
|
||||
return '/qrcs';
|
||||
return '/';
|
||||
}
|
||||
// </SECTION>
|
||||
|
||||
// <SECTION name="request_body_builder" intent="Prepare the request payload for transmission.">
|
||||
// <BELIEF_STATE "This method delegates the serialization logic to the `QrCodeRequestData` DTO. This is a key decoupling principle: the Request class shouldn't know *how* to serialize the data (e.g., mapping camelCase to PascalCase); it should only know that the DTO is responsible for providing a valid array representation.">
|
||||
protected function defaultBody(): array
|
||||
// <BELIEF_STATE "This method builds the request body by adding the required 'command' field and merging the DTO data. The SBP API requires the 'command' field to identify the operation type. The 'GetQRCd' command is used for both static and dynamic QR code generation.">
|
||||
protected function defaultBody(): string
|
||||
{
|
||||
return $this->data->toArray();
|
||||
$bodyArray = array_merge(
|
||||
['command' => 'GetQRCd'],
|
||||
$this->data->toArray()
|
||||
);
|
||||
return CmsSigner::canonicalize($bodyArray);
|
||||
}
|
||||
// </SECTION>
|
||||
|
||||
|
||||
@@ -94,14 +94,17 @@ class SbpConnector extends Connector implements HasPagination
|
||||
public function boot(PendingRequest $pendingRequest): void
|
||||
{
|
||||
$bodyRepo = $pendingRequest->body();
|
||||
if (!$bodyRepo || empty($bodyRepo->all())) {
|
||||
if (!$bodyRepo) {
|
||||
return;
|
||||
}
|
||||
$body = $bodyRepo->all();
|
||||
|
||||
// SBP requires minified JSON for signing. Canonicalization (JCS) is the formal term.
|
||||
$jsonBody = CmsSigner::canonicalize($body);
|
||||
|
||||
// Get body as string (for HasStringBody requests)
|
||||
$bodyString = $bodyRepo->all();
|
||||
if (is_array($bodyString)) {
|
||||
// Fallback for array bodies (HasJsonBody)
|
||||
$bodyString = CmsSigner::canonicalize($bodyString);
|
||||
}
|
||||
|
||||
// Sign the body using the configured private key (prefer signing key, fallback to transport key).
|
||||
$keyPath = $this->alfaConfig->signingPrivateKeyPath ?: $this->alfaConfig->privateKeyPath;
|
||||
|
||||
@@ -118,18 +121,12 @@ class SbpConnector extends Connector implements HasPagination
|
||||
}
|
||||
|
||||
$signature = '';
|
||||
openssl_sign($jsonBody, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
openssl_sign($bodyString, $signature, $privateKey, OPENSSL_ALGO_SHA256);
|
||||
$signatureBase64 = base64_encode($signature);
|
||||
|
||||
// Mutate the request to add SBP-specific headers and set the exact signed body.
|
||||
// Mutate the request to add SBP-specific headers.
|
||||
$pendingRequest->headers()->add('Authorization', $signatureBase64);
|
||||
$pendingRequest->headers()->add('key-name', $this->keyNameAlias);
|
||||
// The SBP API documentation specifies this content-type for signed requests.
|
||||
$pendingRequest->headers()->add('Content-Type', 'application/x-www-form-urlencoded');
|
||||
|
||||
// Note: Saloon/Guzzle might re-serialize the body.
|
||||
// For strict SBP signing, ensuring the body sent matches $jsonBody exactly is crucial.
|
||||
// Saloon's HasJsonBody trait usually handles this fine as long as array order is preserved.
|
||||
}
|
||||
|
||||
public function withCaBundle(string $path): static
|
||||
|
||||
Reference in New Issue
Block a user