diff --git a/app/app/Services/Billing/Gateway/YooKassaDriver.php b/app/app/Services/Billing/Gateway/YooKassaDriver.php new file mode 100644 index 00000000..e72f459f --- /dev/null +++ b/app/app/Services/Billing/Gateway/YooKassaDriver.php @@ -0,0 +1,89 @@ +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]; + } +} diff --git a/app/tests/Unit/Billing/YooKassaDriverTest.php b/app/tests/Unit/Billing/YooKassaDriverTest.php new file mode 100644 index 00000000..a7e5fbfa --- /dev/null +++ b/app/tests/Unit/Billing/YooKassaDriverTest.php @@ -0,0 +1,60 @@ +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'); +});