8.19-bob: To'lov tizimlari (Payment) — Click, Payme, Stripe (to'liq)
8-QISM — NestJS (chuqur) · 19-mavzu · Amaliy real mavzu
1. Kirish va motivatsiya
Bu — kitobning eng amaliy, eng ko'p pul keltiradigan boblaridan biri: to'lov tizimlari (Payment). Har bir jiddiy loyiha — internet do'kon, taksi, yetkazib berish, SaaS obuna, kurs platformasi — pul qabul qilishi kerak. Va bu — backend dasturchining eng mas'uliyatli vazifasi: chunki bu yerda haqiqiy pul harakatlanadi. Bir xato — mijozdan ikki marta pul yechish, yoki to'lovni tan olmaslik, yoki firibgarga buyurtmani tekin berish. Shuning uchun payment — diqqat, xavfsizlik, va aniqlik talab qiladigan mavzu.
Ko'p dasturchi (siz aytganingizdek) bu mavzudan qo'rqadi, chunki u "sirli" ko'rinadi: API key qayerdan olinadi, webhook nima, imzo (signature) qanday tekshiriladi, database qanday tuziladi, "Prepare/Complete" nima degani. Aslida — bu takrorlanadigan naqsh: buyurtma yaratasiz to'lov havolasi/hisob berasiz mijoz to'laydi to'lov tizimi sizga xabar yuboradi (callback/webhook) siz tekshirasiz (haqiqatan to'landimi, summa to'g'rimi) buyurtmani "to'langan" deb belgilaysiz. Hammasi shu. Bu bobda men buni noldan, har bo'lagini tushuntirib o'rgataman.
Bu bob: to'lov ekotizimi (kim kim), O'zbekiston tizimlari (Click, Payme, Uzum), integratsiya modellari, database sxema, to'lov holatlari (state machine), API key/sirlar, idempotency (takror himoyasi — payment'ning eng muhim qoidasi), webhook + imzo tekshirish (xavfsizlik), Payme protokoli (JSON-RPC — 5 ta metod), Click protokoli (Prepare/Complete), Stripe (xalqaro), refund (qaytarish), reconciliation, test. NestJS bilan (controller=webhook, service, DTO, guard — 8-QISM). Bu — sizning kareyrangizdagi eng qadrli ko'nikmalardan.
O'xshatish: to'lov integratsiyasi — restoran va bank terminali. Mijoz ovqat buyuradi (buyurtma yaratiladi —
pending). Ofitsiant terminalga summani kiritadi (hisob/havola beriladi). Mijoz kartani bosadi bank (to'lov tizimi — Payme/Click) pulni tekshiradi va yechadi. Keyin bank terminalga "to'landi" chekini qaytaradi (webhook/callback). Ofitsiant chekni ko'rib, ovqatni beradi (buyurtmapaidbajariladi). Muhim: ofitsiant bankning chekiga ishonadi (mijozning "to'ladim" deganiga emas — mijoz aldashi mumkin). Va agar chek ikki marta kelsa, ofitsiant ovqatni ikki marta bermaydi (idempotency). To'lov kodi — aynan shu mantiqning dasturlashtirilgani.
Nega muhim?
- Har biznesga kerak — pul qabul qilish (e-commerce, SaaS, xizmat).
- Eng mas'uliyatli — haqiqiy pul (xato = pul yo'qotish/firibgarlik).
- O'zbekiston bozori — Click/Payme/Uzum (har loyihada so'raladi).
- Yuqori qadr — payment biladigan dasturchi qimmat (kam mutaxassis).
2. Nazariya — chuqur tushuntirish
2.1. To'lov ekotizimi (kim kim)
TO'LOV ZANJIRI (kim ishtirok etadi):
Mijoz (xaridor) karta (UzCard/Humo/Visa)
Sizning ilovangiz (MERCHANT — savdogar)
To'lov tizimi / Gateway (Payme, Click, Stripe — PSP)
Bank (acquirer — pulni qabul qiluvchi bank)
Karta tarmog'i (UzCard/Humo, Visa/Mastercard)
Mijozning banki (issuer — pulni yechuvchi)
Siz faqat MERCHANT'siz — gateway qolganini hal qiladi (siz bank emassiz)To'lov ekotizimi: siz merchant (savdogar) — pul qabul qiluvchi. Gateway/PSP (Payme, Click, Stripe) — to'lovni amalga oshiruvchi vositachi (sizni bank/karta tarmog'iga ulaydi). Acquirer — sizning bankingiz (pul tushadigan). Issuer — mijoz banki. Siz kartani o'zingiz ishlamaysiz (PCI DSS — qonun; karta ma'lumotini saqlamaysiz) — gateway hal qiladi. Sizning vazifangiz — gateway bilan to'g'ri integratsiya.
2.2. O'zbekiston to'lov tizimlari
┌──────────┬────────────────────────────────────────────────┐
│ Payme │ Eng keng. JSON-RPC protokol. Gateway SIZNI │
│ (Paycom) │ chaqiradi (Merchant API). UzCard/Humo/Visa. │
├──────────┼────────────────────────────────────────────────┤
│ Click │ Keng. Prepare/Complete (2 bosqich). Gateway │
│ │ sizning endpoint'ni chaqiradi. SHOP API. │
├──────────┼────────────────────────────────────────────────┤
│ Uzum │ O'sib borayotgan. Webhook asosli. │
│ (Apelsin)│ │
├──────────┼────────────────────────────────────────────────┤
│ Octo │ Karta to'lovi (acquiring). Token asosli. │
│ Stripe │ XALQARO (Visa/MC global) — O'zbekistonda cheklov │
└──────────┴────────────────────────────────────────────────┘
Mahalliy loyiha: Payme + Click (+ Uzum); xalqaro: StripeO'zbekiston tizimlari: Payme (Paycom — eng keng, JSON-RPC), Click (Prepare/Complete), Uzum, Octo (karta acquiring). Mahalliy loyiha uchun odatda Payme + Click (ko'pincha ikkalasi birga). Stripe — xalqaro (O'zbekistonda to'g'ridan cheklangan, lekin global loyihaga). Har birining protokoli boshqacha, lekin g'oya bir xil 2.4-bob. Bu bobda ikkalasini ham (Payme JSON-RPC, Click Prepare/Complete) va Stripe'ni ko'ramiz.
2.3. Ikki integratsiya modeli
MODEL A — Gateway SIZNI chaqiradi (Merchant API — Payme, Click):
- Mijoz gateway sahifasida to'laydi
- Gateway SIZNING endpoint'laringizni chaqiradi (CheckPerform, Perform...)
- Siz javob berasiz (mumkinmi, bajarildimi)
- Sizda PUBLIC endpoint kerak (gateway chaqiradi)
MODEL B — Siz gateway'ni chaqirasiz + webhook (Stripe, Uzum):
- Siz PaymentIntent/invoice yaratasiz (API chaqirish)
- Mijoz to'laydi
- Gateway sizga WEBHOOK yuboradi (to'landi)
Payme/Click — Model A (sizning endpoint'ni chaqiradi)
Stripe — Model B (siz chaqirasiz + webhook)Ikki model: Model A (Payme, Click) — gateway sizning endpoint'laringizni chaqiradi (siz "server" bo'lasiz, gateway "klient"). Model B (Stripe, Uzum) — siz gateway API'ni chaqirasiz, keyin gateway webhook yuboradi. Farqni tushunish muhim: Payme/Click'da siz protokol metodlarini amalga oshirasiz (implement); Stripe'da siz API'ni chaqirasiz. Ikkalasida ham natija — to'lov tasdig'i (callback/webhook).
2.4. Umumiy to'lov oqimi (universal)
HAR QANDAY TO'LOV (universal naqsh):
1. Mijoz buyurtma beradi ORDER yaratiladi (holat: pending)
2. To'lov havolasi/hisob yaratiladi (transaction: pending)
3. Mijoz to'lov sahifasida to'laydi (gateway)
4. Gateway TASDIQ yuboradi (callback/webhook)
5. Siz TEKSHIRASIZ: imzo to'g'rimi? summa to'g'rimi? takror emasmi?
6. To'g'ri bo'lsa transaction: paid ORDER bajariladi (mahsulot beriladi)
7. Mijozga chek/email (8.10)
Eng muhim: 5-qadam (TEKSHIRISH) — bu xavfsizlikUniversal oqim: order hisob mijoz to'laydi gateway tasdiq TEKSHIRISH paid buyurtma bajariladi. 5-qadam (tekshirish) — eng muhim: imzo (signature — gateway haqiqatan yuborganmi — 2.9), summa (o'zgartirilmaganmi), takror (idempotency — 2.8). Mijozning "to'ladim" deganiga hech qachon ishonmang — faqat gateway tasdig'iga (imzo bilan). Bu — payment xavfsizligining yuragi.
2.5. Database sxema (transactions)
TRANSACTIONS jadvali (har to'lov yozuvi):
┌────────────────┬──────────────────────────────────────┐
│ id │ ichki ID │
│ orderId │ qaysi buyurtma (FK) │
│ provider │ "payme" | "click" | "stripe" │
│ providerTxId │ gateway'dagi tranzaksiya ID (noyob) │
│ amount │ summa (TIYINDA — ×100, butun son!) │
│ currency │ "UZS" │
│ state │ holat (2.6 — state machine) │
│ createdAt │ yaratilgan vaqt │
│ paidAt │ to'langan vaqt │
│ cancelledAt │ bekor qilingan vaqt │
│ reason │ bekor sababi │
└────────────────┴──────────────────────────────────────┘
amount — TIYINDA (butun son): 50000 so'm = 5000000 tiyin
providerTxId — UNIQUE (idempotency — 2.8)Database sxema —
transactionsjadvali (har to'lov yozuvi). Muhim ustunlar:orderId(buyurtma),provider(qaysi tizim),providerTxId(gateway ID — UNIQUE — idempotency),amount,state(holat — 2.6). Summa TIYINDA (butun son —×100; float emas — pul hisobida float xato beradi: 0.1+0.2≠0.3 — 2.6-JS). 50000 so'm = 5000000 tiyin.providerTxIdunique — bir tranzaksiya bir marta 2.8-bob.
2.6. To'lov holatlari (state machine)
TRANSACTION HOLATLARI (state machine):
pending (yaratildi, kutilmoqda)
├── paid (muvaffaqiyatli to'landi) ── refunded (qaytarildi)
├── cancelled (bekor qilindi)
└── failed (xato/muddat tugadi)
HOLAT O'TISHLARI nazorat qilinadi:
- paid pending QAYTA bo'lmaydi (faqat oldinga)
- cancelled tranzaksiyani perform qilib bo'lmaydi
- faqat paid'ni refund qilish mumkin
Payme holatlari: 1 (yaratildi), 2 (to'landi), -1 (bekor, to'lovsiz), -2 (bekor, to'lovdan keyin)State machine — to'lov holatlari va o'tishlari:
pendingpaid/cancelled/failed;paidrefunded. O'tishlar nazorat qilinadi (biznes qoida — DDD 9.4):paidqaytibpendingbo'lmaydi,cancelled'ni perform qilib bo'lmaydi, faqatpaid'ni refund. Noto'g'ri o'tish — xato (pul yo'qotish). Payme o'z holatlari (1/2/-1/-2). To'g'ri state machine — aniq, ishonchli to'lov.
2.7. API key / merchant credentials / sirlar
HAR GATEWAY o'z kalitlari (merchant kabinetdan olinadi):
PAYME: PAYME_MERCHANT_ID (kassa ID), PAYME_KEY (maxfiy kalit — webhook auth)
CLICK: CLICK_SERVICE_ID, CLICK_MERCHANT_ID, CLICK_SECRET_KEY
STRIPE: STRIPE_SECRET_KEY (sk_...), STRIPE_WEBHOOK_SECRET (whsec_...)
XAVFSIZLIK (14):
- Kalitlar .env'da (kodda EMAS — 8.14, 14)
- .gitignore (git'ga tushmasin — eng muhim)
- Test (sandbox) va Production kalitlari ALOHIDA
- Kalit oshkor bo'lsa — DARROV almashtirish (kabinetdan)API key/credentials: har gateway o'z kalitlari (merchant kabinetdan — ro'yxatdan o'tib olinadi): Payme (Merchant ID + Key), Click (Service/Merchant ID + Secret), Stripe (Secret + Webhook Secret). Sirlar .env'da (8.14, 14),
.gitignore(git'ga hech qachon — oshkor bo'lsa pul o'g'irlanishi mumkin), test/prod alohida. Bu — 8.14 (config) ning eng kritik tatbiqi (haqiqiy pul kalitlari).
2.8. Idempotency (takror himoyasi — eng muhim qoida)
MUAMMO: bir to'lov IKKI marta bajarilishi mumkin:
- Tarmoq xatosi gateway webhook'ni QAYTA yuboradi (retry)
- Mijoz tugmani 2 marta bosadi
buyurtma 2 marta bajariladi / pul 2 marta hisoblanadi (XATO!)
YECHIM — IDEMPOTENCY (takror = bir marta):
- providerTxId UNIQUE (DB) ikkinchi marta kelsa, eski natijani qaytar
- Webhook: event ID saqlash (qayta ishlov bermaslik)
- Stripe: Idempotency-Key header (POST so'rovlarda)
if (await txRepo.findByProviderTxId(id)) return mavjudNatija; // takror eskiIdempotency (payment'ning eng muhim qoidasi): bir to'lov ikki marta bajarilmasligi kerak (gateway retry, mijoz 2 marta bosish). Yechim:
providerTxIdUNIQUE (ikkinchi kelganda eski natija qaytariladi — yangi yaratilmaydi), webhook event ID saqlash. Stripe —Idempotency-Keyheader. Idempotency'siz to'lov tizimi — xavfli (ikki marta pul, ikki marta buyurtma). Bu — gateway'lar at-least-once yetkazadi (bir necha marta yuborishi mumkin), shuning uchun majburiy.
2.9. Webhook va imzo tekshirish (signature — xavfsizlik)
WEBHOOK — gateway sizning endpoint'ga to'lov haqida xabar yuboradi
MUAMMO: har kim sizning webhook URL'ga soxta so'rov yuborishi mumkin
"to'landi" deb yolg'on buyurtmani tekin olib ketadi!
YECHIM — IMZO (signature) tekshirish:
- Payme: Basic Auth (Paycom:KEY — Authorization header)
- Click: MD5 imzo (sign_string — kalit bilan hash)
- Stripe: constructEvent (whsec bilan imzo tekshirish)
Imzo NOTO'G'RI rad et (401). FAQAT haqiqiy gateway o'tadi.
Stripe: RAW body kerak (imzo uchun — JSON parse'dan oldin)Webhook imzo tekshirish (xavfsizlik — eng muhim): webhook — gateway tasdig'i, lekin har kim soxta yuborishi mumkin (URL ochiq). Imzo (signature) — gateway haqiqatan yuborganini isbotlaydi: Payme (Basic Auth — Key), Click (MD5 hash — secret), Stripe (
constructEvent— webhook secret). Imzo noto'g'ri rad et (401). Imzosiz webhook — firibgar "to'landi" deb yolg'on aytib, mahsulotni tekin oladi. Stripe'da raw body kerak (parse'dan oldin — body o'zgartirilsa imzo buziladi).
2.10. Payme protokoli (JSON-RPC — Merchant API)
PAYME — JSON-RPC (gateway sizning bitta endpoint'ni chaqiradi: POST /payme)
Authorization: Basic base64("Paycom:PAYME_KEY") auth 2.11-bob
5 ASOSIY METOD (gateway chaqiradi, siz javob berasiz):
1. CheckPerformTransaction — to'lov mumkinmi? (buyurtma bor, summa to'g'ri?)
2. CreateTransaction — tranzaksiya yarat (state: pending)
3. PerformTransaction — to'lovni tasdiqla (state: paid buyurtma bajariladi)
4. CancelTransaction — bekor qil (state: cancelled, refund)
5. CheckTransaction — tranzaksiya holatini so'ra
(+ GetStatement — davr hisoboti — reconciliation 2.15)Payme protokoli (JSON-RPC — docs): gateway bitta endpointni (
POST /payme) turlimethodbilan chaqiradi. 5 metod: CheckPerformTransaction (mumkinmi — buyurtma/summa tekshir), CreateTransaction (yarat — pending), PerformTransaction (tasdiqla — paid, buyurtma bajariladi), CancelTransaction (bekor — refund), CheckTransaction (holat so'ra). Siz har metodga to'g'ri javob berasiz (Payme spetsifikatsiyasi bo'yicha). Bu — Model A 2.3-bob.
2.11. Payme auth va xato kodlari
// Payme — Basic Auth (har so'rovda tekshiriladi)
// Authorization: Basic base64("Paycom:PAYME_KEY")
function paymeAuthTekshir(authHeader: string, key: string): boolean {
const [, b64] = (authHeader || "").split(" ");
const [login, parol] = Buffer.from(b64 || "", "base64").toString().split(":");
return login === "Paycom" && parol === key; // kalit mos kelishi kerak
}
// Payme xato kodlari (spetsifikatsiya bo'yicha — JSON-RPC error)
// -32504 — auth xato; -31050..-31099 — buyurtma topilmadi/summa xato
// -31008 — amalni bajarib bo'lmaydi; -31003 — tranzaksiya topilmadiPayme auth — har so'rovda Basic Auth (
Paycom:KEY— base64). Kalit mos kelmasa xato (-32504). Xato kodlari — Payme spetsifikatsiyasi bo'yicha aniq (JSON-RPCerrorformati): buyurtma topilmadi/summa xato (-31050..), amal bajarilmaydi (-31008), tranzaksiya yo'q (-31003). Payme xato kodlarini aniq qaytarish kerak (aks holda integratsiya o'tmaydi — Payme test qiladi).
2.12. Click protokoli (Prepare + Complete)
CLICK — 2 bosqichli (gateway sizning 2 endpoint'ni chaqiradi):
1. PREPARE (/click/prepare):
- Click "to'lovga tayyormisiz?" deb so'raydi
- Siz: buyurtma bor? summa to'g'ri? tranzaksiya yarat (pending)
- merchant_prepare_id qaytarasiz
2. COMPLETE (/click/complete):
- Click "to'lov bajarildi, tasdiqlang" deydi
- Siz: imzo tekshir tranzaksiya paid buyurtma bajariladi
IMZO (sign_string) — MD5:
md5(click_trans_id + service_id + SECRET_KEY + merchant_trans_id + amount + action + sign_time)Click protokoli — 2 bosqich: Prepare (Click "tayyormisiz?" siz tekshirib tranzaksiya yaratasiz) va Complete (Click "bajarildi" siz tasdiqlaysiz, buyurtmani bajarasiz). Har bosqichda imzo (
sign_string— MD5 hash: trans_id + service_id + secret + ... — 2.9). Imzo noto'g'ri rad. Click ham Model A (sizning endpoint'ni chaqiradi). 2 bosqich — pul yechishdan oldin tasdiqlash (ishonchlilik).
2.13. Stripe (xalqaro — PaymentIntent + webhook)
// Stripe — Model B (siz chaqirasiz + webhook)
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// 1. PaymentIntent yaratish (frontend uchun client_secret)
const intent = await stripe.paymentIntents.create(
{ amount: 5000, currency: "usd", metadata: { orderId: "123" } },
{ idempotencyKey: `order_123` }, // idempotency (2.8)
);
// 2. Frontend client_secret bilan to'laydi (Stripe.js)
// 3. Stripe WEBHOOK yuboradi (payment_intent.succeeded) 2.9 verify
// MUQOBIL: Stripe Checkout (hosted — Stripe'ning tayyor to'lov sahifasiga
// yo'naltirish; frontend forma yozish shart emas — Payme/Click redirect'iga o'xshash):
const session = await stripe.checkout.sessions.create(
{
mode: "payment",
line_items: [{ // nima sotilyapti
price_data: {
currency: "usd",
product_data: { name: "Buyurtma #123" },
unit_amount: 5000, // TIYINDA (cent — butun son, 2.5)
},
quantity: 1,
}],
metadata: { orderId: "123" }, // webhook'da kerak
success_url: "https://shop.uz/ok?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://shop.uz/bekor", // to'lamay chiqsa
},
{ idempotencyKey: `checkout_123` }, // idempotency (2.8)
);
// session.url ga mijozni yo'naltirasiz (redirect); to'lagach Stripe
// webhook yuboradi: "checkout.session.completed" (2.9 verify)Stripe (xalqaro — Model B): ikki uslub bor. (1) PaymentIntent — siz
client_secretolib, frontend'da (Stripe.js) o'z formangizni chizasiz (moslashuvchan, lekin ko'proq kod). (2) Checkout Session — Stripe'ning tayyor to'lov sahifasiga yo'naltirasiz (session.url— redirect, xuddi Payme/Click checkout'iga o'xshash; forma yozish shart emas, PCI yuki Stripe'da — 2.1). Har ikkalasidametadata.orderId(webhook'da buyurtmani topish uchun) vaidempotencyKey2.8-bob beriladi. To'lagach Stripe webhook yuboradi (PaymentIntentpayment_intent.succeeded; Checkoutcheckout.session.completed), sizconstructEventbilan imzoni tekshirasiz 2.9-bob. Summa har doim tiyinda/centda (unit_amountbutun son — 2.5). Stripe — eng toza, hujjatlangan (xalqaro standart), lekin O'zbekistonda to'g'ridan cheklangan.
2.14. Refund (pulni qaytarish)
REFUND — to'langan pulni qaytarish (qisman yoki to'liq):
- Faqat PAID tranzaksiyani refund qilish mumkin (2.6 state machine)
- To'liq yoki qisman (partial)
- Payme: CancelTransaction (state -2 — to'lovdan keyin bekor)
- Click: refund API / merchant kabinet
- Stripe: stripe.refunds.create({ payment_intent })
Refund'ni BEKOR qilib bo'lmaydi (qaytarib olinmaydi)
Idempotency (ikki marta refund = ikki marta pul qaytarish!)Refund — to'langan pulni qaytarish (mijoz qaytargan, buyurtma bekor). Faqat
paid'ni 2.6-bob, to'liq yoki qisman. Payme (CancelTransactionstate -2), Stripe (refunds.create). Refund bekor qilib bo'lmaydi (qaytarib olinmaydi — diqqat), va idempotent bo'lishi shart (ikki marta refund = ikki marta pul yo'qotish). State machine refund'ni nazorat qiladi (faqat paid'dan).
2.15. Reconciliation (solishtirish — hisobdorlik)
RECONCILIATION — sizning DB va gateway hisobotini solishtirish:
Nega: tarmoq xatosi, webhook yetmasligi DB va gateway mos kelmasligi mumkin
- Sizda "pending" lekin gateway'da "paid" (webhook yetmadi)
- Davriy (cron — 8.18) gateway hisobotini olib, solishtirish
- Payme: GetStatement (davr tranzaksiyalari)
- Stripe: balance transactions / events
Nomutanosiblikni aniqlab tuzatish (moliyaviy aniqlik)Reconciliation (solishtirish) — sizning DB va gateway yozuvlarini moslashtirish. Webhook yetmasligi/tarmoq xatosi nomutanosiblik (sizda pending, gateway'da paid). Davriy (cron — 8.18) gateway hisobotini (Payme
GetStatement, Stripe balance) olib, solishtirish. Moliyaviy aniqlik uchun zarur (pul masalasida "taxminan" yo'q). Katta loyihada majburiy (audit — buxgalteriya).
2.16. Test (sandbox) va integratsiya jarayoni
TO'LOV INTEGRATSIYASI JARAYONI:
1. Merchant kabinetda ro'yxat (Payme/Click biznes)
2. TEST (sandbox) kalitlari olish test rejimda ishlab chiqish
3. Test kartalari bilan sinash (gateway beradi)
4. Gateway integratsiyani TEKSHIRADI (Payme test scenariy yuboradi)
5. Tasdiqlangach PRODUCTION kalitlari jonli
HECH QACHON production'da haqiqiy pul bilan "sinab ko'rmang"
Webhook'ni lokal test: ngrok (lokal URL'ni public qilish)Test jarayoni: merchant ro'yxat sandbox (test) kalitlari test kartalar bilan ishlab chiqish gateway integratsiyani tekshiradi (Payme avtomatik test scenariy yuboradi — barcha metod to'g'ri javob berishi kerak) production kalitlari. ngrok (lokal URL'ni public qilish — webhook'ni lokal test). Hech qachon production'da haqiqiy pul bilan sinamang. Test — majburiy (xato production'da = pul).
2.17. Payment xavfsizligi (14 — kritik)
Imzo (signature) HAR webhook'da tekshir (soxta himoyasi — 2.9)
Summa SERVER'da tekshir (mijoz yuborgan summaga ishonma — 2.4)
Idempotency (takror himoyasi — 2.8)
Kalitlar .env (git'ga emas — 2.7, 14)
HTTPS majburiy (webhook — shifrlangan)
Karta ma'lumotini SAQLAMA (PCI DSS — gateway hal qiladi — 2.1)
Audit log (har to'lov harakati — kim, qachon, qancha)
State machine (noto'g'ri o'tish bloklanadi — 2.6)
Buyurtma summasi = to'lov summasi (server tekshiruvi)Payment xavfsizligi (14 — eng kritik): imzo tekshirish 2.9-bob, summa server'da (mijozga ishonmang — 2.4), idempotency 2.8-bob, kalitlar .env 2.7-bob, HTTPS, karta saqlamaslik (PCI DSS — gateway — 2.1), audit log (har harakat), state machine 2.6-bob. Bu yerda xato = haqiqiy pul yo'qotish yoki firibgarlik. Payment kodi — eng diqqat bilan yoziladigan, eng ko'p test qilinadigan kod.
3. Sintaksis — tez ma'lumotnoma
// Transaction holatlari (2.6)
type TxState = "pending" | "paid" | "cancelled" | "failed" | "refunded";
// Payme 2.10-bob: POST /payme — { method, params } JSON-RPC javob
// CheckPerformTransaction, CreateTransaction, PerformTransaction, CancelTransaction, CheckTransaction
// Click 2.12-bob: POST /click/prepare + /click/complete (sign_string MD5 tekshir)
// Stripe (2.13)
stripe.paymentIntents.create({ amount, currency }, { idempotencyKey }); // custom forma
stripe.checkout.sessions.create({ line_items, success_url, cancel_url }); // hosted redirect
stripe.webhooks.constructEvent(rawBody, sig, whsec); // imzo tekshir (2.9)
stripe.refunds.create({ payment_intent }); // refund (2.14)
// Idempotency 2.8-bob: if (await findByProviderTxId(id)) return mavjud;4. Batafsil kod namunalari
Misol 1 — Transaction entity (database — 2.5, 2.6)
// transaction.entity.ts (TypeORM — 8.3)
export enum TxState {
PENDING = "pending",
PAID = "paid",
CANCELLED = "cancelled",
FAILED = "failed",
REFUNDED = "refunded",
}
@Entity("transactions")
export class Transaction {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() orderId: string;
@ManyToOne(() => Order) order: Order; // (8.4)
@Column() provider: "payme" | "click" | "stripe";
@Column({ unique: true, nullable: true }) // UNIQUE — idempotency (2.8)
providerTxId: string;
@Column("bigint") amount: number; // TIYINDA (butun — 2.5)
@Column({ default: "UZS" }) currency: string;
@Column({ type: "enum", enum: TxState, default: TxState.PENDING })
state: TxState; // state machine (2.6)
@Column({ nullable: true }) paidAt: Date;
@Column({ nullable: true }) cancelledAt: Date;
@Column({ nullable: true }) reason: number; // bekor sababi
@CreateDateColumn() createdAt: Date;
}Misol 2 — Payme webhook (JSON-RPC handler — 2.10)
// payme.controller.ts — gateway BITTA endpoint'ni chaqiradi (Model A)
@Controller("payme")
export class PaymeController {
constructor(private paymeService: PaymeService) {}
@Post()
@UseGuards(PaymeAuthGuard) // Basic Auth (2.11)
@HttpCode(200)
async handle(@Body() dto: PaymeRequestDto) {
// method bo'yicha yo'naltirish (5 metod — 2.10)
switch (dto.method) {
case "CheckPerformTransaction": return this.paymeService.checkPerform(dto.params);
case "CreateTransaction": return this.paymeService.create(dto.params);
case "PerformTransaction": return this.paymeService.perform(dto.params);
case "CancelTransaction": return this.paymeService.cancel(dto.params);
case "CheckTransaction": return this.paymeService.check(dto.params);
case "GetStatement": return this.paymeService.statement(dto.params);
default:
return { error: { code: -32601, message: "Method topilmadi" } };
}
}
}Misol 3 — Payme auth guard (2.11)
// payme-auth.guard.ts
@Injectable()
export class PaymeAuthGuard implements CanActivate {
constructor(private config: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const auth = req.headers["authorization"] || "";
const [type, b64] = auth.split(" ");
if (type !== "Basic") throw new PaymeError(-32504, "Auth yo'q");
const [login, key] = Buffer.from(b64 || "", "base64").toString().split(":");
if (login !== "Paycom" || key !== this.config.get("PAYME_KEY")) { // kalit (2.7)
throw new PaymeError(-32504, "Avtorizatsiya xatosi");
}
return true;
}
}Misol 4 — Payme service: CheckPerform + Create (2.10)
@Injectable()
export class PaymeService {
constructor(
@InjectRepository(Transaction) private txRepo: Repository<Transaction>,
@InjectRepository(Order) private orderRepo: Repository<Order>,
) {}
// 1. To'lov mumkinmi? (buyurtma bor, summa to'g'ri — 2.4)
async checkPerform(params: any) {
const order = await this.orderRepo.findOneBy({ id: params.account.order_id });
if (!order) throw new PaymeError(-31050, "Buyurtma topilmadi");
if (order.amount * 100 !== params.amount) { // SUMMA tekshir (tiyinda — 2.5, 2.17)
throw new PaymeError(-31001, "Summa noto'g'ri");
}
if (order.state !== "pending") throw new PaymeError(-31008, "Buyurtma to'langan");
return { result: { allow: true } };
}
// 2. Tranzaksiya yaratish (idempotency — 2.8)
async create(params: any) {
const mavjud = await this.txRepo.findOneBy({ providerTxId: params.id });
if (mavjud) { // TAKROR eski natija (2.8)
return { result: { create_time: mavjud.createdAt.getTime(), transaction: mavjud.id, state: 1 } };
}
await this.checkPerform(params); // qayta tekshir
const tx = await this.txRepo.save({
providerTxId: params.id, provider: "payme",
orderId: params.account.order_id, amount: params.amount, state: TxState.PENDING,
});
return { result: { create_time: tx.createdAt.getTime(), transaction: tx.id, state: 1 } };
}
}Misol 5 — Payme service: Perform (to'lov tasdiqlash — 2.10)
// 3. To'lovni tasdiqlash (paid buyurtma bajariladi)
async perform(params: any) {
const tx = await this.txRepo.findOneBy({ providerTxId: params.id });
if (!tx) throw new PaymeError(-31003, "Tranzaksiya topilmadi");
if (tx.state === TxState.PAID) { // ALLAQACHON to'langan (idempotency — 2.8)
return { result: { transaction: tx.id, perform_time: tx.paidAt.getTime(), state: 2 } };
}
if (tx.state !== TxState.PENDING) throw new PaymeError(-31008, "Holat noto'g'ri");
tx.state = TxState.PAID; // state machine (2.6)
tx.paidAt = new Date();
await this.txRepo.save(tx);
// BUYURTMANI BAJARISH (eng muhim — pul keldi mahsulot/xizmat)
await this.orderRepo.update(tx.orderId, { state: "paid" });
// bu yerda: email/chek 8.10-bob, mahsulot berish, navbat (8.22)
return { result: { transaction: tx.id, perform_time: tx.paidAt.getTime(), state: 2 } };
}Misol 6 — Click Prepare + Complete (2.12)
@Controller("click")
export class ClickController {
constructor(private clickService: ClickService) {}
@Post("prepare")
@HttpCode(200)
prepare(@Body() dto: ClickPrepareDto) {
return this.clickService.prepare(dto);
}
@Post("complete")
@HttpCode(200)
complete(@Body() dto: ClickCompleteDto) {
return this.clickService.complete(dto);
}
}
@Injectable()
export class ClickService {
// Imzo tekshirish (MD5 — 2.9, 2.12)
private imzoTekshir(dto: any, secret: string): boolean {
const signString = createHash("md5").update(
`${dto.click_trans_id}${dto.service_id}${secret}${dto.merchant_trans_id}` +
`${dto.amount}${dto.action}${dto.sign_time}`,
).digest("hex");
return signString === dto.sign_string; // mos kelishi kerak
}
async prepare(dto: ClickPrepareDto) {
if (!this.imzoTekshir(dto, this.config.get("CLICK_SECRET"))) {
return { error: -1, error_note: "Imzo xato" }; // soxta rad (2.9)
}
const order = await this.orderRepo.findOneBy({ id: dto.merchant_trans_id });
if (!order) return { error: -5, error_note: "Buyurtma topilmadi" };
if (order.amount * 100 !== dto.amount) return { error: -2, error_note: "Summa xato" };
const tx = await this.txRepo.save({
providerTxId: dto.click_trans_id, provider: "click",
orderId: order.id, amount: dto.amount, state: TxState.PENDING,
});
return { error: 0, error_note: "Success", merchant_prepare_id: tx.id };
}
async complete(dto: ClickCompleteDto) {
if (!this.imzoTekshir(dto, this.config.get("CLICK_SECRET"))) {
return { error: -1, error_note: "Imzo xato" };
}
const tx = await this.txRepo.findOneBy({ providerTxId: dto.click_trans_id });
if (!tx) return { error: -6, error_note: "Tranzaksiya topilmadi" };
if (tx.state === TxState.PAID) return { error: 0, error_note: "Already paid" }; // idempotency
if (dto.error < 0) { // Click tomonda xato
tx.state = TxState.CANCELLED; await this.txRepo.save(tx);
return { error: dto.error, error_note: "Cancelled" };
}
tx.state = TxState.PAID; tx.paidAt = new Date();
await this.txRepo.save(tx);
await this.orderRepo.update(tx.orderId, { state: "paid" }); // buyurtma bajariladi
return { error: 0, error_note: "Success", merchant_confirm_id: tx.id };
}
}Misol 7 — Stripe PaymentIntent + webhook (2.13, 2.9)
@Controller("stripe")
export class StripeController {
private stripe = new Stripe(this.config.get("STRIPE_SECRET_KEY"));
constructor(private config: ConfigService, private orderRepo: OrderRepository) {}
// 1. PaymentIntent yaratish (frontend uchun)
@Post("intent")
@UseGuards(JwtAuthGuard) // (8.9)
async createIntent(@Body() dto: { orderId: string }) {
const order = await this.orderRepo.findById(dto.orderId);
const intent = await this.stripe.paymentIntents.create(
{ amount: order.amount, currency: "usd", metadata: { orderId: order.id } },
{ idempotencyKey: `order_${order.id}` }, // idempotency (2.8)
);
return { clientSecret: intent.client_secret };
}
// 2. Webhook (Stripe yuboradi — RAW body kerak! 2.9)
@Post("webhook")
@HttpCode(200)
async webhook(@Req() req: RawBodyRequest<Request>, @Headers("stripe-signature") sig: string) {
let event: Stripe.Event;
try {
event = this.stripe.webhooks.constructEvent( // IMZO tekshir (2.9)
req.rawBody, sig, this.config.get("STRIPE_WEBHOOK_SECRET"),
);
} catch {
throw new BadRequestException("Imzo xato"); // soxta rad
}
if (event.type === "payment_intent.succeeded") {
const intent = event.data.object as Stripe.PaymentIntent;
await this.tolovTasdiqla(intent.metadata.orderId, intent.id, event.id);
}
return { received: true };
}
private async tolovTasdiqla(orderId: string, txId: string, eventId: string) {
// idempotency — event ID saqlash (qayta ishlov bermaslik — 2.8)
// state machine paid buyurtma bajariladi (2.6)
}
}
// main.ts: app = NestFactory.create(AppModule, { rawBody: true }); RAW body (2.9)Misol 8 — Idempotency to'liq (2.8)
// Idempotency — webhook event'ni bir marta ishlash
@Entity("processed_events")
export class ProcessedEvent {
@PrimaryColumn() eventId: string; // UNIQUE (provider event ID)
@CreateDateColumn() processedAt: Date;
}
@Injectable()
export class IdempotencyService {
constructor(@InjectRepository(ProcessedEvent) private repo: Repository<ProcessedEvent>) {}
async birMartaIshla(eventId: string, ish: () => Promise<void>): Promise<void> {
const mavjud = await this.repo.findOneBy({ eventId });
if (mavjud) return; // ALLAQACHON ishlangan o'tkazib yubor
await ish(); // birinchi marta bajar
await this.repo.save({ eventId }); // belgilash
}
}
// Webhook'da: await idempotency.birMartaIshla(event.id, () => this.tolovTasdiqla(...));Misol 9 — Refund + unified payment service (2.14, Strategy 9.2)
// Unified payment — gateway'lar ustida abstraksiya (Strategy — 9.2: 2.9, DIP — 9.1)
interface PaymentProvider {
refund(txId: string, amount?: number): Promise<void>;
}
class StripeProvider implements PaymentProvider {
async refund(txId: string, amount?: number) {
await this.stripe.refunds.create({ payment_intent: txId, amount }); // (2.14)
}
}
class PaymeProvider implements PaymentProvider {
async refund(txId: string) { /* CancelTransaction — state -2 */ }
}
@Injectable()
export class PaymentService {
constructor(private providers: Map<string, PaymentProvider>) {}
async refund(txId: string, amount?: number) {
const tx = await this.txRepo.findOneBy({ id: txId });
if (tx.state !== TxState.PAID) throw new BadRequestException("Faqat to'langanni qaytarish mumkin"); // state machine (2.6)
const provider = this.providers.get(tx.provider); // qaysi gateway (Strategy)
await provider.refund(tx.providerTxId, amount);
tx.state = TxState.REFUNDED; // (2.6)
await this.txRepo.save(tx);
}
}Misol 10 — To'liq oqim (e-commerce — 2.4)
// 1. Buyurtma yaratish (to'lovdan oldin — pending)
@Post("orders")
@UseGuards(JwtAuthGuard)
async buyurtmaYarat(@Body() dto: CreateOrderDto, @CurrentUser() user) {
const order = await this.ordersService.yarat({ ...dto, userId: user.id, state: "pending" });
// To'lov havolasi (Payme/Click — checkout URL yoki invoice)
const tolovUrl = this.paymentService.tolovHavolasi("payme", order);
return { order, tolovUrl }; // frontend to'lov sahifasi
}
// 2. Mijoz to'laydi gateway webhook (Misol 2-7) state: paid
// 3. paid bo'lganda buyurtma bajariladi (perform ichida — Misol 5):
async buyurtmaBajar(orderId: string) {
await this.ordersService.holatYangila(orderId, "paid");
await this.inventoryService.zaxiraKamaytir(orderId); // ombor (8.13 transaction)
await this.emailQueue.add("chek", { orderId }); // chek email (8.10, navbat 8.22)
await this.notificationsGateway.yubor(orderId); // real-time (8.18)
}Misol 11 — Payme xato klassi (2.11)
// Payme JSON-RPC xato formati (exception filter bilan — 8.6)
export class PaymeError extends Error {
constructor(public code: number, message: string, public data?: any) {
super(message);
}
}
@Catch(PaymeError)
export class PaymeExceptionFilter implements ExceptionFilter {
catch(exception: PaymeError, host: ArgumentsHost) {
const res = host.switchToHttp().getResponse();
res.status(200).json({ // Payme — har doim 200 + error obyekt
error: { code: exception.code, message: { uz: exception.message, ru: exception.message, en: exception.message } },
});
}
}Misol 12 — Reconciliation cron (2.15)
// Davriy solishtirish (8.18 cron)
@Injectable()
export class ReconciliationService {
@Cron(CronExpression.EVERY_HOUR)
async solishtir() {
// Pending lekin eski tranzaksiyalar (webhook yetmagan bo'lishi mumkin)
const eskiPending = await this.txRepo.find({
where: { state: TxState.PENDING, createdAt: LessThan(new Date(Date.now() - 3600000)) },
});
for (const tx of eskiPending) {
// Gateway'dan haqiqiy holatni so'rash (Payme CheckTransaction / Stripe retrieve)
const haqiqiy = await this.gatewaydanHolatSora(tx);
if (haqiqiy === "paid" && tx.state !== TxState.PAID) {
// DB va gateway mos emas tuzatish (2.15)
await this.tolovTasdiqla(tx.orderId, tx.providerTxId, `recon_${tx.id}`);
this.logger.warn(`Reconciliation: ${tx.id} paid (webhook yetmagan)`);
}
}
}
}5. To'g'ri va noto'g'ri holatlar
1) Mijoz yuborgan summaga ishonish
frontend "summa: 100" shunga ishonish (firibgar 1 so'm to'laydi)
SERVER'da buyurtma summasi bilan solishtir (2.4, 2.17)2) Imzo tekshirmaslik
webhook keldi darrov "to'landi" (soxta — tekin mahsulot — 2.9)
imzo (signature) tekshir faqat haqiqiy gateway3) Idempotency yo'q
webhook 2 marta buyurtma 2 marta bajariladi (2.8)
providerTxId unique / event ID takror = bir marta4) Summa float'da
amount: 50000.50 (float — 0.1+0.2≠0.3 xatosi — 2.5)
tiyinda butun son (5000050)5) Kalit kodda
const KEY = "secret123" (git'ga pul o'g'irlanadi — 2.7)
.env + .gitignore6. Keng tarqalgan xatolar va yechimlari
Xato 1 — Payme integratsiya o'tmaydi (test)
Sababi: xato kodlari noto'g'ri, yoki summa tiyinda emas 2.11-bob. Yechimi: Payme spetsifikatsiyasi bo'yicha aniq xato kodlari; summa ×100.
Xato 2 — Webhook imzo har doim xato (Stripe)
Sababi: raw body emas (parse qilingan — 2.9). Yechimi: rawBody: true; constructEvent raw bilan.
Xato 3 — Buyurtma 2 marta bajariladi
Sababi: idempotency yo'q 2.8-bob. Yechimi: providerTxId unique; perform'da state tekshir.
Xato 4 — Webhook lokal kelmaydi
Sababi: localhost public emas 2.16-bob. Yechimi: ngrok (lokal URL public).
Xato 5 — DB va gateway mos emas
Sababi: webhook yetmadi (tarmoq — 2.15). Yechimi: reconciliation cron (Misol 12).
Xato 6 — Pul yechildi, buyurtma "pending"
Sababi: perform'da buyurtma yangilanmadi (Misol 5). Yechimi: perform ichida buyurtma bajarish + transaction (atomik — 8.13).
7. Integratsiya — bu mavzu stack'ning qayerida uchraydi
- Webhook (keyingi mavzu): to'lov callback.
- Controller/Service/DTO (8.1, 8.5): webhook, payment service.
- Guard (8.6, 8.9): Payme auth, JWT.
- DB transaction 8.13-bob: atomik (to'lov + buyurtma).
- Config 8.14-bob: kalitlar (.env).
- Navbat/email (8.22, 8.10): chek, fulfillment.
- Real-time 8.18-bob: to'lov bildirishnomasi.
- Idempotency: webhook, takror.
- Xavfsizlik (14): imzo, summa, sirlar.
- Telegram bot 8.12-bob: bot to'lov (invoice).
8. Eng yaxshi amaliyotlar (best practices)
- Summa server'da tekshiring (mijozga ishonmang — 2.4, 2.17).
- Imzo har webhook'da (soxta himoyasi — 2.9).
- Idempotency (providerTxId unique, event ID — 2.8).
- Summa tiyinda (butun son — float emas — 2.5).
- Kalitlar .env (git'ga emas, test/prod alohida — 2.7).
- State machine (holat o'tishlari nazorat — 2.6).
- HTTPS + karta saqlamaslik (PCI DSS — 2.1, 2.17).
- Audit log (har to'lov harakati — 2.17).
- Reconciliation (DBgateway solishtirish — 2.15).
- Sandbox test (ngrok, test kartalar — 2.16); to'lov+buyurtma atomik 8.13-bob.
9. Amaliy loyiha: "To'liq To'lov Tizimi"
To'lov integratsiyasini amalda mustahkamlash (eng qadrli ko'nikma).
Maqsad
E-commerce'ga Payme + Click + Stripe to'lov integratsiyasini qo'shish: webhook, imzo, idempotency, refund, reconciliation.
Talablar (requirements)
- Database: Transaction entity (state, providerTxId unique, tiyin — Misol 1, 2.5).
- Payme: JSON-RPC handler (5 metod), auth guard (Misol 2-5, 2.10).
- Click: Prepare/Complete + imzo (Misol 6, 2.12).
- Stripe: PaymentIntent + webhook + constructEvent (Misol 7, 2.13).
- Idempotency: providerTxId unique + event ID (Misol 8, 2.8).
- Imzo tekshirish: har provider 2.9-bob.
- Summa tekshirish: server'da (mijozga ishonmaslik — 2.4).
- Refund: unified service (Misol 9, 2.14).
- Fulfillment: paid buyurtma + email + zaxira (atomik — Misol 10, 5, 8.13).
- Reconciliation: cron solishtirish (Misol 12, 2.15).
Maslahatlar (hint)
- Summa server'da + tiyinda (2.4, 2.5, 4-holat).
- Imzo har webhook (2.9, 2-holat).
- Idempotency (providerTxId unique — 2.8, 3-xato).
- Stripe raw body (2.9, 2-xato).
- Kalitlar .env (2.7, 5-holat).
- ngrok lokal test (2.16, 4-xato).
"Tayyor" mezonlari (acceptance criteria)
- Transaction entity (state, unique, tiyin).
- Payme (5 metod + auth).
- Click (Prepare/Complete + imzo).
- Stripe (PaymentIntent + webhook).
- Idempotency.
- Imzo tekshirish (har provider).
- Summa server tekshiruvi.
- Refund.
- Fulfillment (atomik).
- Reconciliation.
Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.
10. Xulosa va keyingi bobga ko'prik
Bu bobda to'lov tizimlarini noldan, to'liq o'rgandik:
- Ekotizim (merchant/gateway/bank — 2.1); O'zbekiston tizimlari (Payme/Click/Uzum — 2.2); integratsiya modellari 2.3-bob; universal oqim 2.4-bob.
- Database (transaction — tiyin, unique — 2.5); state machine 2.6-bob; kalitlar (.env — 2.7); idempotency (takror himoyasi — 2.8); webhook + imzo (xavfsizlik — 2.9).
- Payme (JSON-RPC — 5 metod — 2.10, 2.11); Click (Prepare/Complete — 2.12); Stripe (PaymentIntent + webhook — 2.13).
- Refund 2.14-bob; reconciliation 2.15-bob; test (sandbox — 2.16); xavfsizlik (14 — 2.17).
Bu — sizning kareyrangizdagi eng qadrli, eng amaliy ko'nikmalardan. Endi har real loyihaga to'lov qo'sha olasiz.
Endi 9-QISM (Arxitektura)ga qaytamiz — keyingi bob 9.5: Monolith vs Microservices. To'lov kabi real mavzular (webhook, PDF, qidiruv) keyinroq kerak bo'lganda qo'shamiz; hozir arxitektura yo'nalishida davom etamiz.
Foydalanilgan rasmiy/ishonchli manbalar
- developer.help.paycom.uz — Payme Merchant API (CheckPerformTransaction, CreateTransaction, ...)
- docs.click.uz — Click SHOP API (Prepare/Complete, sign_string)
- docs.stripe.com — PaymentIntents, Webhooks (constructEvent), Idempotency, Refunds
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!