IlmHamroh
JavaScript Full-stack/29-qism. Backend integratsiyalar: fayl, to'lov, bot, realtime28/69-dars16 daqiqa
Mundarija (21)

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.
  • tolov bazasi sxemasini o'qiysiz: cheklovlar ikki marta to'lashni qanday to'xtatadi.
  • Sandbox, test kalitlar va sirlarni qanday saqlashni bilasiz.
  • tolov paketining tuzilmasi va tranzaksiya() 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:

  1. Bu buyurtma to'landimi? (holat)
  2. Bu xabarni avval ko'rganmizmi? (idempotentlik — idempotency)
  3. 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'tdi

Diagrammaga 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_url ga 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:

js
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:

text
tolov_boshlandi → kutilmoqda
tolov_bekor → yangi
tolov_boshlandi → kutilmoqda
tolandi → tolangan
tolangan → tolandi: mumkin emas

Oxirgi 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):

sql
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:

sql
INSERT INTO buyurtmalar (foydalanuvchi_id, summa)
VALUES (7, 1250000)
RETURNING id, holat, summa;
text
 id | holat |  summa  
----+-------+---------
  1 | yangi | 1250000
(1 row)

INSERT 0 1

Payme orqali birinchi urinish — tranzaksiya yaratilgan:

sql
INSERT INTO tranzaksiyalar
  (provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('payme', 'p-1', 1, 1250000, 'yaratilgan');
text
INSERT 0 1

Endi mijoz boshqa oynada Click orqali ham to'lamoqchi. Ikkinchi faol tranzaksiya:

sql
\set VERBOSITY terse
INSERT INTO tranzaksiyalar
  (provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('click', '4000001', 1, 1250000, 'yaratilgan');
text
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:

sql
\set VERBOSITY terse
INSERT INTO tranzaksiyalar
  (provayder, tashqi_id, buyurtma_id, summa, holat)
VALUES ('payme', 'p-1', 1, 1250000, 'bekor');
text
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:

sql
\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');
text
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-mock dasturi 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):

bash
# .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_almashtiring

8. 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):

text
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/100

Bu 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):

  1. Payme serveri sizning /tolov/payme manzilingizga CheckPerformTransaction yuboradi.
  2. Siz Stripe'dan sessiya havolasini olib, mijozni o'sha sahifaga yuborasiz.
  3. Stripe payment_intent.succeeded hodisasini sizning URL'ingizga POST qiladi.
  4. Click prepare va complete so'rovlarini sizga yuboradi.
Yechim
  1. 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
js
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:

text
{ 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
js
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:

text
yangi → kutilmoqda
kutilmoqda → tolangan
rad: tolandi
tolangan → qaytarilgan
rad: tolov_boshlandi
yakuniy: qaytarilgan

Ikkinchi 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:

text
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 fayli

Bu 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:

ts
// 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:

bash
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 UPDATE toza 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

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