IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime29/69-dars21 daqiqa
Mundarija (23)

Payme Merchant API: JSON-RPC, oltita metod va 12 soatlik timeout

Qisqacha: Payme'da siz so'rov yubormaysiz — Payme serveri sizning endpoint'ingizni JSON-RPC 2.0 bilan chaqiradi: avval "to'lash mumkinmi?" (CheckPerformTransaction), keyin "tranzaksiya yarat" (CreateTransaction), pul yechilgach "bajar" (PerformTransaction). Javob har doim HTTP 200, xato esa JSON ichidagi error.code bilan. Payme text/json yuboradi — express.json() uni o'qimaydi. Har metod takroriy chaqiruvga o'sha javobni qaytarishi, 12 soatdan eski tranzaksiyani bekor qilishi va parallel so'rovlarda ikki marta to'lamasligi kerak. Hammasi lokal simulyator bilan sinalgan; haqiqiy Payme — sinalmagan.

Bu darsda

  • JSON-RPC 2.0 so'rov va javob formatini o'qiysiz.
  • Payme'ning oltita metodini va tranzaksiya holatlarini bilasiz.
  • text/json tuzog'ini va Basic auth tekshiruvini ko'rasiz.
  • 12 soatlik timeout'ni to'g'ri saqlashni (ROLLBACK tuzog'i) o'rganasiz.
  • Parallel so'rovlar va takroriy chaqiruvlarni test bilan tekshirasiz.
  • payme/merchant.ts modulini yozasiz.

Oldin bilishingiz kerak: To'lov ekotizimi va umumiy oqim, Mashhur middleware'lar (express.json), crypto 1: tasodif, hash va HMAC (timingSafeEqual), Qulflar: row, table va advisory.

Oldingi darsda server-to-server oqimini umumiy ko'rdik. Endi uni birinchi haqiqiy provayder — Payme hujjati bo'yicha yozamiz.

Diqqat: Haqiqiy Payme bilan ishlash uchun Payme Business bilan hamkorlik shartnomasi va kassa (test kabineti) kerak. Bu darsning kodi lokal simulyator bilan sinalgan — Payme serverining so'rovlarini hujjat bo'yicha takrorlaydigan soxta server. Hujjat: developer.help.paycom.uz (kanon 2026-10-27 da o'qigan). Haqiqiy Payme sandbox — sinalmagan.

1. Nega bu kerak?

Payme — O'zbekistondagi eng keng tarqalgan to'lov ilovalaridan biri. Do'konga Payme ulash — mijoz Payme ilovasida "do'kon X, buyurtma 42" ni topib to'lashi. Bu paytda Payme sizdan buyurtma haqida so'raydi. Agar serveringiz noto'g'ri javob bersa:

  • mijoz to'laydi, lekin buyurtma "to'lanmagan" bo'lib qoladi;
  • yoki bitta buyurtma uchun ikki marta pul yechiladi;
  • yoki Payme xatolar tufayli integratsiyani qabul qilmaydi.

Payme hujjati har metodning javobini va har xato kodini aniq belgilaydi. Bu dars — shu spetsifikatsiyani kodga aylantirish.

2. JSON-RPC 2.0

JSON-RPC — HTTP ustidagi oddiy masofaviy chaqiruv (remote procedure call) protokoli. REST'dagidek har amal uchun alohida URL va metod emas: bitta URL, bitta POST, amal nomi esa tanadagi method maydonida. Kanon simulyatori yuborgan va olgan haqiqiy so'rovlar (<payme-id> va <ms> — har safar boshqa qiymatlar, kanon ularni almashtirgan):

text
Payme so'rov: {"jsonrpc":"2.0","id":1,"method":"CreateTransaction","params":{"id":"<payme-id>","time":<ms>,"amount":1250000,"account":{"order_id":"1"}}}
Payme javob (HTTP 200): {"jsonrpc":"2.0","id":1,"result":{"create_time":<ms>,"transaction":"1","state":1}}
Payme xato: {"jsonrpc":"2.0","id":1,"error":{"code":-31050,"message":{"ru":"Неверный код заказа","uz":"Buyurtma raqami noto'g'ri","en":"Order not found"},"data":"order_id"}}

Bitta so'rovning "anatomiyasi":

Qism Payme'da Siz nima qilasiz
kim chaqiradi Payme serveri → sizning endpoint URL'ni kabinetga yozasiz
HTTP metod doim POST boshqasiga -32300
Content-Type text/json tanani matn sifatida o'qiysiz
Authorization Basic login va kassa kaliti timingSafeEqual bilan tekshirasiz
tana JSON-RPC: method + params metodni chaqirasiz
javob doim HTTP 200, result yoki error xatoni ham 200 bilan

Jadvalning har qatorini quyidagi bo'limlarda birma-bir ko'ramiz. Avval tananing o'zi — uchta qoida:

  1. Javobdagi id — so'rovdagi id (JSON-RPC so'rov raqami). Payme javoblarni shu bilan juftlaydi.
  2. Javobda yoki result, yoki error — ikkalasi birga emas.
  3. error.message — uch tilda (ru, uz, en): Payme uni mijozga ko'rsatadi. data — qaysi maydon xato (order_id).

