# Node.js SDK

> Qut Pay-дің Node.js клиентін орнату, счёт жасау, күйін сұрау, қайтару және webhook қолтаңбасын тексеру. Толық жұмыс істейтін Express мысалы raw body-мен және қате өңдеу.

## Қысқаша

Node.js SDK — бір файлдан тұратын, сыртқы тәуелділігі жоқ ESM модулі. Node.js 18-ден жоғары нұсқада жұмыс істейді (ішінде кіріктірілген `fetch` пен `node:crypto` қолданылады). Екі нәрсені береді: `QutPay` клиенті және `verifyWebhook` функциясы.

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

SDK міндетті емес — бәрін кәдімгі `fetch` арқылы да жасауға болады. Бірақ қолтаңба тексеруді өз қолыңызбен жазбай, дайынын алғаныңыз дұрыс.

## Орнату

Архивті жүктеп алып, жобаңыздағы бір қалтаға (мысалы `vendor/qutpay-sdk`) ашыңыз да, `package.json` ішіне жергілікті тәуелділік ретінде қосыңыз:

```bash
npm install ./vendor/qutpay-sdk
```

Немесе `index.js` файлын жобаңызға тікелей көшіріп, жолы бойынша импорттаңыз. Пакет атауы — `@qutpay/sdk`, түрі — ESM (`"type": "module"`), сондықтан CommonJS жобасында `await import('@qutpay/sdk')` арқылы алыңыз.

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

```js
import { QutPay, verifyWebhook } from '@qutpay/sdk';

const qp = new QutPay({
  apiKey: process.env.QUTPAY_API_KEY,   // qp_live_… немесе qp_test_…
  baseUrl: 'https://api.qut.kz',        // әдепкі мән, өзгертудің қажеті жоқ
  timeoutMs: 20000,                     // әдепкі 20 секунд
});
```

`apiKey` берілмесе конструктор бірден қате лақтырады. Кілт **тек серверде** тұруы керек — браузерге, мобильді қосымшаға немесе жария репозиторийге салмаңыз.

## Счёт жасау

```js
const inv = await 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 тақырыбына кетеді
});

console.log(inv.id, inv.status, inv.payUrl, inv.expiresAt);
```

Жауапта `id`, `status`, `payUrl`, `qrUrl`, `deepLink`, `qrImageUrl`, `expiresAt` болады. Клиентті `inv.payUrl` адресіне бағыттасаңыз жеткілікті.

`kind: 'phone'` таңдасаңыз `customer.phone` міндетті, сома бүтін теңге болуы керек.

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

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

```js
const fresh = await qp.getInvoice(inv.id, { live: true });
if (fresh.status === 'paid') await markOrderPaid('4471');

await qp.refundInvoice(inv.id, 5000, 'Тауардың бір бөлігі қайтарылды');
```

SDK-да **жазылым, bulk және форма-хук әдістері жоқ.** Оларды тікелей HTTP арқылы шақырыңыз:

```js
const resp = await fetch('https://api.qut.kz/api/v1/invoices/bulk', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.QUTPAY_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ invoices: [{ amount: 1000, externalOrderId: 'a-1' }] }),
});
const result = await resp.json(); // HTTP 207, әр элемент бойынша нәтиже
```

## Webhook қолтаңбасын тексеру

```js
verifyWebhook({ secret, rawBody, headers, toleranceSec = 300 }) // → true / false
```

`rawBody` — **денесі өзгертілмеген байттар немесе жол**. JSON-ға айналдырылған объект жарамайды: қолтаңба дәл сол байттар бойынша есептеледі. Функция `X-Webhook-Signature` пен `X-Webhook-Timestamp` тақырыптарын өзі оқиды, `sha256=` префиксін тексереді, 5 минуттан ескі жеткізілімді қабылдамайды және салыстыруды уақыт бойынша тұрақты әдіспен жасайды.

## Толық Express мысалы

