# Laravel сайтына Kaspi QR қосу

> Qut Pay счётын жасап, сатып алушыны payUrl-ге жіберетін контроллер; $request->getContent() шикі денесі бойынша HMAC тексеретін, CSRF-тен шығарылған, тапсырысты идемпотентті жаңартатын webhook маршруты. PHP SDK немесе Http::post арқылы, алдымен sandbox-та.

## Қысқаша

Laravel-де Qut Pay арқылы Kaspi QR интеграциясы — екі маршрут. Біріншісі `X-API-Key` тақырыбымен `POST https://api.qut.kz/api/v1/invoices` сұрауымен счёт жасап, `redirect()->away($inv['payUrl'])` жасайды. Екіншісі — `routes/api.php` ішіндегі `POST /qutpay/webhook` (CSRF-сіз): `X-Webhook-Signature` қолтаңбасын **шикі дене** `$request->getContent()` бойынша тексереді, тапсырысты `externalOrderId` бойынша табады, оны бір рет төленді деп белгілеп, `200` қайтарады. Кілт пен құпия `.env`-те тұрады, алғашқы сынақ — `qp_test_…` кілтімен sandbox-та.

## Дайындық

1. https://qut.kz/app кабинетінде API кілтін жасаңыз (sandbox үшін `qp_test_…`, нақты режим үшін `qp_live_…`) және Интеграциялар бөлімінде `https://site.kz/api/qutpay/webhook` webhook адресін қосыңыз; құпия бір рет көрсетіледі — [webhook баптау](/kb/webhook-setup).
2. `.env` ішінде: `QUTPAY_API_KEY=…` және `QUTPAY_WEBHOOK_SECRET=…`; `config/services.php` ішінде: `'qutpay' => ['key' => env('QUTPAY_API_KEY'), 'webhook_secret' => env('QUTPAY_WEBHOOK_SECRET')]`. Кілт Blade-шаблондарға, JS-ке және репозиторийге түспеуі керек.
3. [PHP SDK](/kb/sdk-php) жүктеп алыңыз (https://api.qut.kz/downloads/qutpay-sdk-php.zip) — бір файл `src/QutPay.php`, PHP 7.4+, тәуелділіктер жоқ. SDK-сыз да бәрі `Http::withHeaders(['X-API-Key' => …])->post(…)` арқылы істеледі.
4. Тапсырыстар кестесіне `qutpay_invoice_id` және `paid_at` өрістерін қосыңыз.

## Код: төлем контроллері мен webhook

```php
// routes/web.php:  Route::post('/orders/{order}/pay', [PayController::class, 'pay']);
// routes/api.php:  Route::post('/qutpay/webhook', [PayController::class, 'webhook']);  // api тобы, CSRF жоқ

class PayController extends Controller
{
    public function pay(Order $order)
    {
        $qp  = new \QutPay\Client(config('services.qutpay.key'));
        $inv = $qp->createInvoice([
            'amount'          => $order->total,
            'description'     => "Тапсырыс №{$order->id}",
            'externalOrderId' => (string) $order->id,
            'successUrl'      => route('orders.show', $order),
            'idempotencyKey'  => "order-{$order->id}",   // қайта басу екінші счёт жасамайды
        ]);
        $order->update(['qutpay_invoice_id' => $inv['id']]);

        return redirect()->away($inv['payUrl']);         // QR мен «Kaspi-де төлеу» батырмасы бар бет
    }

    public function webhook(Request $request)
    {
        $raw = $request->getContent();                   // шикі дене, $request->all() ЕМЕС
        if (!\QutPay\Webhook::verify(config('services.qutpay.webhook_secret'), $raw, $request->headers->all())) {
            return response()->noContent(401);
        }
        $e = json_decode($raw, true);
        if (($e['event'] ?? '') === 'invoice.paid') {
            $inv = $e['invoice'];
            DB::transaction(function () use ($inv) {
                $order = Order::whereKey($inv['externalOrderId'])->lockForUpdate()->first();
                if ($order && $order->paid_at === null && (float) $inv['amount'] === (float) $order->total) {
                    $order->update(['paid_at' => now(), 'qutpay_invoice_id' => $inv['id']]);
                }
            });
        }
        return response()->noContent(200);                // 8 секунд ішінде, әйтпесе қайталанады
    }
}
```

SDK-сыз қолтаңба былай есептеледі: `hash_equals('sha256=' . hash_hmac('sha256', $request->header('X-Webhook-Timestamp') . '.' . $raw, $secret), $request->header('X-Webhook-Signature', ''))`, қосымша `X-Webhook-Timestamp` 5 минуттан ескі болса қабылдамау.

## Неге назар аудару керек

| Жағдай | Неге маңызды |
|---|---|
| Webhook маршруты `routes/api.php` ішінде | `web` тобында CSRF іске қосылып, Laravel 419 қайтарады; біз жеткізуді сәтсіз деп санап, ~2 сағат ішінде 11 ретке дейін қайталаймыз. Маршрут бәрібір `web`-те болса, оны `VerifyCsrfToken` ерекшеліктеріне қосыңыз |
| `$request->all()` емес, `$request->getContent()` | HMAC байттар бойынша есептеледі; қайта жиналған JSON ешқашан сәйкес келмейді — [қолтаңба сәйкес келмейді](/kb/webhook-signature-mismatch) |
| Идемпотенттілік | Бір оқиға екі рет келуі мүмкін; жол блокировкасы астындағы `paid_at === null` тексеруі қайталауды зиянсыз етеді — [идемпотенттілік](/kb/idempotency) |
| «Рахмет» беті | `successUrl`-ге оралу — төлемнің дәлелі емес; «төленді» деп базадағы `paid_at` немесе `$qp->getInvoice($id, true)` бойынша көрсетіңіз |

SDK қателері `->status` және `->errorCode` өрістері бар `\QutPay\ApiException` түрінде келеді (`invalid_amount`, `tariff_limit_reached`, `kaspi_session_expired` …) — [қателер каталогы](/kb/error-catalog).

## Sandbox-та тексеру

1. `qp_test_…` кілтімен `/orders/1/pay` ашыңыз — төлем бетіне бағытталасыз.
2. Төлемді имитациялаңыз: `$qp->simulateInvoice($id, 'paid')` немесе `{"status":"paid"}` денесімен `POST /api/v1/invoices/{id}/simulate` — webhook қолтаңбасымен нағыз күйінде кетеді — [sandbox](/kb/sandbox-simulate).
3. Тапсырыста `paid_at` пайда болғанына, ал қайталанған жеткізу (кабинеттегі тексеру батырмасы немесе сұрауды қайталау) ештеңе өзгертпейтініне көз жеткізіңіз.
4. Кассирді қосқан соң `qp_live_…` кілтіне ауысыңыз — [іске қосу алдындағы тексеру тізімі](/kb/going-live-checklist).

## Сұрақ-жауап

**SDK-сыз, тек `Http::post` арқылы бола ма?** Иә: `Http::withHeaders(['X-API-Key' => config('services.qutpay.key'), 'Idempotency-Key' => "order-{$order->id}"])->post('https://api.qut.kz/api/v1/invoices', [...])->json()` — өрістер сол: `amount`, `externalOrderId`, `description`, `successUrl`, `failUrl`, `metadata` — [счёттың барлық өрістері](/kb/create-invoice-api).

**Локалда webhook жетпейді.** Нақты режимде адрес нағыз домендегі `https` болуы керек; sandbox-та `http` және туннель адрестері жарайды. Жеткізу журналы — кабинетте, Интеграциялар бөлімінде.

**Webhook болса, polling керек пе?** Төлемді күту бетінде 5–10 секунд сайын өз серверіңізден сұрауға болады, ал ол — `GET /invoices/{id}`; бұл сатып алушыға ыңғайлылық, ақиқат көзі webhook болып қалады — [webhook пе, сұрау ма](/kb/polling-vs-webhook).