E'tibor bering: params.id — bu Payme tranzaksiyasining id'si (24 belgili satr), JSON-RPC id dan boshqa narsa. Javobdagi transaction esa — bizning tranzaksiya id'imiz.

2.1 Javob doim HTTP 200

Payme hujjati: javob har doim HTTP 200 bo'lsin, xato bo'lsa ham. 200 bo'lmagan javobni Payme "tizim xatosi" (-32400) deb hisoblaydi. REST'da o'rgangan "404 — topilmadi" odati bu yerda ishlamaydi: buyurtma topilmasa — HTTP 200 va error.code = -31050.

3. Birinchi tuzoq: text/json

Payme so'rovni Content-Type: text/json; charset=UTF-8 bilan yuboradi. Kanon tajribasi oddiy Express server bilan tekshirdi:

text
express.json() + text/json → {"body":null,"tur":"text/json; charset=UTF-8"} — tana O'QILMADI (sukut type: application/json)

express.json() sukutda faqat application/json ni o'qiydi (Mashhur middleware'lar). Boshqa tur kelsa — jim o'tkazib yuboradi, req.body bo'sh. Kod "params yo'q" deb -32600 qaytaradi va siz soatlab "Payme noto'g'ri yuboryapti" deb o'ylaysiz.

Kanon yechimi — tanani matn sifatida o'qish va o'zimiz parse qilish:

ts
r.all(
  "/",
  express.text({ type: () => true, limit: "64kb" }),
  async (req, res) => { /* JSON.parse(req.body) ... */ },
);

Ikki foyda bor. Birinchisi — har qanday Content-Type o'qiladi. Ikkinchisi — buzilgan JSON uchun aniq -32700 (Parse error) qaytarish mumkin. express.json() bunday holda Express xato ishlovchisiga 400 bilan tushardi — HTTP 200 qoidasi buzilardi. r.all — GET ham shu ishlovchiga keladi va -32300 ("POST emas") oladi.

4. Autentifikatsiya: Basic auth

Har so'rovda Authorization: Basic base64(login:kalit) sarlavhasi. Kalit — kassa kaliti (kabinetda: test va prod uchun alohida). Login Payme mutaxassisi beradi; sandbox namunalarida odatda Paycom — kanon testi shuni ishlatadi, hujjatda bu nom aniq yozilmagan.

js
const sarlavha = "Basic " + btoa("Paycom:SOXTA-kalit");
console.log(sarlavha);
const m = /^Basic (.+)$/.exec(sarlavha);
console.log(atob(m[1]));

Konsolda:

text
Basic UGF5Y29tOlNPWFRBLWthbGl0
Paycom:SOXTA-kalit

Base64 — shifr emas, oddiy kodlash: sarlavhani ko'rgan har kim kalitni o'qiydi. Shuning uchun faqat HTTPS. Serverda solishtirish timingSafeEqual bilan (crypto 1: tasodif, hash va HMAC) — oddiy === birinchi farq qilgan belgida to'xtaydi va javob vaqti orqali kalitni harfma-harf topishga imkon beradi:

ts
function authOk(
  sarlavha: string | undefined,
  login: string,
  kalit: string,
): boolean {
  const m = /^Basic (.+)$/.exec(sarlavha ?? "");
  if (!m?.[1]) return false;
  const kelgan = Buffer.from(m[1], "base64");
  const kutilgan = Buffer.from(`${login}:${kalit}`);
  return (
    kelgan.length === kutilgan.length &&
    timingSafeEqual(kelgan, kutilgan)
  );
}

Uzunlik avval tekshiriladi: timingSafeEqual turli uzunlikdagi buferlarda xato tashlaydi. Auth xatosi — -32504 ("huquq yetarli emas").

Hujjat yana bir qatlamni aytadi: so'rovlar faqat Payme IP manzillaridan (185.234.113.1–15) keladi. Kanon buni ixtiyoriy ruxsatIp sozlamasi qilib qo'ydi — odatda reverse proxy'da cheklanadi.

5. Oltita metod

