# PHP SDK

> Qut Pay-дің PHP клиентін жобаға қосу, счёт жасау, қайтару және webhook қолтаңбасын тексеру. Таза PHP пен Laravel мысалдары, raw body-ны дұрыс алу және ApiException арқылы қате өңдеу.

## Қысқаша

PHP SDK — бір ғана файл: `src/QutPay.php`. PHP 7.4 және одан жоғары нұсқада, `curl` мен `json` кеңейтулерімен жұмыс істейді, басқа тәуелділігі жоқ. Composer арқылы да, қарапайым `require` арқылы да қосуға болады.

Ішінде үш класс бар:

| Класс | Не үшін |
|---|---|
| `\QutPay\Client` | API шақырулары: счёт жасау, оқу, болдырмау, қайтару |
| `\QutPay\Webhook` | Кіріс хабарламаның қолтаңбасын тексеру |
| `\QutPay\ApiException` | Қате: `->status` (HTTP) және `->errorCode` (машиналық код) |

Архив: **https://api.qut.kz/downloads/qutpay-sdk-php.zip**

## Орнату

Ең қарапайымы — архивтегі `src/QutPay.php` файлын жобаңызға көшіріп, тікелей қосу:

```php
require __DIR__ . '/vendor/qutpay/QutPay.php';
```

Composer қолдансаңыз, архивті бір қалтаға ашып, жергілікті репозиторий ретінде көрсетіңіз:

```json
{
  "repositories": [{ "type": "path", "url": "./vendor-local/qutpay-sdk" }],
  "require": { "qutpay/sdk": "*" }
}
```

Пакет атауы — `qutpay/sdk`, автозагрузка PSR-4 бойынша `QutPay\` кеңістігіне бағытталған.

## Инициализация

```php
$qp = new \QutPay\Client(getenv('QUTPAY_API_KEY'));       // baseUrl әдепкі https://api.qut.kz
// Қажет болса: new \QutPay\Client($key, 'https://api.qut.kz', 20);  // үшіншісі — таймаут, секунд
```

Кілт бос болса конструктор `InvalidArgumentException` лақтырады. Кілтті кодқа жазбаңыз — орта айнымалысынан немесе баптау файлынан алыңыз, ал ол файл репозиторийге түспеуі керек.

## Счёт жасау

```php
$inv = $qp->createInvoice([
    'amount'          => 12500,
    'kind'            => 'qr',              // 'qr' (әдепкі) немесе 'phone'
    'description'     => 'Тапсырыс №4471',
    'externalOrderId' => '4471',
    'customer'        => ['name' => 'Айгүл', 'phone' => '77011234567', 'email' => 'a@b.kz'],
    'successUrl'      => 'https://site.kz/ok',
    'failUrl'         => 'https://site.kz/fail',
    'metadata'        => ['branch' => 'almaty-abay'],
    'idempotencyKey'  => 'order-4471',      // Idempotency-Key тақырыбына кетеді
]);

header('Location: ' . $inv['payUrl']);
exit;
```

Жауап — ассоциативті массив: `id`, `status`, `payUrl`, `qrUrl`, `deepLink`, `qrImageUrl`, `expiresAt`.

## Қалған әдістер

| Әдіс | Не істейді |
|---|---|
| `$qp->getInvoice($id)` | Счётты оқиды. `$qp->getInvoice($id, true)` — күйді Kaspi-ден сұрап жаңартады |
| `$qp->listInvoices(['status' => 'paid', 'limit' => 50])` | Тізім. Сүзгілер: `status`, `externalOrderId`, `search`, `from`, `to`, `limit`, `offset` |
| `$qp->cancelInvoice($id)` | Ашық счётты болдырмайды |
| `$qp->refundInvoice($id, $amount = null, $reason = null)` | Қайтару. `$amount = null` — толық қайтару |
| `$qp->simulateInvoice($id, 'paid')` | Тек sandbox: төлемді имитациялау |
| `$qp->account()` | Ұйым туралы қысқа ақпарат — кілтті тексеруге ыңғайлы |

```php
$fresh = $qp->getInvoice($inv['id'], true);
if ($fresh['status'] === 'paid') {
    markOrderPaid('4471');
}

$qp->refundInvoice($inv['id'], 5000, 'Тауардың бір бөлігі қайтарылды');
```

SDK-да **жазылым, bulk және форма-хук әдістері жоқ.** Оларды тікелей curl арқылы шақырыңыз — эндпоинттер [api.qut.kz/docs](https://api.qut.kz/docs) бетінде.

## Webhook: таза PHP

```php
<?php
require __DIR__ . '/QutPay.php';

$secret = getenv('QUTPAY_WEBHOOK_SECRET');
$raw    = file_get_contents('php://input');   // өзгертілмеген дене

if (!\QutPay\Webhook::verify($secret, $raw, getallheaders())) {
    http_response_code(401);
    exit;
}

