Mundarija (21)
- Bu darsda
- 1. Nega bu kerak?
- 2. Ishtirokchilar
- 3. Uch oqim turi
- 3.1 Server-to-server: provayder bizni chaqiradi
- 3.2 Redirect (checkout): mijoz provayder sahifasiga
- 3.3 Webhook: provayder voqea haqida xabar beradi
- 4. Buyurtma holat mashinasi
- 5. Sxema: tolov bazasi
- 6. Parallel so'rovlar: qatorni qulflash
- 7. Sandbox, test kalitlar va sirlar
- 8. Oqim qancha vaqt oladi
- 9. Ko'p uchraydigan xatolar
- 10. Mashqlar
- 1-mashq (oson): qaysi oqim?
- 2-mashq (o'rta): takroriy so'rovni tanish
- 3-mashq (qiyin): holat tarixi
- 11. Loyiha qadami: tolov paketi va tranzaksiya()
- 12. Real ishda
- Xulosa
- Manbalar
To'lov ekotizimi va umumiy oqim: merchant, provayder va tranzaksiyalar jadvali
Qisqacha: Payme, Click, Uzum va Stripe — har biri boshqa protokol, lekin oqim bir xil: buyurtma → to'lov urinishi → tasdiq → xizmat. Sizning serveringiz — merchant (sotuvchi). Pulni kartadan yechadigan — provayder. Ular uch xil yo'l bilan gaplashadi: provayder sizning API'ingizni chaqiradi (server-to-server), mijozni provayder sahifasiga yuborasiz (redirect) yoki provayder voqea haqida xabar beradi (webhook). Hamma oqim bitta bazaga tushadi:
buyurtmalar,tranzaksiyalar,kiruvchi_hodisalar,hisob_yozuvlari. Bazaning o'zi ikki marta to'lashga yo'l qo'ymaydi.
Bu darsda
- To'lovdagi ishtirokchilarni (merchant, provayder, bank, mijoz) ajratasiz.
- Uch oqim turini va qaysi provayder qaysi birini ishlatishini bilasiz.
- Buyurtma holat mashinasini (state machine) yozasiz.
tolovbazasi sxemasini o'qiysiz: cheklovlar ikki marta to'lashni qanday to'xtatadi.- Sandbox, test kalitlar va sirlarni qanday saqlashni bilasiz.
tolovpaketining tuzilmasi vatranzaksiya()yordamchisini qo'shasiz.
Oldin bilishingiz kerak: Pul bilan ishlash JS'da, Tranzaksiya asoslari, Partial, expression va covering indekslar, Qulflar: row, table va advisory, Idempotency-Key.
Oldingi darsda pulni tiyinda saqlashni o'rgandik. Endi bu tiyinlar mijozning kartasidan sizning hisobingizga qanday yetib kelishini ko'ramiz.
1. Nega bu kerak?
Tasavvur qiling: onlayn do'kon. Mijoz "To'lash" tugmasini bosdi, Payme sahifasida karta ma'lumotini kiritdi, pul yechildi. Shu payt internet uzildi. Do'kon serveri "to'landi" xabarini oldimi? Mijoz qayta bosdi — ikki marta yechildimi? Buyurtma yetkazildimi?
To'lov — tarmoq orqali ikki mustaqil tizim o'rtasidagi kelishuv. Har qadamda xabar yo'qolishi, kechikishi yoki ikki marta kelishi mumkin. Shuning uchun to'lov kodining asosiy vazifasi — chiroyli tugma emas, balki shu savollarga aniq javob:
- Bu buyurtma to'landimi? (holat)
- Bu xabarni avval ko'rganmizmi? (idempotentlik — idempotency)
- Bizdagi yozuv provayderdagi yozuvga mosmi? (solishtirish — reconciliation)
Bu blokning hamma darsi shu uch savolga qaytadi.
2. Ishtirokchilar
| Kim | Vazifasi | Misol |
|---|---|---|
| Mijoz | kartasi bilan to'laydi | Aziz |
| Merchant | tovar sotadi, buyurtma yaratadi | sizning do'koningiz |
| Provayder (PSP) | kartadan pul yechadi, merchantga xabar beradi | Payme, Click, Uzum, Stripe |
| Bank va karta tizimi | pulni haqiqatda o'tkazadi | Uzcard, Humo, Visa |
Provayder (payment service provider, PSP) — sizni banklar bilan bog'laydigan vositachi. Siz har bank bilan alohida shartnoma tuzmaysiz: provayder bilan bitta shartnoma, bitta API. Ba'zi provayderlar aggregator — ular orqali bir nechta to'lov usuli (Payme, Click, karta) bitta sahifada chiqadi.
Pul ham darhol sizning hisobingizga tushmaydi. Avval provayderda yig'iladi, keyin shartnoma bo'yicha (kunlik yoki haftalik) bank hisobingizga o'tkaziladi — komissiya ushlanib. Shuning uchun ichki hisobda "provayderdagi pulimiz" alohida turadi (Refund, reconciliation va moliyaviy hisob darsida).
3. Uch oqim turi
3.1 Server-to-server: provayder bizni chaqiradi
O'zbekiston provayderlari (Payme, Click, Uzum) asosan shunday ishlaydi. Mijoz provayder ilovasida yoki sahifasida to'laydi, provayder esa sizning serveringizga so'rov yuboradi: "bu buyurtma bormi?", "tranzaksiya yarataman", "pul yechildi, tasdiqla". Siz merchant API ni amalga oshirasiz — provayder hujjatida yozilgan formatda javob berasiz.
sequenceDiagram
participant M as Mijoz
participant P as Provayder
participant S as Bizning server
participant DB as Baza
M->>P: To'lash (karta)
P->>S: Tekshir: buyurtma 42, 12 500 so'm
S->>DB: buyurtma bormi, summa mosmi
S-->>P: ruxsat
P->>S: Tranzaksiya yarat (tashqi id)
S->>DB: tranzaksiya 'yaratilgan', buyurtma 'kutilmoqda'
S-->>P: yaratildi
P->>P: kartadan yechish
P->>S: Bajar (tasdiqla)
S->>DB: 'bajarilgan', buyurtma 'tolangan', ledger
S-->>P: bajarildi
P-->>M: To'lov o'tdiDiagrammaga qarang: pul yechilishidan oldin provayder sizdan ruxsat so'raydi va tranzaksiya yaratadi. Keyin pul yechiladi va yana sizga "bajar" deydi. Har bosqichda javobingiz kechiksa yoki yo'qolsa, provayder qayta so'raydi — shuning uchun har metod takroriy chaqiruvga tayyor bo'lishi shart.
3.2 Redirect (checkout): mijoz provayder sahifasiga
Siz provayder API'ini chaqirasiz ("12 500 so'mlik to'lov sessiyasi yarat"), u havola qaytaradi, mijozni o'sha havolaga yuborasiz. To'lov tugagach mijoz success_url ga qaytadi. Stripe Checkout va Uzum Checkout shunday.
Stripe'ni misol sifatida o'rganamiz: dunyodagi ko'p to'lov API'lari unga o'xshab qurilgan. Lekin Stripe'ning rasmiy ro'yxatida O'zbekiston yo'q — u yerda ro'yxatdan o'tgan biznes Stripe hisobini ocha olmaydi (stripe.com/global, 2026-10-28 holatiga). Batafsil — Stripe darsida.
Muhim: mijozning success_url ga qaytishi — to'lov isboti emas. Mijoz manzilni qo'lda yozishi yoki brauzerni yopishi mumkin. Haqiqiy natija keyingi oqim bilan keladi.
3.3 Webhook: provayder voqea haqida xabar beradi
To'lov tugagach provayder sizning URL'ingizga POST yuboradi: "payment_intent.succeeded, summa 1 250 000". Siz imzoni tekshirasiz, takrorni filtrlaysiz va buyurtmani to'langan qilasiz. Stripe shunday (Kiruvchi webhook'larni qabul qilish darsida chuqur).
| Oqim | Kim kimni chaqiradi | Provayderlar (kanon) |
|---|---|---|
| server-to-server | provayder → biz (sinxron) | Payme, Click, Uzum |
| redirect/checkout | biz → provayder, mijoz o'tadi | Stripe Checkout, Uzum Checkout |
| webhook | provayder → biz (asinxron) | Stripe |
Amalda ko'pincha ikkitasi birga: Stripe'da sessiya redirect bilan, natija webhook bilan.
Tekshirib ko'ring: Mijoz Stripe sahifasida to'ladi va
success_urlga qaytdi. Siz shu so'rovni ko'rib buyurtmani "to'langan" qildingiz. Nima xato?
Javob
success_url ga istalgan odam to'lovsiz kira oladi — bu oddiy GET so'rov. Buyurtma holatini faqat provayderdan kelgan tekshirilgan xabar (imzoli webhook yoki provayder API'idan so'rash) o'zgartiradi. success_url faqat mijozga "rahmat, tekshiryapmiz" sahifasini ko'rsatadi.
4. Buyurtma holat mashinasi
Buyurtma har doim aniq bitta holatda va faqat ruxsat etilgan o'tishlar bilan o'zgaradi — bu holat mashinasi (state machine). Kanon buyurtmalar jadvalida beshta holat:
stateDiagram-v2
[*] --> yangi
yangi --> kutilmoqda: tranzaksiya yaratildi
kutilmoqda --> tolangan: bajarildi
kutilmoqda --> yangi: bekor yoki timeout
yangi --> bekor: mijoz bekor qildi
tolangan --> qaytarilgan: refund
bekor --> [*]
qaytarilgan --> [*]kutilmoqda → yangi o'tishiga e'tibor bering: to'lov urinishi muvaffaqiyatsiz bo'lsa (Payme 12 soatlik timeout, Click'da mablag' yetmadi), buyurtma qayta to'lashga ochiladi. Mijoz boshqa karta yoki boshqa provayder bilan urinib ko'radi.
Kodda holat mashinasi — "holat × hodisa → yangi holat" jadvali:
const OTISH = {
yangi: { tolov_boshlandi: "kutilmoqda", bekor_qil: "bekor" },
kutilmoqda: {
tolandi: "tolangan",
tolov_bekor: "yangi",
bekor_qil: "bekor",
},
tolangan: { qaytarildi: "qaytarilgan" },
bekor: {},
qaytarilgan: {},
};
function keyingi(holat, hodisa) {
const yangi = OTISH[holat]?.[hodisa];
if (!yangi) throw new Error(`${holat} → ${hodisa}: mumkin emas`);
return yangi;
}
let h = "yangi";
for (const e of ["tolov_boshlandi", "tolov_bekor",
"tolov_boshlandi", "tolandi"]) {
h = keyingi(h, e);
console.log(e, "→", h);
}
try {
keyingi("tolangan", "tolandi");
} catch (e) {
console.log(e.message);
}Konsolda:
tolov_boshlandi → kutilmoqda
tolov_bekor → yangi
tolov_boshlandi → kutilmoqda
tolandi → tolangan
tolangan → tolandi: mumkin emasOxirgi qator — to'lov blokidagi eng muhim himoya: allaqachon to'langan buyurtma ikkinchi marta "to'landi" bo'lmaydi. Provayder xabarni qayta yuborsa, kod xatoni emas, oldingi natijani qaytarishi kerak (idempotentlik). Buni keyingi darslarda har provayder uchun ko'ramiz.
Kanon jadvalda alohida tranzaksiya holatlari ham bor: yaratilgan, bajarilgan, bekor, qaytarilgan. Buyurtma — "nima sotildi", tranzaksiya — "qaysi provayder orqali qaysi urinish". Bitta buyurtmada bir nechta tranzaksiya bo'lishi mumkin: Payme'da bekor qilingan urinish va Click'da muvaffaqiyatli urinish.
5. Sxema: tolov bazasi
Kanon to'lov qatlamini alohida bazaga qo'ydi (tolov/sql/001-tolov.sql). Beshta jadval:
| Jadval | Nima saqlaydi |
|---|---|
buyurtmalar |
summa (tiyin, bigint), holat |
tranzaksiyalar |
provayder, tashqi id, buyurtma, summa, holat, vaqtlar |
kiruvchi_hodisalar |
webhook'lar (inbox) — takrorni filtrlash uchun |
hisob_yozuvlari |
ikki yoqlama yozuv (ledger) |
qaytarishlar |
refund'lar, idempotentlik kaliti bilan |
Asosiy ikkita jadval (izohlar qisqartirilgan, ustunlar kanondagidek):
CREATE TABLE buyurtmalar (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
foydalanuvchi_id bigint NOT NULL,
summa bigint NOT NULL CHECK (summa > 0),
holat text NOT NULL DEFAULT 'yangi'
CHECK (holat IN ('yangi', 'kutilmoqda', 'tolangan',
'bekor', 'qaytarilgan')),
tavsif text NOT NULL DEFAULT '',
yaratilgan_vaqt timestamptz NOT NULL DEFAULT now(),
yangilangan_vaqt timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE tranzaksiyalar (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
provayder text NOT NULL
CHECK (provayder IN ('payme', 'click', 'uzum', 'stripe')),
tashqi_id text NOT NULL, -- Payme id, click_trans_id ...
buyurtma_id bigint NOT NULL REFERENCES buyurtmalar (id),
summa bigint NOT NULL CHECK (summa > 0),
holat text NOT NULL CHECK (holat IN ('yaratilgan',
'bajarilgan', 'bekor', 'qaytarilgan')),
sabab smallint, -- Payme bekor sababi
tashqi_vaqt bigint, -- provayder vaqti (ms)
yaratilgan_vaqt timestamptz NOT NULL DEFAULT now(),
bajarilgan_vaqt timestamptz,
bekor_vaqt timestamptz,
qoshimcha jsonb NOT NULL DEFAULT '{}',
CONSTRAINT tranzaksiyalar_tashqi_key
UNIQUE (provayder, tashqi_id),
CONSTRAINT tranzaksiyalar_vaqt_check CHECK (
(holat = 'bajarilgan') <= (bajarilgan_vaqt IS NOT NULL))
);
CREATE UNIQUE INDEX tranzaksiyalar_faol_key
ON tranzaksiyalar (buyurtma_id) WHERE holat = 'yaratilgan';Har cheklov — bitta xato turiga qarshi. Ularni o'z Postgres 17.6 klasterimizda kanon sxemasi ustida sinadik (biz ishga tushirdik). \set VERBOSITY terse — xato xabarining faqat birinchi qatori (DETAIL ichida vaqt bor, u har safar boshqa).
Buyurtma yaratamiz:
INSERT INTO buyurtmalar (foydalanuvchi_id, summa)
VALUES (7, 1250000)
RETURNING id, holat, summa; id | holat | summa
----+-------+---------
1 | yangi | 1250000
(1 row)
INSERT 0 1Payme orqali birinchi urinish — tranzaksiya yaratilgan:
INSERT INTO tranzaksiyalar
(provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('payme', 'p-1', 1, 1250000, 'yaratilgan');INSERT 0 1Endi mijoz boshqa oynada Click orqali ham to'lamoqchi. Ikkinchi faol tranzaksiya:
\set VERBOSITY terse
INSERT INTO tranzaksiyalar
(provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('click', '4000001', 1, 1250000, 'yaratilgan');ERROR: duplicate key value violates unique constraint "tranzaksiyalar_faol_key"Bu qisman noyob indeks (Partial, expression va covering indekslar): WHERE holat = 'yaratilgan' qatorlari orasida buyurtma_id noyob. Bekor qilingan va bajarilgan tranzaksiyalar hisobga kirmaydi — ular istalgancha bo'lishi mumkin. Bitta buyurtmada bir vaqtda faqat bitta jarayondagi to'lov. Kod xato qilsa ham, ikki provayder bir buyurtmaga parallel pul yecha olmaydi.
Provayder o'sha tranzaksiyani ikkinchi marta yubordi:
\set VERBOSITY terse
INSERT INTO tranzaksiyalar
(provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('payme', 'p-1', 1, 1250000, 'bekor');ERROR: duplicate key value violates unique constraint "tranzaksiyalar_tashqi_key"UNIQUE (provayder, tashqi_id) — bitta provayder tranzaksiyasi bazada bir marta. Bu idempotentlikning poydevori: takroriy so'rov yangi qator yaratmaydi, kod esa mavjud qatorni topib, oldingi javobni qaytaradi. Kalit juft: Payme'ning p-1 i va Click'ning p-1 i — har xil tranzaksiyalar.
Ikki kichik, lekin foydali tekshiruv:
\set VERBOSITY terse
UPDATE buyurtmalar SET holat = 'tolandi' WHERE id = 1;
INSERT INTO tranzaksiyalar
(provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('payme', 'p-2', 1, 1250000, 'bajarilgan');ERROR: new row for relation "buyurtmalar" violates check constraint "buyurtmalar_holat_check"
ERROR: new row for relation "tranzaksiyalar" violates check constraint "tranzaksiyalar_vaqt_check"Birinchisi — holat nomidagi xato (tolandi emas, tolangan). Ikkinchisi qiziqroq: (holat = 'bajarilgan') <= (bajarilgan_vaqt IS NOT NULL). Postgres'da false < true, shuning uchun bu ifoda "agar bajarilgan bo'lsa, bajarilgan vaqt bo'lishi shart" degani (mantiqdagi implikatsiya). Vaqtsiz "bajarilgan" yozuv — keyin hisobotda "qachon to'langan?" savoliga javob yo'q.
Qolgan jadvallar keyingi darslarda: kiruvchi_hodisalar — Stripe va webhook darslarida, hisob_yozuvlari va qaytarishlar — refund darsida.
6. Parallel so'rovlar: qatorni qulflash
Qisman indeks oxirgi chiziq. Kanon kodi bundan oldin ham himoya qiladi: tranzaksiya yaratishda buyurtma qatorini SELECT … FOR UPDATE bilan qulflaydi (Qulflar: row, table va advisory). Ikkita parallel so'rov kelsa, ikkinchisi birinchisi tugashini kutadi, keyin "faol tranzaksiya bor" ni ko'radi va toza xato qaytaradi (Payme'da -31008). Indeksga borib urilish (duplicate key) esa kutilmagan xato bo'lib qolardi.
Kanon buni o'lchadi: Payme simulyatori bitta buyurtmaga 10 ta parallel CreateTransaction yubordi — 1 muvaffaqiyat, 9 ta -31008. Click'da 10 ta parallel prepare — bitta faol tranzaksiya. Bular keyingi darslarda.
7. Sandbox, test kalitlar va sirlar
Har provayder sandbox (sinov muhiti) beradi: haqiqiy pul yechilmaydi, test kartalar ishlaydi. Lekin O'zbekiston provayderlarida sandboxga kirish uchun ham odatda hamkorlik shartnomasi va kassa (merchant kabineti) kerak.
Diqqat: Bu kursning to'lov darslari lokal simulyator bilan yozilgan. Kanon Payme, Click va Uzum serverlarining xatti-harakatini hujjat bo'yicha takrorlaydigan lokal soxta serverlar (simulyatorlar) yozdi. Stripe uchun Stripe'ning rasmiy
stripe-mockdasturi ishlatildi. Haqiqiy Payme, Click, Uzum va Stripe — sinalmagan (2026-10-27). Haqiqiy integratsiyada provayder hujjati va test kabineti ustun.
Kanon testlari Payme'ning hujjatdagi "pesochnitsa" (sandbox) ssenariylarini takrorladi: takroriy Create/Perform/Cancel — o'sha javob, buyurtmada boshqa faol tranzaksiya — -31008.
Sirlar (Sirlarni boshqarish): kassa kaliti, service_id va webhook siri — .env da, kodda emas. Sandbox va prod kalitlari alohida. Namuna (.env.example uslubida, qiymatlar soxta):
# .env.example — haqiqiy qiymatlar .env da, git'ga qo'shilmaydi
DATABASE_URL=postgres://postgres@127.0.0.1:5432/tolov
PAYME_LOGIN=Paycom
PAYME_KALIT=almashtiring
CLICK_SERVICE_ID=00000
CLICK_SIR=almashtiring
UZUM_LOGIN=almashtiring
UZUM_PAROL=almashtiring
STRIPE_KALIT=sk_test_almashtiring
STRIPE_WEBHOOK_SIR=whsec_almashtiring8. Oqim qancha vaqt oladi
Kanon har provayder uchun 100 ta to'liq to'lovni ketma-ket o'tkazdi (lokal simulyator + Postgres 18.6, bitta mashina — tarmoq kechikishi yo'q):
Payme (Check+Create+Perform) mediana 44.0 ms, p95 183.3 ms, muvaffaqiyatli 100/100
Click (Prepare+Complete) mediana 42.8 ms, p95 224.5 ms, muvaffaqiyatli 100/100
Uzum (check+create+confirm) mediana 40.3 ms, p95 54.9 ms, muvaffaqiyatli 100/100Bu bizning kod va bazaning ulushi. Haqiqiy dunyoda provayder bilan tarmoq va karta tizimi qo'shiladi. Provayderlar javobni ma'lum vaqt kutadi (Payme hujjatida — har metodga timeout), shuning uchun merchant API'da og'ir ish (email, PDF chek) qilinmaydi — u navbatga qo'yiladi (BullMQ asoslari).
9. Ko'p uchraydigan xatolar
| Xato | Oqibati | To'g'risi |
|---|---|---|
success_url ga ishonish |
to'lovsiz buyurtma | faqat provayder xabari |
| tashqi id'ni saqlamaslik | takror so'rov — yangi tranzaksiya | UNIQUE (provayder, tashqi_id) |
| buyurtmada bir nechta faol to'lov | ikki marta yechish | qisman noyob indeks + FOR UPDATE |
| holat ustuni erkin matn | tolandi, paid, TOLANGAN |
CHECK yoki enum |
| summani provayderdan olib yozish | mijoz summani o'zgartiradi | summa bizning buyurtmadan |
| kalitlar kodda | sir git tarixida qoladi | .env, sandbox/prod alohida |
| merchant API'da sekin ish | provayder timeout'i | navbat |
10. Mashqlar
Mashqlar kurs/mashqlar/29/28-ekotizim/ da.
1-mashq (oson): qaysi oqim?
Har holat uchun oqim turini ayting (server-to-server, redirect, webhook):
- Payme serveri sizning
/tolov/paymemanzilingizgaCheckPerformTransactionyuboradi. - Siz Stripe'dan sessiya havolasini olib, mijozni o'sha sahifaga yuborasiz.
- Stripe
payment_intent.succeededhodisasini sizning URL'ingizga POST qiladi. - Click
preparevacompleteso'rovlarini sizga yuboradi.
Yechim
- Server-to-server. 2. Redirect (checkout). 3. Webhook. 4. Server-to-server.
2-mashq (o'rta): takroriy so'rovni tanish
Bazasiz, Map bilan tranzaksiyaYarat(provayder, tashqiId, buyurtmaId) yozing. Kalit — ${provayder}:${tashqiId}. Bor bo'lsa — o'sha yozuvni { yangi: false } bilan qaytarsin. Yo'q bo'lsa, lekin shu buyurtmada boshqa yaratilgan yozuv bo'lsa — "faol-bor" xatosi. Aks holda yangi yozuv (id — tartib raqami). To'rt chaqiruvni sinang: payme a → 1, payme a (takror), click a (boshqa provayder, o'sha buyurtma), payme b buyurtma 2.
Yechim
const jadval = new Map();
let keyingiId = 1;
function tranzaksiyaYarat(provayder, tashqiId, buyurtmaId) {
const kalit = `${provayder}:${tashqiId}`;
const bor = jadval.get(kalit);
if (bor) return { ...bor, yangi: false };
for (const t of jadval.values()) {
if (t.buyurtmaId === buyurtmaId && t.holat === "yaratilgan") {
throw new Error("faol-bor");
}
}
const t = { id: keyingiId++, buyurtmaId, holat: "yaratilgan" };
jadval.set(kalit, t);
return { ...t, yangi: true };
}
console.log(tranzaksiyaYarat("payme", "a", 1));
console.log(tranzaksiyaYarat("payme", "a", 1));
try {
tranzaksiyaYarat("click", "a", 1);
} catch (e) {
console.log(e.message);
}
console.log(tranzaksiyaYarat("payme", "b", 2));Konsolda:
{ id: 1, buyurtmaId: 1, holat: 'yaratilgan', yangi: true }
{ id: 1, buyurtmaId: 1, holat: 'yaratilgan', yangi: false }
faol-bor
{ id: 2, buyurtmaId: 2, holat: 'yaratilgan', yangi: true }Bu bazadagi ikki cheklovning xotiradagi nusxasi: Map kaliti — UNIQUE (provayder, tashqi_id), sikl — qisman indeks. Farqi: xotirada parallel so'rovlar va server qayta ishga tushishi hisobga olinmaydi — shuning uchun haqiqiy kodda bu tekshiruvlarni baza qiladi.
3-mashq (qiyin): holat tarixi
OTISH jadvalidan foydalanib qolla(hodisalar) yozing: "yangi" dan boshlab hodisalarni ketma-ket qo'llaydi va har qadamni tarixga yozadi. Mumkin bo'lmagan hodisa uchiradi — u holda tarixda rad: <hodisa> qoladi, holat o'zgarmaydi va keyingi hodisalar davom etadi. Oxirida tarix va yakuniy holatni chiqarsin. Hodisalar: tolov_boshlandi, tolandi, tolandi, qaytarildi, tolov_boshlandi.
Yechim
const OTISH = {
yangi: { tolov_boshlandi: "kutilmoqda", bekor_qil: "bekor" },
kutilmoqda: {
tolandi: "tolangan",
tolov_bekor: "yangi",
bekor_qil: "bekor",
},
tolangan: { qaytarildi: "qaytarilgan" },
bekor: {},
qaytarilgan: {},
};
function qolla(hodisalar) {
let holat = "yangi";
const tarix = [];
for (const e of hodisalar) {
const yangi = OTISH[holat][e];
if (yangi) {
tarix.push(`${holat} → ${yangi}`);
holat = yangi;
} else {
tarix.push(`rad: ${e}`);
}
}
return { holat, tarix };
}
const r = qolla(["tolov_boshlandi", "tolandi", "tolandi",
"qaytarildi", "tolov_boshlandi"]);
console.log(r.tarix.join("\n"));
console.log("yakuniy:", r.holat);Konsolda:
yangi → kutilmoqda
kutilmoqda → tolangan
rad: tolandi
tolangan → qaytarilgan
rad: tolov_boshlandi
yakuniy: qaytarilganIkkinchi tolandi — provayderning takroriy xabari: rad etildi, holat buzilmadi. qaytarilgan — yakuniy holat, undan chiqish yo'q. Haqiqiy tizimda bunday tarix audit izi (audit trail) bo'ladi: kim, qachon, qaysi holatdan qaysiga.
11. Loyiha qadami: tolov paketi va tranzaksiya()
Kanon to'lov kodini alohida paketga yig'di. U vazifalar-api ga kirmaydi — 38-qismdagi bozor loyihasida (do'kon) ishlatiladi. Tuzilma:
tolov/
package.json express 5.3.0, pg 8.23.1, stripe 23.0.0, zod 4.6.7
sql/001-tolov.sql 5 jadval, ledger triggerlari, view
src/pul.ts oldingi dars
src/db.ts tranzaksiya() va ledger yozuvlari
src/payme/ Payme Merchant API
src/click/ Click SHOP API
src/uzum/ Uzum Merchant API
src/stripe/ Stripe
src/obuna.ts obuna holat mashinasi
src/hisob.ts refund va reconciliation
mock/ Payme, Click, Uzum simulyatorlari
test/ 7 ta test fayliBu darsda src/db.ts ning birinchi qismi — tranzaksiya() yordamchisi. Har merchant metodi o'z ishini bitta ma'lumotlar bazasi tranzaksiyasida bajaradi (Tranzaksiya asoslari): xato bo'lsa ROLLBACK, ulanish doim hovuzga (pool) qaytadi. Formatlangan nusxa, ma'nosi kanondagidek:
// db.ts — tranzaksiya yordamchisi va ledger (ikki yoqlama yozuv).
import { randomUUID } from "node:crypto";
import type { Pool, PoolClient } from "pg";
export async function tranzaksiya<T>(
pool: Pool,
ish: (c: PoolClient) => Promise<T>,
): Promise<T> {
const c = await pool.connect();
try {
await c.query("BEGIN");
const r = await ish(c);
await c.query("COMMIT");
return r;
} catch (e) {
await c.query("ROLLBACK");
throw e;
} finally {
c.release();
}
}Fayl davomidagi ledgerYoz, tolovYozuvi va qaytarishYozuvi — refund va ledger darsida.
Testlar har test fayli uchun vaqtinchalik baza yaratadi (tolovBazasi() — CREATE DATABASE, 001-tolov.sql ni qo'llash, oxirida DROP DATABASE) va Express serverni listen(0) bilan ochadi. DATABASE_URL bo'lmasa, baza testlari o'tkazib yuboriladi. Kanonda Postgres 18.6 bilan 33/33 o'tdi; biz Postgres 17.6 klasterimizda ishga tushirdik — 32 o'tdi, Stripe-mock testi (Docker kerak) o'tkazib yuborildi.
Commit xabari:
M='feat(tolov): paket, 001-tolov.sql sxemasi'
M+=' va tranzaksiya() yordamchisi'
git commit -m "$M"12. Real ishda
- Provayder tanlovi — komissiya, pulni hisobingizga o'tkazish muddati, qaysi kartalar va ilovalar, hujjat sifati. O'zbekistonda do'konlar ko'pincha Payme va Click'ni birga ulaydi.
- Hujjatni o'qish ko'nikmasi — har provayderda bir xil savollar: qaysi oqim, qanday auth, summa qaysi birlikda, xato kodlari, takroriy so'rovlar, timeout'lar. Keyingi to'rt dars shu ro'yxat bo'yicha boradi.
- PCI DSS — karta ma'lumotlarini saqlash standarti. Redirect va server-to-server oqimlarda karta raqami sizning serveringizga umuman kelmaydi — bu talablarni ancha kamaytiradi.
- Intervyuda: "To'lov ikki marta o'tib ketmasligini qanday ta'minlaysiz?", "Webhook kelmasa nima qilasiz?"
Xulosa
- To'lovda to'rt ishtirokchi: mijoz, merchant (siz), provayder, bank.
- Uch oqim: server-to-server (Payme, Click, Uzum), redirect (Stripe Checkout), webhook (Stripe).
success_url— isbot emas. - Buyurtma — holat mashinasi; to'langan buyurtma qayta "to'landi" bo'lmaydi.
UNIQUE (provayder, tashqi_id)takrorni, qisman noyob indeks parallel faol to'lovni to'xtatadi;FOR UPDATEtoza xato beradi.- Sandbox uchun ham shartnoma kerak; bu kurs — lokal simulyator, haqiqiy provayderlar sinalmagan.
Keyingi dars: Payme Merchant API — JSON-RPC, oltita metod, 12 soatlik timeout va xato kodlari.
Manbalar
- Payme Business, Merchant API: https://developer.help.paycom.uz — kanon 2026-10-27 da o'qigan
- Click: https://docs.click.uz va https://github.com/click-llc/click-integration-php — kanon 2026-10-27 da o'qigan
- Uzum Bank Merchant API: https://developer.uzumbank.uz/merchant — kanon 2026-10-27 da o'qigan
- Stripe, to'lov oqimlari: https://docs.stripe.com/payments/accept-a-payment
- PostgreSQL qisman indekslar: https://www.postgresql.org/docs/18/indexes-partial.html
- PCI DSS: https://www.pcisecuritystandards.org
- Kanon (IlmHamroh, 2026-10-27):
tolovpaketi, PostgreSQL 18.6, Node 24.21.0; haqiqiy provayderlar sinalmagan. SQL va testlar — biz ishga tushirdik, PostgreSQL 17.6 (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!