Қысқаша
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-та.
Дайындық
- https://qut.kz/app кабинетінде API кілтін жасаңыз (sandbox үшін
qp_test_…, нақты режим үшінqp_live_…) және Интеграциялар бөліміндеhttps://site.kz/api/qutpay/webhookwebhook адресін қосыңыз; құпия бір рет көрсетіледі — webhook баптау. .envішінде:QUTPAY_API_KEY=…жәнеQUTPAY_WEBHOOK_SECRET=…;config/services.phpішінде:'qutpay' => ['key' => env('QUTPAY_API_KEY'), 'webhook_secret' => env('QUTPAY_WEBHOOK_SECRET')]. Кілт Blade-шаблондарға, JS-ке және репозиторийге түспеуі керек.- PHP SDK жүктеп алыңыз (https://api.qut.kz/downloads/qutpay-sdk-php.zip) — бір файл
src/QutPay.php, PHP 7.4+, тәуелділіктер жоқ. SDK-сыз да бәріHttp::withHeaders(['X-API-Key' => …])->post(…)арқылы істеледі. - Тапсырыстар кестесіне
qutpay_invoice_idжәнеpaid_atөрістерін қосыңыз.
Код: төлем контроллері мен webhook
// 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 ешқашан сәйкес келмейді — қолтаңба сәйкес келмейді |
| Идемпотенттілік | Бір оқиға екі рет келуі мүмкін; жол блокировкасы астындағы paid_at === null тексеруі қайталауды зиянсыз етеді — идемпотенттілік |
| «Рахмет» беті | successUrl-ге оралу — төлемнің дәлелі емес; «төленді» деп базадағы paid_at немесе $qp->getInvoice($id, true) бойынша көрсетіңіз |
SDK қателері ->status және ->errorCode өрістері бар \QutPay\ApiException түрінде келеді (invalid_amount, tariff_limit_reached, kaspi_session_expired …) — қателер каталогы.
Sandbox-та тексеру
qp_test_…кілтімен/orders/1/payашыңыз — төлем бетіне бағытталасыз.- Төлемді имитациялаңыз:
$qp->simulateInvoice($id, 'paid')немесе{"status":"paid"}денесіменPOST /api/v1/invoices/{id}/simulate— webhook қолтаңбасымен нағыз күйінде кетеді — sandbox. - Тапсырыста
paid_atпайда болғанына, ал қайталанған жеткізу (кабинеттегі тексеру батырмасы немесе сұрауды қайталау) ештеңе өзгертпейтініне көз жеткізіңіз. - Кассирді қосқан соң
qp_live_…кілтіне ауысыңыз — іске қосу алдындағы тексеру тізімі.
Сұрақ-жауап
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 — счёттың барлық өрістері.
Локалда webhook жетпейді. Нақты режимде адрес нағыз домендегі https болуы керек; sandbox-та http және туннель адрестері жарайды. Жеткізу журналы — кабинетте, Интеграциялар бөлімінде.
Webhook болса, polling керек пе? Төлемді күту бетінде 5–10 секунд сайын өз серверіңізден сұрауға болады, ал ол — GET /invoices/{id}; бұл сатып алушыға ыңғайлылық, ақиқат көзі webhook болып қалады — webhook пе, сұрау ма.