feat(billing): YooKassaDriver — создание платежа и server-to-server сверка

This commit is contained in:
Дмитрий
2026-06-22 20:36:17 +03:00
parent 5c0e3760f6
commit 3fdfa4a2ee
2 changed files with 149 additions and 0 deletions
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace App\Services\Billing\Gateway;
use App\Models\PaymentGateway;
use Illuminate\Support\Facades\Http;
use RuntimeException;
/**
* Драйвер ЮKassa (YooKassa API v3).
* Auth: Basic shopId:secretKey. Idempotence-Key заголовок при создании.
* Чек 54-ФЗ передаётся секцией receipt (включается флагом billing_receipt_enabled).
*/
final class YooKassaDriver implements PaymentGatewayDriver
{
private const BASE = 'https://api.yookassa.ru/v3';
public function createPayment(
PaymentGateway $gateway,
string $amountRub,
string $idempotenceKey,
string $returnUrl,
?array $receipt,
): CreatePaymentResult {
[$shopId, $secret] = $this->creds($gateway);
$payload = [
'amount' => ['value' => $amountRub, 'currency' => 'RUB'],
'capture' => true,
'confirmation' => ['type' => 'redirect', 'return_url' => $returnUrl],
'description' => 'Пополнение баланса Лидерра',
];
if ($receipt !== null) {
$payload['receipt'] = $receipt;
}
$resp = Http::withBasicAuth($shopId, $secret)
->withHeaders(['Idempotence-Key' => $idempotenceKey])
->acceptJson()
->post(self::BASE.'/payments', $payload);
if (! $resp->successful()) {
throw new RuntimeException('YooKassa createPayment failed: HTTP '.$resp->status());
}
$id = (string) $resp->json('id');
$url = (string) $resp->json('confirmation.confirmation_url');
if ($id === '' || $url === '') {
throw new RuntimeException('YooKassa createPayment: пустой id/confirmation_url');
}
return new CreatePaymentResult($id, $url);
}
public function verifyPayment(PaymentGateway $gateway, string $gatewayPaymentId): WebhookVerifyResult
{
[$shopId, $secret] = $this->creds($gateway);
$resp = Http::withBasicAuth($shopId, $secret)
->acceptJson()
->get(self::BASE.'/payments/'.$gatewayPaymentId);
if (! $resp->successful()) {
throw new RuntimeException('YooKassa verifyPayment failed: HTTP '.$resp->status());
}
return new WebhookVerifyResult(
gatewayPaymentId: (string) $resp->json('id'),
status: (string) $resp->json('status'),
amountRub: (string) $resp->json('amount.value'),
paymentMethod: $resp->json('payment_method.type'),
);
}
/** @return array{0:string,1:string} [shopId, secretKey] */
private function creds(PaymentGateway $gateway): array
{
$c = $gateway->credentials();
$shopId = (string) ($c['shop_id'] ?? '');
$secret = (string) ($c['secret_key'] ?? '');
if ($shopId === '' || $secret === '') {
throw new RuntimeException('YooKassa: shop_id/secret_key не настроены');
}
return [$shopId, $secret];
}
}
@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
use App\Models\PaymentGateway;
use App\Services\Billing\Gateway\YooKassaDriver;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Http;
uses(Tests\TestCase::class);
function fakeGateway(): PaymentGateway
{
$gw = new PaymentGateway;
$gw->code = 'yookassa';
$gw->config = Crypt::encrypt(['shop_id' => 'shop_1', 'secret_key' => 'test_secret']);
return $gw;
}
it('создаёт платёж и возвращает id + confirmation_url', function () {
Http::fake([
'api.yookassa.ru/v3/payments' => Http::response([
'id' => '2da2b...test',
'status' => 'pending',
'confirmation' => ['type' => 'redirect', 'confirmation_url' => 'https://yoomoney.ru/checkout/2da2b'],
], 200),
]);
$res = (new YooKassaDriver)->createPayment(
fakeGateway(), '500.00', 'b3f1c2d4-0000-4000-8000-000000000001', 'https://liderra.ru/billing', null
);
expect($res->gatewayPaymentId)->toBe('2da2b...test')
->and($res->confirmationUrl)->toBe('https://yoomoney.ru/checkout/2da2b');
Http::assertSent(function ($request) {
return $request->hasHeader('Idempotence-Key', 'b3f1c2d4-0000-4000-8000-000000000001')
&& $request['amount']['value'] === '500.00'
&& $request['amount']['currency'] === 'RUB'
&& $request['capture'] === true;
});
});
it('сверяет платёж и распознаёт succeeded', function () {
Http::fake([
'api.yookassa.ru/v3/payments/pay_77' => Http::response([
'id' => 'pay_77',
'status' => 'succeeded',
'amount' => ['value' => '1000.00', 'currency' => 'RUB'],
'payment_method' => ['type' => 'bank_card'],
], 200),
]);
$res = (new YooKassaDriver)->verifyPayment(fakeGateway(), 'pay_77');
expect($res->isSucceeded())->toBeTrue()
->and($res->amountRub)->toBe('1000.00')
->and($res->paymentMethod)->toBe('bank_card');
});