Metod Qachon Bizning ish
CheckPerformTransaction to'lovdan oldin buyurtma bormi, summa mosmi, holati to'g'rimi
CreateTransaction to'lov boshlanganda tranzaksiya yaratilgan, buyurtma kutilmoqda
PerformTransaction pul yechilgach bajarilgan, buyurtma tolangan, ledger
CancelTransaction bekor qilish yaratilgan → -1; bajarilgan → -2 (refund)
CheckTransaction holatni so'rash vaqtlar, holat, sabab
GetStatement davr hisoboti davrdagi tranzaksiyalar ro'yxati

Hujjatda yana SetFiscalData (fiskal chek ma'lumoti) bor — kanonga kirmadi.

5.1 Tranzaksiya holatlari

state Ma'nosi Bizning holat
1 yaratilgan, pul hali yechilmagan yaratilgan
2 bajarilgan bajarilgan
-1 bajarilmasdan bekor qilingan bekor
-2 bajarilgandan keyin bekor (pul qaytarildi) qaytarilgan

Diqqat: Holatlar jadvali va bekor qilish sabablari (reason: 1–5, 10) kanon o'qigan Payme hujjat sahifalarida topilmadi ("Типы данных" sahifasi ochilmadi). Yuqoridagi qiymatlar — keng ishlatiladigan, ochiq integratsiyalarda uchraydigan qiymatlar. Faqat sabab 4 (timeout) metod sahifasida bor. Haqiqiy integratsiyada hujjatdan tekshiring.

5.2 Xato kodlari

Kod Ma'nosi
-32300 so'rov POST emas
-32700 JSON tahlil xatosi
-32600 RPC so'rov noto'g'ri (maydon yo'q)
-32601 metod topilmadi
-32504 huquq yetarli emas (auth)
-32400 tizim xatosi (kutilmagan)
-31001 noto'g'ri summa
-31003 tranzaksiya topilmadi
-31007 bajarilgan, bekor qilib bo'lmaydi
-31008 amalni bajarib bo'lmaydi
-31050 … -31099 buyurtma (account) xatolari

