Mundarija (23)
- Bu darsda
- 1. Nega bu kerak?
- 2. JSON-RPC 2.0
- 2.1 Javob doim HTTP 200
- 3. Birinchi tuzoq: text/json
- 4. Autentifikatsiya: Basic auth
- 5. Oltita metod
- 5.1 Tranzaksiya holatlari
- 5.2 Xato kodlari
- 6. CreateTransaction — eng nozik metod
- 7. 12 soatlik timeout va ROLLBACK tuzog'i
- 8. CancelTransaction va refund
- 9. Parallel so'rovlar
- 10. Simulyator
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): qaysi kod?
- 2-mashq (o'rta): JSON-RPC javob yasovchi
- 3-mashq (qiyin): so'rov tekshiruvchisi
- 13. Loyiha qadami: payme/merchant.ts
- 14. Real ishda
- Xulosa
- Manbalar
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 ichidagierror.codebilan. Paymetext/jsonyuboradi —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/jsontuzog'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.tsmodulini 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):
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:
- Javobdagi
id— so'rovdagiid(JSON-RPC so'rov raqami). Payme javoblarni shu bilan juftlaydi. - Javobda yoki
result, yokierror— ikkalasi birga emas. 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:
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:
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.
const sarlavha = "Basic " + btoa("Paycom:SOXTA-kalit");
console.log(sarlavha);
const m = /^Basic (.+)$/.exec(sarlavha);
console.log(atob(m[1]));Konsolda:
Basic UGF5Y29tOlNPWFRBLWthbGl0
Paycom:SOXTA-kalitBase64 — 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:
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 2Diagrammadagi 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):
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:
- Buyurtma qatori
FOR UPDATEbilan qulflanadi (buyurtmaOl(c, p, true)). Bir buyurtmaga parallel kelgan so'rovlar navbat bilan bajariladi. - Shu Payme
idbilan tranzaksiya bor bo'lsa — yangi yaratilmaydi. Holatiyaratilganbo'lsa, o'sha javob qaytadi. Boshqa holat —-31008. - Yangi bo'lsa — summa va buyurtma holati tekshiriladi.
- Buyurtmada boshqa faol tranzaksiya bo'lsa —
-31008. Payme hujjatining sandbox ssenariysi buni talab qiladi: bitta buyurtma — bitta jarayondagi to'lov. - Payme yuborgan
time12 soatdan eski bo'lsa — yaratmaymiz. INSERT, buyurtmakutilmoqda.
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:
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:
1 kutadi
11.9 kutadi
12.01 bekor: state -1, reason 4Nozik 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:
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
timemaydonidan hisoblaymiz, o'zimizningyaratilgan_vaqtdan 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 yanayangi— 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:
/** 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?
- Buyurtma 42 bazada yo'q.
- Payme
amount: 1000yubordi, buyurtma 12 500 so'm. - So'rov tanasi
{buzilgan. method: "ChangePassword".- Bajarilgan va yetkazilgan buyurtmani bekor qilish.
Yechim
-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
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:
{"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
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:
{ 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 100Kanon 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):
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):
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:
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):
DATABASE_URL=postgres://postgres@127.0.0.1:5432/postgres \
node --test test/payme.test.ts▶ 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.5817Yetti 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:
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
SetFiscalDatametodi vareceivers/detailmaydonlari bor — hujjat bo'yicha, kanonga kirmagan. - Monitoring:
-32400soni 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, xatoerror.codeda. 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
COMMITbo'lishi kerak: xatoni qaytaring, tashlamang.
Keyingi dars: Click integratsiyasi — Prepare/Complete, MD5 imzo va so'mda keladigan summa.
Manbalar
- Payme Business Merchant API (protokol, metodlar, xatolar, pesochnitsa): https://developer.help.paycom.uz — kanon 2026-10-27 da o'qigan; holatlar va sabablar jadvali sahifasi topilmadi
- JSON-RPC 2.0 spetsifikatsiyasi: https://www.jsonrpc.org/specification
- Express
express.text: https://expressjs.com/en/5x/api.html#express.text - Node
crypto.timingSafeEqual: https://nodejs.org/api/crypto.html#cryptotimingsafeequala-b - Kanon (IlmHamroh, 2026-10-27):
tolovpaketi, express 5.3.0, pg 8.23.1, Node 24.21.0, lokal Payme simulyatori; haqiqiy Payme — sinalmagan. Testlar — biz ishga tushirdik, PostgreSQL 17.6 (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!