$e       = json_decode($raw, true);
$event   = $e['event'] ?? '';
$invoice = $e['invoice'] ?? [];

// Идемпотенттілік: бір оқиға екі рет келуі мүмкін
if (!alreadyHandled($invoice['id'], $invoice['status'])) {
    if ($event === 'invoice.paid')     markOrderPaid($invoice['externalOrderId'], $invoice);
    if ($event === 'invoice.refunded') markOrderRefunded($invoice['externalOrderId']);
    rememberHandled($invoice['id'], $invoice['status']);
}

http_response_code(200);   // 2xx бермесеңіз 11 рет қайталанады
```

`Webhook::verify($secret, $rawBody, $headers, $toleranceSec = 300)` тақырыптарды регистрге қарамай іздейді, `sha256=` префиксін тексереді, 5 минуттан ескі жеткізілімді қабылдамайды және `hash_equals` арқылы салыстырады.

`getallheaders()` кейбір FastCGI баптауларында болмайды. Ондай жағдайда тақырыптарды өзіңіз жинаңыз:

```php
$headers = [
    'X-Webhook-Signature' => $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '',
    'X-Webhook-Timestamp' => $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '',
];
```

## Webhook: Laravel

Laravel-де денені `$request->getContent()` арқылы алыңыз — ол өзгертілмеген жол қайтарады. `$request->all()` **жарамайды**.

```php
// routes/api.php
Route::post('/qutpay-webhook', [QutPayController::class, 'handle']);
```

```php
// app/Http/Controllers/QutPayController.php
use Illuminate\Http\Request;

class QutPayController extends Controller
{
    public function handle(Request $request)
    {
        $raw = $request->getContent();
        $ok  = \QutPay\Webhook::verify(
            config('services.qutpay.webhook_secret'),
            $raw,
            $request->headers->all()        // мәндері массив — verify оны түсінеді
        );
        if (!$ok) {
            return response()->noContent(401);
        }

        $e = json_decode($raw, true);
        ProcessQutPayEvent::dispatch($e);   // ауыр жұмысты кезекке беріңіз

        return response()->noContent(200);
    }
}
```

Екі нәрсені ұмытпаңыз:

1. **CSRF.** Маршрутты `routes/api.php` ішіне қойыңыз немесе `VerifyCsrfToken` ішіндегі `$except` тізіміне қосыңыз — әйтпесе Laravel 419 қайтарады да, біз оны сәтсіз жеткізілім деп есептейміз.
2. **Жылдам жауап.** Ауыр логиканы (хат жіберу, есеп жаңарту) кезекке шығарып, 2xx-ті бірден қайтарыңыз.

Счёт жасау контроллерінде:

```php
$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}",
]);

return redirect()->away($inv['payUrl']);
```

## Қате өңдеу

Кез келген сәтсіз жауапта SDK `\QutPay\ApiException` лақтырады:

| Өріс | Мәні |
|---|---|
| `->status` | HTTP күйі. Желі қатесінде `0` |
| `->errorCode` | Машиналық код: `invalid_amount`, `invoice_not_found`, `request_rate_limited`… Желі үзілсе `network_error` |
| `->getMessage()` | Адамға арналған мәтін |

```php
try {
    $inv = $qp->createInvoice([...]);
} catch (\QutPay\ApiException $e) {
    switch ($e->errorCode) {
        case 'kaspi_session_expired':
            notifyAdmin('Кассир байланысы үзілді');
            break;
        case 'tariff_limit_reached':
        case 'tariff_daily_burst':
            notifyAdmin('Лимит');          // қайталамаңыз
            break;
        case 'request_rate_limited':
        case 'network_error':
            retryLater();                   // 1, 2, 4, 8 секунд бэкоффпен
            break;
        default:
            report($e);
    }
    abort(502);
}
```

Шешімді әрқашан `errorCode` бойынша қабылдаңыз: мәтін өзгеруі мүмкін, код өзгермейді. Толық тізім: [Қателер каталогы](/kb/error-catalog).

## Жиі қойылатын сұрақтар

**Packagist-тен орнатуға бола ма?** Жоқ, пакет жария тізілімде емес. Архивті жүктеп, `path` репозиторийі ретінде қосыңыз немесе файлды көшіріп алыңыз.

**Guzzle керек пе?** Жоқ. SDK тек `curl` кеңейтуін пайдаланады.

**WordPress дүкенім бар, осы SDK керек пе?** Жоқ. WooCommerce үшін дайын плагин бар: [WooCommerce плагині](/kb/woocommerce).

**Symfony-де raw body қалай аламын?** Laravel-дегідей: `$request->getContent()` және `$request->headers->all()`.

**Қолтаңба сәйкес келмейді.** Дене бір жерде өзгертілген: денені JSON-ға айналдырып, қайта жолға түрлендірмеңіз, кэш пен қорғаныс плагиндері денеге тимесін. Толығы: [Webhook қолтаңбасы сәйкес келмейді](/kb/webhook-signature-mismatch).