-32xxx — protokol darajasi, -31xxx — Payme biznes xatolari. Protokol kodlarining uchtasi (-32700, -32600, -32601) JSON-RPC 2.0 standartidan olingan; -32300, -32400 va -32504 esa Payme'ning o'z kodlari (standartda yo'q). Kanon -31050 (buyurtma yo'q) va -31051 (buyurtma allaqachon to'langan yoki bekor) ni ishlatadi.

-32400 ga alohida e'tibor: kanon har kutilmagan xatoni (baza uzildi, kodda bug) logga yozadi va keyin -32400 qaytaradi. Kutilmagan xatoni jimgina yutib yuborish — eng yomon odat: Payme "tizim xatosi" ko'radi, siz esa logda hech narsa topmaysiz.

sequenceDiagram
  participant P as Payme
  participant S as Merchant API
  participant DB as Baza
  P->>S: CheckPerformTransaction
  S->>DB: buyurtma, summa, holat
  S-->>P: allow true
  P->>S: CreateTransaction (id, time)
  S->>DB: FOR UPDATE, INSERT 'yaratilgan'
  S-->>P: state 1
  P->>S: CreateTransaction (o'sha id, qayta)
  S-->>P: state 1 (o'sha javob)
  P->>S: PerformTransaction
  S->>DB: 'bajarilgan', 'tolangan', ledger
  S-->>P: state 2

Diagrammadagi takroriy CreateTransaction tasodif emas: Payme javobni olmasa (tarmoq uzildi), xuddi o'sha id bilan qayta so'raydi. Server buni yangi tranzaksiya deb qabul qilsa, -31008 qaytarardi va to'lov to'xtardi. Shuning uchun har metod o'sha id uchun o'sha javobni qaytaradi — bu idempotentlik.

6. CreateTransaction — eng nozik metod

Kanon kodining asosiy qismi (formatlangan nusxa, ma'nosi kanondagidek):

ts
    CreateTransaction: (p) =>
      tranzaksiya(s.pool, async (c) => {
        if (!sonmi(p["time"])) throw X.sorov("time");
        // buyurtma qatori qulflanadi — parallel Create'lar ketma-ket
        const b = await buyurtmaOl(c, p, true);
        const bor = await txOl(c, p["id"], true);
        if (bor) {
          if (bor.holat !== "yaratilgan") throw X.mumkinEmas();
          if (muddatOtdimi(bor)) {
            await timeoutBekor(c, bor);
            return X.mumkinEmas();
          } // throw qilinsa ROLLBACK bekor qilishni ham yo'qotardi
          return {
            create_time: ms(bor.yaratilgan_vaqt),
            transaction: bor.id,
            state: 1,
          };
        }
        tekshir(b, p["amount"]);
        // Bir martalik hisob: buyurtmada boshqa faol tranzaksiya
        // bo'lsa — -31008 (pesochnitsa ssenariysi)
        const faol = await c.query(
          "SELECT 1 FROM tranzaksiyalar WHERE buyurtma_id = $1 AND " +
            "holat = 'yaratilgan'",
          [b.id],
        );
        if (faol.rowCount) throw X.mumkinEmas();
        if (hozir() - p["time"] > TIMEOUT_MS) throw X.mumkinEmas();
        const t = await c.query<TxQator>(
          `INSERT INTO tranzaksiyalar (provayder, tashqi_id,
             buyurtma_id, summa, holat, tashqi_vaqt, yaratilgan_vaqt)
           VALUES ('payme', $1, $2, $3, 'yaratilgan', $4,
             to_timestamp($5 / 1000.0)) RETURNING *`,
          [p["id"], b.id, p["amount"], p["time"], hozir()],
        );
        await c.query(
          "UPDATE buyurtmalar SET holat = 'kutilmoqda', " +
            "yangilangan_vaqt = now() WHERE id = $1",
          [b.id],
        );
        const q = t.rows[0];
        if (!q) throw new Error("INSERT natijasiz");
        return {
          create_time: ms(q.yaratilgan_vaqt),
          transaction: q.id,
          state: 1,
        };
      }).then((r) => {
        if (r instanceof PaymeXato) throw r;
        return r;
      }),

Qadamlar:

  1. Buyurtma qatori FOR UPDATE bilan qulflanadi (buyurtmaOl(c, p, true)). Bir buyurtmaga parallel kelgan so'rovlar navbat bilan bajariladi.
  2. Shu Payme id bilan tranzaksiya bor bo'lsa — yangi yaratilmaydi. Holati yaratilgan bo'lsa, o'sha javob qaytadi. Boshqa holat — -31008.
  3. Yangi bo'lsa — summa va buyurtma holati tekshiriladi.
  4. Buyurtmada boshqa faol tranzaksiya bo'lsa — -31008. Payme hujjatining sandbox ssenariysi buni talab qiladi: bitta buyurtma — bitta jarayondagi to'lov.
  5. Payme yuborgan time 12 soatdan eski bo'lsa — yaratmaymiz.
  6. INSERT, buyurtma kutilmoqda.

7. 12 soatlik timeout va ROLLBACK tuzog'i

Payme hujjati (CreateTransaction sahifasi, 2026-10-28 da qayta tekshirildi): tranzaksiya yaratilgandan (Payme'ning time maydonidan) 12 soat — 43 200 000 ms — o'tsa va hali bajarilmagan bo'lsa, u bekor qilinadi: holat -1, sabab 4. Mantiq oddiy:

js
const TIMEOUT_MS = 43_200_000;
const yaratildi = Date.UTC(2026, 9, 27, 10);

for (const soat of [1, 11.9, 12.01]) {
  const hozir = yaratildi + soat * 3_600_000;
  const otdi = hozir - yaratildi > TIMEOUT_MS;
  console.log(soat, otdi ? "bekor: state -1, reason 4" : "kutadi");
}

Konsolda:

text
1 kutadi
11.9 kutadi
12.01 bekor: state -1, reason 4

Nozik joy — qayerda xato qaytarish. Kanonning birinchi varianti PerformTransaction ichida shunday edi: tranzaksiyani bekor qil (UPDATE … 'bekor', sabab = 4), keyin throw X.mumkinEmas(). Kutilgan natija: bazada -1, Payme'ga -31008. Haqiqiy natija: throw → tranzaksiya() yordamchisi ROLLBACK qiladi → bekor qilish ham bekor bo'ladi. Payme -31008 ni oldi, bizda esa tranzaksiya hamon yaratilgan, buyurtma esa kutilmoqda da qotib qoldi — mijoz qayta to'lay olmaydi.

Tuzatish: xatoni qaytarish (return X.mumkinEmas()), tashlash emas. Tranzaksiya COMMIT bo'ladi, keyin .then uni Payme xatosiga aylantirib tashlaydi:

ts
if (muddatOtdimi(t)) {
  await timeoutBekor(c, t);
  return X.mumkinEmas(); // throw — ROLLBACK, bekor qilish yo'qolardi
}
// ...
}).then((r) => {
  if (r instanceof PaymeXato) throw r; // COMMIT'dan keyin
  return r;
}),

Bu xato kanonda test bilan topildi va endi test bilan himoyalangan: soat 12 soat + 1 ms oldinga suriladi, Perform → -31008, CheckTransaction → state -1, reason 4, va buyurtma qayta to'lanadi.

Testlar soatni qanday suradi? paymeRouter hozir funksiyasini sozlama sifatida oladi (sukut — Date.now). Test o'z soatini beradi va uni o'zgartiradi (Vaqt va tasodifni boshqarish). 12 soat kutish shart emas.

Tekshirib ko'ring: Nega timeout'ni Payme'ning time maydonidan hisoblaymiz, o'zimizning yaratilgan_vaqt dan emas?

Javob

12 soat — Payme tomonidagi qoida, u o'z vaqti bilan hisoblaydi. Bizning server so'rovni kechroq olgan bo'lishi mumkin (tarmoq, qayta urinish). Ikki tomon bir xil boshlang'ich nuqtadan sanasa, holatlar mos keladi. Shuning uchun tashqi_vaqt ustunida Payme time saqlanadi.

8. CancelTransaction va refund

  • yaratilgan → bekor (state -1), buyurtma yana yangi — mijoz qayta to'lashi mumkin.
  • bajarilgan → qaytarilgan (state -2): pul qaytariladi, ledger'da teskari yozuv.
  • Xizmat allaqachon ko'rsatilgan bo'lsa (tovar yetkazilgan) — -31007. Buni qaror qiluvchi funksiya kanonda sozlama: bekorQilinadimi(buyurtmaId).
  • Takroriy Cancel — o'sha javob.

Payme CancelTransaction — faqat to'liq qaytarish. Qisman qaytarishni Refund, reconciliation va moliyaviy hisob darsida ko'ramiz.

9. Parallel so'rovlar

Kanon testida simulyator bitta buyurtmaga 10 ta parallel CreateTransaction yubordi, har biri boshqa Payme id bilan. Natija: 1 ta state 1, 9 ta -31008. Ishlagan himoya — buyurtma qatoridagi FOR UPDATE qulfi: birinchi so'rov qulfni oldi, qolganlari kutdi, keyin "faol tranzaksiya bor" ni ko'rdi. Qulf bo'lmasa ham, qisman noyob indeks ikkinchi INSERT ni to'xtatardi — lekin bu kutilmagan xato (-32400) bo'lardi.

Kanon simulyatori bilan 100 ta to'liq to'lov (Check + Create + Perform, ketma-ket): mediana 44.0 ms, p95 183.3 ms, 100/100 muvaffaqiyatli (bitta mashina, tarmoqsiz).

10. Simulyator

mock/payme.ts — Payme serverining o'rnini bosuvchi klass. U fetch bilan bizning endpoint'ga Payme kabi so'rov yuboradi: text/json, Basic auth, JSON-RPC, tartib raqamli id. Payme tranzaksiya id'si — 24 ta tasodifiy hex belgi. Asosiy metod:

ts

  /** To'liq muvaffaqiyatli to'lov: Check → Create → Perform. */
  async tola(
    orderId: number | string,
    summa: number,
    time = Date.now(),
  ) {
    const id = PaymeSimulyator.id();
    const tek = await this.chaqir("CheckPerformTransaction", {
      amount: summa,
      account: { order_id: String(orderId) },
    });
    if (tek.javob.error)
      // JSON-RPC javobidagi `id` (so'rov raqami) bizning `id` ni
      // bosmasin
      return { ...tek.javob, id, bosqich: "check" };
    const yar = await this.chaqir("CreateTransaction", {
      id,
      time,
      amount: summa,
      account: { order_id: String(orderId) },
    });
    if (yar.javob.error)
      return { ...yar.javob, id, bosqich: "create" };
    const baj = await this.chaqir("PerformTransaction", { id });
    return { ...baj.javob, id, bosqich: "perform" };

Kanon simulyatorda ham xato topdi: javobni { ...javob, id } bilan qaytarganda, JSON-RPC javobidagi id (so'rov raqami) bizning Payme id imizni bosib ketardi. Tartib almashtirildi: { ...yar.javob, id, bosqich }. Simulyator ham kod — u ham test qilinadi.

Simulyator taqlid qilmaydigan narsalar: Payme'ning haqiqiy qayta urinish jadvali, IP manzillari, tarmoq timeout'i, SetFiscalData.

11. Ko'p uchraydigan xatolar

Xato Oqibati To'g'risi
express.json() req.body bo'sh express.text({ type: () => true }) + JSON.parse
xatoda HTTP 400/500 Payme -32400 deb hisoblaydi doim 200 + error.code
takroriy Create da xato to'lov to'xtaydi o'sha id — o'sha javob
timeout'da throw ROLLBACK, tranzaksiya qotadi return xato, COMMIT'dan keyin tashlash
summa so'mda 100 marta farq Payme amount — tiyinda
kalitni === bilan solishtirish vaqt orqali sizish timingSafeEqual
kutilmagan xatoni yutish sabab noma'lum logga yozish, keyin -32400

12. Mashqlar

Mashqlar kurs/mashqlar/29/29-payme/ da.

1-mashq (oson): qaysi kod?

  1. Buyurtma 42 bazada yo'q.
  2. Payme amount: 1000 yubordi, buyurtma 12 500 so'm.
  3. So'rov tanasi {buzilgan.
  4. method: "ChangePassword".
  5. Bajarilgan va yetkazilgan buyurtmani bekor qilish.
Yechim
  1. -31050 (data: "order_id"). 2. -31001. 3. -32700. 4. -32601 (data: "ChangePassword"). 5. -31007. Hammasi HTTP 200 bilan — kanon testi buni ham tekshiradi.

2-mashq (o'rta): JSON-RPC javob yasovchi

javob(id, natija) yozing: natija — Error bo'lib, kod xususiyati bo'lsa, error obyekti (code, message, ixtiyoriy data); aks holda result. id yo'q bo'lsa — null. Uch holatni chiqaring: muvaffaqiyat, -31050 xato (data: "order_id"), id siz -32700.

Yechim
js
class PaymeXato extends Error {
  constructor(kod, lok, data) {
    super(lok.en);
    this.kod = kod;
    this.lok = lok;
    this.data = data;
  }
}

function javob(id, natija) {
  const asos = { jsonrpc: "2.0", id: id ?? null };
  if (natija instanceof PaymeXato) {
    const error = { code: natija.kod, message: natija.lok };
    if (natija.data) error.data = natija.data;
    return JSON.stringify({ ...asos, error });
  }
  return JSON.stringify({ ...asos, result: natija });
}

const lok = (uz, en) => ({ ru: "-", uz, en });
console.log(javob(1, { allow: true }));
console.log(javob(2, new PaymeXato(-31050,
  lok("Buyurtma raqami noto'g'ri", "Order not found"), "order_id")));
console.log(javob(undefined, new PaymeXato(-32700,
  lok("JSON tahlil xatosi", "Parse error"))));

Konsolda:

text
{"jsonrpc":"2.0","id":1,"result":{"allow":true}}
{"jsonrpc":"2.0","id":2,"error":{"code":-31050,"message":{"ru":"-","uz":"Buyurtma raqami noto'g'ri","en":"Order not found"},"data":"order_id"}}
{"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":{"ru":"-","uz":"JSON tahlil xatosi","en":"Parse error"}}}

Buzilgan JSON'da id ni bilib bo'lmaydi — shuning uchun null. Kanon kodida ham id boshida null, JSON muvaffaqiyatli o'qilgandan keyin to'ldiriladi.

3-mashq (qiyin): so'rov tekshiruvchisi

sorovniTekshir(httpMetod, xomTana, metodlar) yozing — kanon routerining protokol qismi, bazasiz. Qaytarsin: { ok: true, id, method, params } yoki { ok: false, kod, id }. Tartib: POST emas → -32300; JSON buzuq → -32700; obyekt emas yoki method satr emas yoki params obyekt emas → -32600; metod ro'yxatda yo'q → -32601. Beshta holatni sinang.

Yechim
js
function sorovniTekshir(httpMetod, xomTana, metodlar) {
  if (httpMetod !== "POST") {
    return { ok: false, kod: -32300, id: null };
  }
  let tana;
  try {
    tana = JSON.parse(xomTana);
  } catch {
    return { ok: false, kod: -32700, id: null };
  }
  if (typeof tana !== "object" || tana === null) {
    return { ok: false, kod: -32600, id: null };
  }
  const id = tana.id ?? null;
  const { method, params } = tana;
  if (typeof method !== "string" || typeof params !== "object"
    || params === null) {
    return { ok: false, kod: -32600, id };
  }
  if (!metodlar.includes(method)) {
    return { ok: false, kod: -32601, id };
  }
  return { ok: true, id, method, params };
}

const M = ["CheckPerformTransaction", "CreateTransaction"];
const t = (o) => JSON.stringify({ jsonrpc: "2.0", ...o });
console.log(sorovniTekshir("GET", "", M));
console.log(sorovniTekshir("POST", "{buzilgan", M));
console.log(sorovniTekshir("POST", t({ id: 3, method: "X" }), M));
const y = t({ id: 4, method: "Y", params: {} });
console.log(sorovniTekshir("POST", y, M));
const c = t({
  id: 5, method: "CreateTransaction", params: { amount: 100 },
});
const r = sorovniTekshir("POST", c, M);
console.log(r.ok, r.id, r.method, r.params.amount);

Konsolda:

text
{ ok: false, kod: -32300, id: null }
{ ok: false, kod: -32700, id: null }
{ ok: false, kod: -32600, id: 3 }
{ ok: false, kod: -32601, id: 4 }
true 5 CreateTransaction 100

Kanon routerida auth tekshiruvi JSON tahlilidan keyin, metod bajarilishidan oldin turadi — shunda auth xatosi javobida ham to'g'ri id qaytadi.

13. Loyiha qadami: payme/merchant.ts

Modulning yuragi — router: tanani matn sifatida o'qish, protokol tekshiruvlari, auth, metodni chaqirish va hamma xatoni HTTP 200 javobiga aylantirish. Formatlangan nusxa (izohlar va uzun qatorlar bo'lingan, ma'nosi kanondagidek; fayl boshidagi xato lug'ati X va buyurtmaOl/txOl yordamchilari — darsda tasvirlangan, bu yerda qisqartirdik):

ts
export function paymeRouter(s: PaymeSozlama): Router {
  const r = express.Router();
  const hozir = s.hozir ?? Date.now;
  // Payme `text/json` yuboradi — har qanday turni matn sifatida
  // o'qiymiz va o'zimiz parse qilamiz (-32700 ni to'g'ri qaytarish
  // uchun ham)
  r.all(
    "/",
    express.text({ type: () => true, limit: "64kb" }),
    async (req: Request, res: Response) => {
      let id: unknown = null;
      const javob = (b: object) => {
        res.status(200).json({ jsonrpc: "2.0", id, ...b });
      };
      try {
        if (req.method !== "POST") throw X.post();
        let tana: unknown;
        try {
          tana = JSON.parse(
            typeof req.body === "string" ? req.body : "",
          );
        } catch {
          throw X.json();
        }
        if (typeof tana !== "object" || tana === null)
          throw X.sorov();
        const { method, params } = tana as {
          method?: unknown;
          params?: unknown;
          id?: unknown;
        };
        id = (tana as { id?: unknown }).id ?? null;
        // Auth — JSON tahlilidan keyin (javobda id qaytishi uchun),
        // metod bajarilishidan oldin
        if (
          !authOk(req.get("authorization"), s.login, s.kalit) ||
          (s.ruxsatIp && !s.ruxsatIp(req.ip ?? ""))
        )
          throw X.huquq();
        if (
          typeof method !== "string" ||
          typeof params !== "object" ||
          params === null
        )
          throw X.sorov();
        const p = params as Record<string, unknown>;
        const f = usullar[method];
        if (!f) throw X.metod(method);
        javob({ result: await f(p) });
      } catch (e) {
        if (e instanceof PaymeXato)
          javob({
            error: {
              code: e.kod,
              message: e.lok,
              ...(e.data ? { data: e.data } : {}),
            },
          });
        else {
          (s.log ?? console.error)(e);
          javob({
            error: {
              code: -32400,
              message: {
                ru: "Системная ошибка",
                uz: "Tizim xatosi",
                en: "Internal error",
              },
            },
          });
        } // kutilmagan xato YUTILMAYDI — logga
      }
    },
  );

PerformTransaction — CreateTransaction bilan bir xil naqsh: qulf, takrorga o'sha javob, timeout, keyin bajarilgan + buyurtma tolangan + ledger yozuvi (bitta ma'lumotlar bazasi tranzaksiyasida):

ts
    PerformTransaction: (p) =>
      tranzaksiya(s.pool, async (c) => {
        const t = await txOl(c, p["id"], true);
        if (!t) throw X.topilmadi();
        if (t.holat === "bajarilgan")
          return {
            transaction: t.id,
            perform_time: ms(t.bajarilgan_vaqt),
            state: 2,
          };
        if (t.holat !== "yaratilgan") throw X.mumkinEmas();
        if (muddatOtdimi(t)) {
          await timeoutBekor(c, t);
          return X.mumkinEmas();
        } // bekor qilish SAQLANADI, keyin xato qaytariladi
        const u = await c.query<TxQator>(
          "UPDATE tranzaksiyalar SET holat = 'bajarilgan', " +
            "bajarilgan_vaqt = to_timestamp($2 / 1000.0) WHERE id " +
            "= $1 RETURNING *",
          [t.id, hozir()],
        );
        await c.query(
          "UPDATE buyurtmalar SET holat = 'tolangan', " +
            "yangilangan_vaqt = now() WHERE id = $1",
          [t.buyurtma_id],
        );
        await tolovYozuvi(
          c,
          "payme",
          Number(t.summa),
          Number(t.id),
        );
        const q = u.rows[0];
        if (!q) throw new Error("UPDATE natijasiz");
        return {
          transaction: q.id,
          perform_time: ms(q.bajarilgan_vaqt),
          state: 2,
        };
      }).then((r) => {
        if (r instanceof PaymeXato) throw r;
        return r;
      }),

Testlar — tolov/test/payme.test.ts, yettita: protokol xatolari (hammasi HTTP 200), sandbox'ning 1- va 2-ssenariylari, -31007, 12 soat timeout, parallel 10 Create, GetStatement. Parcha:

ts
test("parallel: 10 ta turli Payme id bilan bir buyurtmaga "
  + "Create — faqat bittasi muvaffaqiyatli", async () => {
  const oid = await buyurtma(pool, 3000);
  const r = await Promise.all(Array.from({ length: 10 }, () =>
    p.chaqir("CreateTransaction", {
      id: PaymeSimulyator.id(), time: soat, amount: 3000,
      account: { order_id: String(oid) },
    })));
  assert.equal(
    r.filter((x) => x.javob.result?.["state"] === 1).length, 1);
  assert.equal(r.filter((x) => kod(x) === -31008).length, 9);
});

Testlarni biz ishga tushirdik (kanon nusxasi, Node 24.21.0, o'z Postgres 17.6 klasterimiz):

bash
DATABASE_URL=postgres://postgres@127.0.0.1:5432/postgres \
  node --test test/payme.test.ts
text
▶ Payme Merchant API (simulyator)
  ✔ protokol: noto'g'ri auth -32504, GET -32300, buzilgan JSON -32700, metod yo'q -32601, maydon -32600 — HAMMASI HTTP 200 (146.3316ms)
  ✔ 1-ssenariy: yaratish va bajarilmagan tranzaksiyani bekor qilish (81.0031ms)
  ✔ 2-ssenariy: yaratish, bajarish (takror — o'sha javob), bajarilganni bekor qilish (refund, ledger) (49.974ms)
  ✔ bajarilgan, lekin xizmat ko'rsatilgan buyurtma — bekor qilinmaydi -31007 (22.4385ms)
  ✔ 12 soat timeout: Perform → -31008, tranzaksiya -1 sabab 4 SAQLANADI, buyurtma qayta to'lanadi (42.6065ms)
  ✔ parallel: 10 ta turli Payme id bilan bir buyurtmaga Create — faqat bittasi muvaffaqiyatli (188.4144ms)
  ✔ GetStatement: davrdagi tranzaksiyalar (rekonsilyatsiya uchun) (4.735ms)
✔ Payme Merchant API (simulyator) (1840.8169ms)
ℹ tests 7
ℹ suites 1
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2473.5817

Yetti test o'tdi (ℹ fail 0). Beshinchi qatorga qarang: "SAQLANADI" — timeout'dagi bekor qilish COMMIT bo'ldi, ya'ni ROLLBACK tuzog'i yopilgan. Millisekundlar har safar boshqa. Kanonda (Postgres 18.6) ham 7/7.

Commit xabari:

bash
M='feat(tolov): Payme Merchant API — JSON-RPC, 6 metod,'
M+=' 12 soat timeout, simulyator'
git commit -m "$M"

14. Real ishda

  • Ulanish tartibi: Payme Business'da kassa ochiladi, test kalit beriladi, siz endpoint manzilini kiritasiz, Payme'ning test sahifasida ssenariylarni o'tkazasiz, keyin prod kalit. Kanon bu bosqichlarni sinamagan.
  • Fiskal chek: O'zbekistonda onlayn to'lov uchun fiskal chek talab qilinadi; Payme'da SetFiscalData metodi va receivers/detail maydonlari bor — hujjat bo'yicha, kanonga kirmagan.
  • Monitoring: -32400 soni oshsa — signal. Har javob kodini logga yozing (kanon kutilmagan xatolarni yozadi).
  • Intervyuda: "Payme Create ikki marta kelsa nima qilasiz?", "Nega har doim HTTP 200?"

Xulosa

  • Payme sizni chaqiradi: JSON-RPC 2.0, bitta URL, method + params; javob doim HTTP 200, xato error.code da.
  • text/json — express.json() o'qimaydi; matn sifatida o'qib, o'zingiz parse qiling.
  • Basic auth — timingSafeEqual; faqat HTTPS.
  • Oltita metod, to'rtta holat (1, 2, -1, -2); holat va sabab jadvali hujjatda topilmadi — keng ishlatiladigan qiymatlar.
  • Takroriy chaqiruv — o'sha javob; buyurtmada bitta faol tranzaksiya; parallel 10 → 1 + 9 × -31008.
  • Timeout bekor qilinishi COMMIT bo'lishi kerak: xatoni qaytaring, tashlamang.

Keyingi dars: Click integratsiyasi — Prepare/Complete, MD5 imzo va so'mda keladigan summa.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!