```js
import express from 'express';
import { QutPay, verifyWebhook } from '@qutpay/sdk';

const app = express();
const qp = new QutPay({ apiKey: process.env.QUTPAY_API_KEY });

// 1) Webhook — express.json() ҚОСЫЛҒАНҒА ДЕЙІН тұруы керек
app.post('/qutpay-webhook', express.raw({ type: '*/*' }), async (req, res) => {
  const ok = verifyWebhook({
    secret: process.env.QUTPAY_WEBHOOK_SECRET,
    rawBody: req.body,          // Buffer
    headers: req.headers,
  });
  if (!ok) return res.sendStatus(401);

  const { event, invoice } = JSON.parse(req.body.toString('utf8'));

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

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

// 2) Қалған маршруттар үшін JSON
app.use(express.json());

app.post('/buy', async (req, res) => {
  const order = await createOrder(req.body);
  try {
    const inv = await qp.createInvoice({
      amount: order.total,
      description: `Тапсырыс №${order.id}`,
      externalOrderId: String(order.id),
      successUrl: `https://site.kz/orders/${order.id}`,
      idempotencyKey: `order-${order.id}`,
    });
    await saveInvoiceId(order.id, inv.id);
    res.redirect(303, inv.payUrl);
  } catch (err) {
    console.error('qutpay', err.status, err.code, err.message);
    res.status(502).render('pay-error');
  }
});

app.listen(3000);
```

Ең жиі кездесетін қате — `express.json()` webhook маршрутынан бұрын тұруы. Сонда `req.body` объектіге айналып кетеді де, қолтаңба ешқашан сәйкес келмейді.

## Қате өңдеу

SDK әр сәтсіз жауапты `Error` түрінде лақтырады, оған екі қосымша өріс қояды:

| Өріс | Мәні |
|---|---|
| `err.status` | HTTP күйі. Желі қатесі мен таймаутта `0` |
| `err.code` | Машиналық код: `invalid_amount`, `invoice_not_found`, `request_rate_limited`… Таймаутта `timeout`, желі үзілсе `network_error` |
| `err.message` | Адамға арналған мәтін |

```js
try {
  await qp.createInvoice({ amount, externalOrderId });
} catch (err) {
  switch (err.code) {
    case 'kaspi_session_expired':
      notifyAdmin('Кассир байланысы үзілді');
      break;
    case 'tariff_limit_reached':
    case 'tariff_daily_burst':
      notifyAdmin('Лимит');
      break;                      // қайталамаңыз
    case 'request_rate_limited':
    case 'timeout':
    case 'network_error':
      await retryLater();         // 1, 2, 4, 8 секунд бэкоффпен
      break;
    default:
      throw err;
  }
}
```

Логикаңызды әрқашан `err.code` мәніне қарап жазыңыз: `message` мәтіні өзгеруі мүмкін, код өзгермейді. Барлық кодтар: [Қателер каталогы](/kb/error-catalog).

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

**npm-нен орнатуға бола ма?** Жоқ, пакет жария тізілімде емес. Архивті жүктеп алып, жергілікті тәуелділік ретінде қосыңыз.

**CommonJS жобасында жұмыс істей ме?** Иә: `const { QutPay } = await import('@qutpay/sdk');`.

**TypeScript-та қолдануға бола ма?** Иә. Модульде JSDoc аннотациялары бар, бірақ бөлек `.d.ts` файлы жоқ — өз типтеріңізді жазуыңызға тура келеді.

**Fastify немесе Koa-да raw body қалай аламын?** Fastify-да `addContentTypeParser('*', { parseAs: 'buffer' }, …)`, Koa-да `koa-bodyparser` орнына `raw-body`. Негізгі шарт — денені өзгертілмеген күйінде алу.

**Сынауды қалай бастаған дұрыс?** Sandbox кілтімен счёт жасап, `qp.simulateInvoice(id, 'paid')` шақырыңыз — webhook шынайы жіберіледі: [Sandbox-та төлемді симуляциялау](/kb/sandbox-simulate).
