Mundarija (17)
- Bu darsda
- 1. Nega bu kerak?
- 2. Asosiy obyektlar
- 3. Idempotency-Key: nega majburiy
- 4. Webhook imzosi: Stripe-Signature
- 5. Xom tana: express.raw
- 6. Hodisani qayta ishlash
- 7. stripe-mock: nima sinaldi, nima yo'q
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): sarlavhani tahlil qilish
- 3-mashq (qiyin): eng kichik birlik
- 10. Loyiha qadami: stripe/stripe.ts
- 11. Real ishda
- Xulosa
- Manbalar
Stripe: PaymentIntent, Checkout va imzoli webhook
Qisqacha: Stripe — xalqaro to'lovlar standarti va O'zbekiston provayderlaridan teskari: bu yerda siz Stripe API'ini chaqirasiz (PaymentIntent yoki Checkout Session yaratasiz), natijani esa Stripe webhook bilan xabar qiladi. Ikki qoida hammasidan muhim: har yaratuvchi so'rovga
Idempotency-Key(rasmiy kutubxona tarmoq xatosida qayta urinadi) va webhook imzosini xom tana ustida tekshirish (express.raw,express.jsonemas). Kanon rasmiystripe-mockva lokal webhook testlari bilan ishladi; haqiqiy Stripe hisobi, test kartalar vastripe listen— sinalmagan.
Bu darsda
- PaymentIntent, Checkout Session va Customer nima ekanini bilasiz.
Idempotency-Keynega majburiy ekanini tushunasiz.Stripe-Signatureimzosini Web Crypto bilan qo'lda tekshirasiz.- Xom tana, vaqt oynasi (tolerance) va takror hodisalarni kanon testi bilan ko'rasiz.
stripe-mocknimani tekshiradi va nimani tekshirmaydi — bilasiz.stripe/stripe.tsmodulini yozasiz.
Oldin bilishingiz kerak: Uzum va boshqa mahalliy to'lov tizimlari, Idempotency-Key, crypto 1: tasodif, hash va HMAC, Request body o'qish va hajm cheklovi.
Oldingi uch darsda provayder bizni chaqirdi. Stripe'da yo'nalish aralash: biz so'raymiz, Stripe esa keyin xabar beradi. Shu sababli bu dars keyingi ikki darsga — kiruvchi va chiquvchi webhook'larga — ko'prik.
Diqqat: Kanon haqiqiy Stripe hisobini ochmadi. PaymentIntent va Checkout Session Stripe'ning rasmiy stripe-mock dasturi bilan (0.206.0, Docker'da, lokal) va soxta
sk_test_…kalit bilan sinaldi. Webhook imzosi — stripe 23.0.0 paketi bilan, tarmoqsiz. Stripe hujjatiga ko'ra (2026-10-28 holati): O'zbekiston Stripe hisob ochish mumkin bo'lgan mamlakatlar ro'yxatida yo'q — hisobni ro'yxatdagi mamlakatda ro'yxatdan o'tgan kompaniya ochadi. UZS esa to'lov valyutasi sifatida ro'yxatda bor va ikki kasrli valyuta (nol kasrlilar qatorida emas):amounttiyinda yuboriladi. Haqiqiy loyihadan oldin ikkalasini ham Stripe hujjatidan qayta tekshiring.
1. Nega bu kerak?
Mahalliy provayderlar O'zbekiston kartalari uchun. Chet ellik mijoz (Visa/Mastercard, Apple Pay), xorijiy SaaS obunasi yoki xalqaro marketplace — bu Stripe va unga o'xshashlar hududi. Stripe API'si sohada namuna hisoblanadi: ko'p provayder uning tushunchalarini (intent, idempotency key, imzoli webhook) takrorlaydi. Stripe'ni tushunsangiz, boshqa xalqaro provayderni ham tez o'qiysiz.
2. Asosiy obyektlar
| Obyekt | Ma'nosi |
|---|---|
PaymentIntent (pi_…) |
"shu summani olmoqchiman" niyati; holatlar bilan to'lov oxirigacha boradi |
Checkout Session (cs_…) |
Stripe'ning tayyor to'lov sahifasi; ichida PaymentIntent yaratiladi |
Customer (cus_…) |
mijoz yozuvi; saqlangan kartalar va obunalar unga bog'lanadi |
Event (evt_…) |
webhook bilan keladigan hodisa: payment_intent.succeeded va h.k. |
Ikki yo'l:
- Checkout Session — eng oddiy. Sessiya yaratasiz, mijozni Stripe sahifasiga yuborasiz (redirect), Stripe karta, 3D Secure va xatolarni o'zi boshqaradi.
- PaymentIntent + o'z formangiz — Stripe'ning brauzer kutubxonasi (Elements) bilan to'lov formasi sizning sahifangizda. Ko'proq nazorat, ko'proq ish.
3D Secure (3DS) — bank mijozdan qo'shimcha tasdiq so'raydi (SMS kod, bank ilovasi). PaymentIntent bunda requires_action holatiga o'tadi. Stripe hujjatida buni sinash uchun maxsus test kartalar bor. Kanonda sinalmagan: stripe-mock kartalar va 3DS ni taqlid qilmaydi.
sequenceDiagram
participant M as Mijoz
participant S as Bizning server
participant ST as Stripe
M->>S: Buyurtma 42 ni to'lash
S->>ST: checkout.sessions.create (Idempotency-Key)
ST-->>S: cs_..., url
S-->>M: redirect url
M->>ST: karta, 3DS
ST-->>M: success_url ga qaytish
ST->>S: webhook payment_intent.succeeded (imzo)
S->>S: imzo, dedup, summa, 'tolangan'
S-->>ST: 200Xuddi shu oqim jadvalda — kim kimga va qaysi yo'l bilan murojaat qiladi:
| Qadam | Kim → kimga | Nima ketadi |
|---|---|---|
| 1 | mijoz → bizning server | "buyurtmani to'layman" |
| 2 | server → Stripe API | sessiya yaratish, Idempotency-Key bilan |
| 3 | server → mijoz | Stripe sahifasiga redirect |
| 4 | mijoz → Stripe | karta ma'lumoti, 3DS |
| 5 | Stripe → bizning server | imzoli webhook (hodisa) |
Bizning server karta ma'lumotini umuman ko'rmaydi (4-qadam). Pul haqidagi haqiqatni esa faqat 5-qadam olib keladi.
Diagrammadagi tartibga e'tibor bering: mijoz success_url ga qaytishi va webhook kelishi — ikki mustaqil voqea. Ulardan qaysi biri birinchi kelishi kafolatlanmagan. Buyurtmani faqat webhook o'zgartiradi (To'lov ekotizimi va umumiy oqim darsidagi qoida).
3. Idempotency-Key: nega majburiy
Kanon Stripe mijozini shunday yaratadi:
export function stripeMijoz(
kalit: string,
mock?: { host: string; port: number },
): Stripe {
return new Stripe(kalit, {
// tarmoq xatosida qayta urinadi — shuning uchun POST'larda
// Idempotency-Key MAJBURIY
maxNetworkRetries: 2,
timeout: 10_000,
...(mock
? {
host: mock.host,
port: mock.port,
protocol: "http" as const,
}
: {}),
});
}maxNetworkRetries: 2 — tarmoq xatosi yoki timeout'da rasmiy kutubxona so'rovni o'zi qayta yuboradi. Endi tasavvur qiling: birinchi paymentIntents.create Stripe'ga yetib bordi, PaymentIntent yaratildi, lekin javob yo'lda yo'qoldi. Kutubxona qayta yubordi — ikkinchi PaymentIntent. Mijoz ikkita to'lov oynasini ko'radi yoki, yomoni, ikki marta to'laydi.
Yechim — Idempotency-Key darsidagi naqsh. So'rovga noyob kalit qo'shiladi; Stripe shu kalitli takroriy so'rovga birinchi javobni qaytaradi:
/**
* Buyurtma uchun PaymentIntent: summa — eng kichik birlikda (UZS
* uchun tiyin; nol kasrli valyutalar — JPY va h.k. — boshqacha!).
*/
export async function tolovNiyati(
stripe: Stripe,
b: { id: number; summa: number; valyuta: string },
) {
return stripe.paymentIntents.create(
{
amount: b.summa,
currency: b.valyuta,
metadata: { buyurtma_id: String(b.id) },
automatic_payment_methods: { enabled: true },
},
{ idempotencyKey: `buyurtma-${String(b.id)}-pi` },
);
}Kalit — buyurtma-42-pi: buyurtmadan deterministik yasalgan. Tasodifiy UUID bo'lsa, server qayta ishga tushganda yoki foydalanuvchi tugmani ikki marta bossa yangi kalit chiqardi va himoya ishlamasdi.
Kanon testi kutubxonaning request hodisasiga quloq solib, Stripe'ga haqiqatan qaysi sarlavha ketganini yozib oldi: ["buyurtma-42-pi"]. Ya'ni kalit faqat kodda yozilgan emas — so'rovda bor.
Uchta summa qoidasi:
amount— eng kichik birlikda (sent, tiyin), butun son. Pul bilan ishlash JS'da darsidagi qoida.- Ba'zi valyutalar nol kasrli (zero-decimal): Stripe hujjatida ularning ro'yxati bor (masalan, JPY). U yerda
amount: 500— 500 iyena, 5 emas. metadata.buyurtma_id— Stripe obyektini bizning buyurtmaga bog'laydi. Webhook kelganda aynan shu bo'yicha buyurtmani topamiz.
4. Webhook imzosi: Stripe-Signature
Stripe webhook'i POST, tanasi — Event JSON'i. Uni Stripe yuborganini qanday bilamiz? Sarlavha:
Stripe-Signature: t=1793095200,v1=4d93498e29ea0b4bd564b000ab4e7cec2ea9412787d796d073ce52d7d4cb3909t— imzo vaqti (unix soniya).v1— HMAC-SHA256: kalit — webhook siri (whsec_…), matn —"<t>.<xom tana>", natija — hex.
Kalitsiz imzo yasab bo'lmaydi, vaqt esa eski so'rovni qayta yuborishga (replay) qarshi. Stripe hujjatiga ko'ra rasmiy kutubxonalarning sukut vaqt oynasi (tolerance) — 5 daqiqa, ya'ni 300 soniya. Bu sarlavhani biz stripe 23.0.0 ning generateTestHeaderString funksiyasi bilan oldik (biz ishga tushirdik). Endi xuddi shu imzoni paketsiz, brauzerdagi Web Crypto bilan tekshiramiz:
const enc = new TextEncoder();
async function hmacHex(sir, matn) {
const kalit = await crypto.subtle.importKey(
"raw", enc.encode(sir), { name: "HMAC", hash: "SHA-256" },
false, ["sign"],
);
const baytlar = enc.encode(matn);
const imzo = await crypto.subtle.sign("HMAC", kalit, baytlar);
return [...new Uint8Array(imzo)]
.map((b) => b.toString(16).padStart(2, "0")).join("");
}
async function tekshir(tana, sarlavha, sir, hozir, tolerans = 300) {
const qism = Object.fromEntries(
sarlavha.split(",").map((x) => x.split("=")),
);
if (!qism.t || !qism.v1) return "sarlavha buzuq";
const t = Number(qism.t);
if (Math.abs(hozir - t) > tolerans) return "tolerance tashqarisida";
const kutilgan = await hmacHex(sir, `${t}.${tana}`);
return kutilgan === qism.v1 ? "ok" : "imzo mos emas";
}
(async () => {
const sir = "whsec_SOXTA_lokal";
const tana = '{"id":"evt_1","type":"payment_intent.succeeded"}';
const t = 1793095200;
const sarlavha = `t=${t},v1=${await hmacHex(sir, `${t}.${tana}`)}`;
console.log(sarlavha);
console.log(await tekshir(tana, sarlavha, sir, t + 10));
console.log(await tekshir(tana, sarlavha, sir, t + 301));
const chiroyli = JSON.stringify(JSON.parse(tana), null, 2);
console.log(await tekshir(chiroyli, sarlavha, sir, t + 10));
console.log(await tekshir(tana, sarlavha, "whsec_boshqa", t + 10));
})();Konsolda birinchi qatorga (sarlavha) va keyingi to'rtta tekshiruv natijasiga qarang:
t=1793095200,v1=4d93498e29ea0b4bd564b000ab4e7cec2ea9412787d796d073ce52d7d4cb3909
ok
tolerance tashqarisida
imzo mos emas
imzo mos emasBirinchi qator stripe paketi yasagan sarlavha bilan belgisigacha bir xil — demak formula to'g'ri. Qolgan to'rttasi:
- To'g'ri imzo, 10 soniya ichida —
ok. - 301 soniyadan keyin — vaqt oynasidan tashqarida. Stripe paketining sukut chegarasi — 300 soniya.
- Bir xil ma'lumot, boshqa formatlash (
null, 2— chiroyli JSON) — imzo mos emas. Bu darsning eng muhim qatori. - Boshqa sir — mos emas.
Stripe sirni (whsec_…) butun satr sifatida kalit qiladi — biz ham enc.encode(sir). Keyingi darsdagi Standard Webhooks formati esa whsec_ dan keyingi qismni base64'dan baytlarga o'giradi. Bir xil ko'rinadigan sir, ikki xil qoida — har formatni hujjatdan o'qing.
Haqiqiy kodda satrlarni === bilan emas, doimiy vaqtli solishtirish bilan tekshiring (Node'da timingSafeEqual). Stripe paketi buni o'zi qiladi.
5. Xom tana: express.raw
Uchinchi tekshiruv (chiroyli JSON) amalda qanday xato beradi? Express'da odatiy sozlama — app.use(express.json()). U tanani o'qiydi, JSON.parse qiladi va req.body ga obyekt qo'yadi. Imzoni tekshirish uchun esa sizda faqat obyekt bor — uni qayta JSON.stringify qilsangiz, bo'shliqlar, kalitlar tartibi yoki unicode ekranlash Stripe yuborgan baytlardan farq qilishi mumkin. Imzo mos kelmaydi va har bir haqiqiy webhook rad etiladi.
Kanon webhook marshrutiga faqat o'zining express.raw ini qo'yadi — req.body Buffer bo'lib qoladi:
r.post(
"/",
express.raw({ type: "application/json", limit: "1mb" }),
async (req, res) => {
hodisa = s.stripe.webhooks.constructEvent(
req.body as Buffer,
req.get("stripe-signature") ?? "",
s.sir,
300,
);
// ...Diqqat: Agar ilovada
app.use(express.json())global ulangan bo'lsa va webhook marshruti undan keyin turgan bo'lsa,express.json()tanani birinchi o'qib bo'ladi. Webhook marshrutini global JSON middleware'dan oldin ulang yoki JSON'ni faqat kerakli router'larga qo'ying (Middleware va request lifecycle).
Kanon testi xuddi shu tuzoqni tekshirdi: to'g'ri imzo, lekin tana JSON.stringify(JSON.parse(t), null, 2) bilan qayta formatlangan — HTTP 400.
6. Hodisani qayta ishlash
Imzo to'g'ri bo'lsa, kanon hodisani bitta ma'lumotlar bazasi tranzaksiyasida ishlaydi:
INSERT INTO kiruvchi_hodisalar … ON CONFLICT DO NOTHING—(provayder, hodisa_id)noyob. Qator qo'shilmasa — bu hodisa avval kelgan: javob"takror", boshqa hech narsa qilinmaydi.payment_intent.succeededbo'lsa —metadata.buyurtma_idbo'yicha buyurtmaFOR UPDATEbilan olinadi.- Summa tekshiruvi:
amount_receivedbuyurtma summasiga teng bo'lmasa —"summa-mos-emas", buyurtma to'lanmaydi. Pul olingan, lekin nimadir noto'g'ri — bu qo'lda ko'rib chiqiladi (solishtirish — reconciliation; Refund, reconciliation va moliyaviy hisob darsida). - Tranzaksiya
bajarilgan, buyurtmatolangan, ledger yozuvi. - Boshqa hodisa turlari —
"e'tiborsiz", lekin baribir saqlanadi (keyin kerak bo'lishi mumkin).
Javob — doim tez 200. Faqat bizning ichki xatomizda (baza uzildi) — 500: Stripe hodisani keyinroq qayta yuboradi (Stripe hujjatiga ko'ra — jonli rejimda 3 kungacha, eksponensial kutish bilan). Imzo xato bo'lsa — 400.
Kanon testi takrorni shunday tekshiradi: bir xil hodisa ikki marta yuboriladi — birinchisiga { qabul: true, natija: "tolandi" }, ikkinchisiga { qabul: true, natija: "takror" }.
Tekshirib ko'ring: Nega takror hodisaga 400 yoki 409 emas, 200 qaytaramiz?
Javob
2xx bo'lmagan javobni Stripe "yetkazilmadi" deb tushunadi va qayta yuboraveradi. Takror hodisa — xato emas, biz uni allaqachon ishlaganmiz. 200 — "qabul qildim, boshqa yubormang".
7. stripe-mock: nima sinaldi, nima yo'q
Stripe'ning rasmiy stripe-mock dasturi Stripe API'ning OpenAPI sxemasiga mos javob qaytaradi. Kanon u bilan (127.0.0.1, Docker, image 2026-10-01) PaymentIntent va Checkout Session yaratdi va testda tekshirdi: pi.id pi_ bilan boshlanadi, amount va currency qaytadi, sessiya cs_ bilan.
Kanon topgan farqlar — lokal soxta server (simulyator) haqiqiy Stripe emasligini ko'rsatadi:
| Holat | stripe-mock | Haqiqiy Stripe (hujjat bo'yicha) |
|---|---|---|
sk_test_ + pastki chiziqli kalit |
rad: "valid looking" emas | haqiqiy kalit formati |
amount: -1 |
qabul qildi | minimal summa talab qilinadi |
| noma'lum parametr | StripeInvalidRequestError |
xuddi shunday |
| to'lov holati | holatsiz — to'lov o'tmaydi | holatlar bo'yicha o'tadi |
| webhook | yubormaydi | yuboradi |
| test kartalar, 3DS | yo'q | bor |
Saboq: simulyator shaklni (so'rov va javob formatini) tekshiradi, biznes qoidalarini emas. Manfiy summani o'z kodingizda tekshiring — simulyator sizni ogohlantirmaydi.
Webhook'ni lokal sinash uchun Stripe CLI'da stripe listen buyrug'i bor: u haqiqiy hisobdagi hodisalarni sizning localhost manzilingizga uzatadi. Kanon uni sinamagan (hisob kerak). Kanon webhook testlarida hodisani o'zi yasadi va generateTestHeaderString bilan imzoladi — tarmoqsiz, hisobsiz.
8. Ko'p uchraydigan xatolar
| Xato | Oqibati | To'g'risi |
|---|---|---|
Idempotency-Key siz create |
qayta urinishda ikki PaymentIntent | buyurtmadan deterministik kalit |
| kalit — tasodifiy UUID | qayta bosishda himoya yo'q | buyurtma-<id>-pi |
express.json() webhook'da |
har imzo mos emas | express.raw |
success_url da buyurtmani to'lash |
to'lovsiz buyurtma | faqat webhook |
| hodisa id'sini saqlamaslik | takror — ikki marta ishlov | kiruvchi_hodisalar + ON CONFLICT |
| summani tekshirmaslik | boshqa summa — "to'landi" | amount_received = buyurtma summasi |
| ichki xatoda 200 | Stripe qayta yubormaydi, hodisa yo'qoladi | 500 — qayta yuborilsin |
| simulyatorga ishonish | manfiy summa o'tdi | biznes qoidalari o'z kodingizda |
9. Mashqlar
Mashqlar kurs/mashqlar/29/32-stripe/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
success_urlga qaytgan mijozning buyurtmasini "to'langan" qilsa bo'ladi.- Webhook imzosi qayta formatlangan JSON bilan ham mos keladi, chunki ma'lumot bir xil.
- Stripe'ga 12.50 dollar uchun
amount: 1250yuboriladi. - stripe-mock manfiy summani rad etadi.
- Takror webhook hodisasiga 200 qaytarish kerak.
Yechim
- Noto'g'ri — faqat imzoli webhook. 2. Noto'g'ri — imzo baytlar ustida; kanon testida 400. 3. To'g'ri — sentda. 4. Noto'g'ri — kanonda qabul qildi. 5. To'g'ri — aks holda Stripe qayta yuboradi.
2-mashq (o'rta): sarlavhani tahlil qilish
Stripe sirni almashtirish (rotatsiya) paytida sarlavhada bir nechta v1 yuborishi mumkin: t=…,v1=aaa,v1=bbb. sarlavhaniOch(s) yozing: { t, v1: [...] } qaytarsin (Object.fromEntries oxirgi v1 ni qoldirib, qolganini yo'qotadi — undan foydalanmang). Noma'lum kalitlar (v0) e'tiborsiz. Uch holatni sinang.
Yechim
function sarlavhaniOch(s) {
const natija = { t: undefined, v1: [] };
for (const qism of s.split(",")) {
const i = qism.indexOf("=");
if (i < 0) continue;
const [k, v] = [qism.slice(0, i).trim(), qism.slice(i + 1)];
if (k === "t") natija.t = Number(v);
else if (k === "v1") natija.v1.push(v);
}
return natija;
}
console.log(sarlavhaniOch("t=1793095200,v1=aaa"));
console.log(sarlavhaniOch("t=1793095200,v1=aaa,v1=bbb,v0=eski"));
console.log(sarlavhaniOch("buzuq"));Konsolda:
{ t: 1793095200, v1: [ 'aaa' ] }
{ t: 1793095200, v1: [ 'aaa', 'bbb' ] }
{ t: undefined, v1: [] }Tekshiruvda imzo v1 ro'yxatidagi istalgan biri bilan mos kelsa — ok. Darsdagi ixcham tekshir faqat bittasini oladi; Stripe paketi va keyingi darsdagi kanon imzo.ts hammasini tekshiradi.
3-mashq (qiyin): eng kichik birlik
engKichikBirlik(somSatr, valyuta) yozing: "12.50" va "usd" → 1250; nol kasrli valyutalar (NOL_KASR = ["jpy", "krw"] — mashq uchun qisqa ro'yxat; to'liq ro'yxat Stripe hujjatida) uchun kasr bo'lsa — xato, bo'lmasa son o'zi. Float ishlatmang (Pul bilan ishlash JS'da). To'rt holatni sinang: 12.50 usd, 500 jpy, 500.5 jpy, 0.1 usd.
Yechim
const NOL_KASR = ["jpy", "krw"];
function engKichikBirlik(s, valyuta) {
const m = /^(\d+)(?:\.(\d{1,2}))?$/.exec(s);
if (!m) throw new Error(`noto'g'ri summa: ${s}`);
const [, butun, kasr = ""] = m;
if (NOL_KASR.includes(valyuta)) {
if (kasr) throw new Error(`${valyuta} kasrsiz: ${s}`);
return Number(butun);
}
return Number(butun) * 100 + Number(kasr.padEnd(2, "0"));
}
for (const [s, v] of [["12.50", "usd"], ["500", "jpy"],
["500.5", "jpy"], ["0.1", "usd"]]) {
try {
console.log(s, v, engKichikBirlik(s, v));
} catch (e) {
console.log(e.message);
}
}Konsolda:
12.50 usd 1250
500 jpy 500
jpy kasrsiz: 500.5
0.1 usd 100.1 usd → 10 sent (1 emas) — padEnd bilan. UZS Stripe hujjatida ikki kasrli valyuta, ya'ni "12.50" so'm → 1250 tiyin (usd bilan bir xil yo'l). Kanon kodi tiyinda yuboradi; bu stripe-mock'da sinalgan, haqiqiy Stripe'da emas.
10. Loyiha qadami: stripe/stripe.ts
To'liq modul (formatlangan nusxa, ma'nosi kanondagidek):
// stripe/stripe.ts — Stripe: PaymentIntent (idempotency key bilan),
// Checkout Session, webhook (imzo `Stripe-Signature`, XOM tana,
// tolerance, hodisa dedup).
// Lokal: rasmiy `stripe/stripe-mock` image (soxta `sk_test_…` — har
// qanday kalitni qabul qiladi, holatsiz: to'lov o'tmaydi,
// kartalar/3DS/webhook yubormaydi).
// Haqiqiy Stripe hisobi, test kartalar, `stripe listen` (CLI) —
// SINALMAGAN.
import express, { type Router } from "express";
import Stripe from "stripe";
import type { Pool } from "pg";
import { tranzaksiya, tolovYozuvi } from "../db.ts";
export function stripeMijoz(
kalit: string,
mock?: { host: string; port: number },
): Stripe {
return new Stripe(kalit, {
// tarmoq xatosida qayta urinadi — shuning uchun POST'larda
// Idempotency-Key MAJBURIY
maxNetworkRetries: 2,
timeout: 10_000,
...(mock
? {
host: mock.host,
port: mock.port,
protocol: "http" as const,
}
: {}),
});
}
/**
* Buyurtma uchun PaymentIntent: summa — eng kichik birlikda (UZS
* uchun tiyin; nol kasrli valyutalar — JPY va h.k. — boshqacha!).
*/
export async function tolovNiyati(
stripe: Stripe,
b: { id: number; summa: number; valyuta: string },
) {
return stripe.paymentIntents.create(
{
amount: b.summa,
currency: b.valyuta,
metadata: { buyurtma_id: String(b.id) },
automatic_payment_methods: { enabled: true },
},
{ idempotencyKey: `buyurtma-${String(b.id)}-pi` },
);
}
/**
* Webhook: `express.raw` — imzo XOM baytlar ustida hisoblanadi;
* `express.json()` dan keyin qayta serializatsiya qilingan tana
* imzoga mos kelmaydi.
* Tez 200 qaytaramiz; hodisa `kiruvchi_hodisalar` ga (event.id noyob)
* — takror yetkazilsa ikkinchi marta ishlanmaydi.
*/
export function stripeWebhook(s: {
pool: Pool;
stripe: Stripe;
sir: string;
toleransSoniya?: number;
log?: (e: unknown) => void;
}): Router {
const r = express.Router();
r.post(
"/",
express.raw({ type: "application/json", limit: "1mb" }),
async (req, res) => {
let hodisa: Stripe.Event;
try {
hodisa = s.stripe.webhooks.constructEvent(
req.body as Buffer,
req.get("stripe-signature") ?? "",
s.sir,
s.toleransSoniya ?? 300,
);
} catch (e) {
res
.status(400)
.json({
xato:
e instanceof Error
? e.message.split("\n")[0]
: "imzo",
});
return;
}
try {
const natija = await tranzaksiya(s.pool, async (c) => {
const yangi = await c.query(
"INSERT INTO kiruvchi_hodisalar (provayder, hodisa_id, " +
"tur, tana) VALUES ('stripe', $1, $2, $3) ON " +
"CONFLICT DO NOTHING",
[hodisa.id, hodisa.type, hodisa],
);
if (yangi.rowCount === 0) return "takror";
if (hodisa.type === "payment_intent.succeeded") {
const pi = hodisa.data.object;
const bid = pi.metadata["buyurtma_id"];
const b = (
await c.query<{
id: string;
summa: string;
holat: string;
}>(
"SELECT id, summa, holat FROM buyurtmalar WHERE id " +
"= $1 FOR UPDATE",
[bid],
)
).rows[0];
if (!b) return "buyurtma-yoq";
if (Number(b.summa) !== pi.amount_received)
// pul olingan, lekin summa boshqa — qo'lda ko'rib
// chiqish (reconciliation)
return "summa-mos-emas";
if (b.holat === "tolangan") return "allaqachon";
const t = await c.query<{ id: string }>(
"INSERT INTO tranzaksiyalar (provayder, tashqi_id, " +
"buyurtma_id, summa, holat, bajarilgan_vaqt) " +
"VALUES ('stripe', $1, $2, $3, 'bajarilgan', " +
"now()) ON CONFLICT (provayder, tashqi_id) DO " +
"NOTHING RETURNING id",
[pi.id, b.id, pi.amount_received],
);
const tid = t.rows[0]?.id;
if (tid === undefined) return "allaqachon";
await c.query(
"UPDATE buyurtmalar SET holat = 'tolangan', " +
"yangilangan_vaqt = now() WHERE id = $1",
[b.id],
);
await tolovYozuvi(
c,
"stripe",
pi.amount_received,
Number(tid),
);
await c.query(
"UPDATE kiruvchi_hodisalar SET ishlangan_vaqt = " +
"now() WHERE provayder = 'stripe' AND hodisa_id = " +
"$1",
[hodisa.id],
);
return "tolandi";
}
return "e'tiborsiz";
});
res.status(200).json({ qabul: true, natija });
} catch (e) {
(s.log ?? console.error)(e);
// Stripe qayta yuboradi (hujjat: 3 kungacha eksponensial)
res.status(500).json({ qabul: false });
}
},
);
return r;
}Testlar — tolov/test/stripe.test.ts, to'rtta. Birinchisi — stripe-mock bilan PaymentIntent va Checkout Session (STRIPE_MOCK muhit o'zgaruvchisi bo'lsa). Qolgan uchtasi webhook'niki: to'g'ri imzo va takror, imzo xatolari (boshqa sir, o'zgartirilgan tana, qayta formatlangan JSON, 301 soniya eski — xatoda "tolerance" so'zi, bo'sh sarlavha), summa mos emas. Docker'siz biz uchtasini ishga tushirdik (Node 24.21.0, Postgres 17.6; stripe-mock testi o'tkazib yuborildi — ﹣ belgisi). Birinchi qatordagi ﹣ va uchta ✔ ga qarang:
﹣ stripe-mock bilan PaymentIntent (1.2756ms) # STRIPE_MOCK yo'q
▶ Stripe webhook (imzo, takror, xom tana)
✔ to'g'ri imzo → to'landi; xuddi o'sha hodisa yana → takror (dedup) (79.8626ms)
✔ imzo xatolari: boshqa sir, o'zgartirilgan tana, qayta formatlangan JSON, eski vaqt (> 300 s) (23.4193ms)
✔ summa mos emas — buyurtma to'lanmaydi (qo'lda ko'rish) (7.6143ms)
✔ Stripe webhook (imzo, takror, xom tana) (1164.4241ms)
ℹ tests 3
ℹ suites 2
ℹ pass 3
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2189.1362Nima bo'ldi? Uchala webhook testi o'tdi: takror hodisa ikkinchi marta ishlanmadi, buzilgan imzolar rad etildi, boshqa summa buyurtmani to'lamadi. skipped 0 ga aldanmang: o'tkazilgan test ﹣ belgisi bilan birinchi qatorda turibdi (node:test uni shunday sanaydi).
Kanonda stripe-mock bilan to'rttasi ham o'tgan (tolov jami 33/33).
Commit xabari:
M='feat(tolov): Stripe — PaymentIntent idempotency, Checkout,'
M+=' xom tanali webhook, dedup'
git commit -m "$M"11. Real ishda
- Stripe Dashboard webhook sahifasida har yetkazishning statusi va qayta yuborish tugmasi bor. Imzo xatolari ko'paysa — birinchi gumon:
express.json()yoki proxy tanani o'zgartiryapti. - Hodisalar tartibi kafolatlanmagan:
payment_intent.succeededpayment_intent.createddan oldin kelishi mumkin. Kod hodisa obyektiga, tartibga emas, tayanishi kerak (Kiruvchi webhook'larni qabul qilish). - API versiyasi — hisob va webhook endpoint'ida qotiriladi; Event JSON'i shu versiya bo'yicha keladi. Yangilashni rejalashtiring.
- Intervyuda: "Nega webhook xom tana talab qiladi?", "Idempotency key qanday tanlanadi?"
Xulosa
- Stripe'da biz API'ni chaqiramiz (PaymentIntent, Checkout Session), natija webhook bilan keladi.
- Rasmiy kutubxona qayta urinadi — har create'da buyurtmadan yasalgan
Idempotency-Key. Stripe-Signature:tvav1 = HMAC-SHA256(whsec, "t.tana"), vaqt oynasi 300 soniya; qayta formatlangan JSON — imzo mos emas.- Webhook marshruti —
express.raw; hodisakiruvchi_hodisalarda dedup, summa tekshiriladi, takrorga 200, ichki xatoga 500. - stripe-mock shaklni tekshiradi, biznes qoidalarini emas (manfiy summa o'tdi); haqiqiy Stripe, test kartalar, 3DS,
stripe listen— sinalmagan.
Keyingi dars: Obuna va davriy to'lovlar — sinov davri, dunning, proratsiya va huquq (entitlement) holat mashinasi.
Manbalar
- Stripe PaymentIntents API: https://docs.stripe.com/api/payment_intents
- Stripe Checkout: https://docs.stripe.com/payments/checkout
- Stripe idempotent so'rovlar: https://docs.stripe.com/api/idempotent_requests
- Stripe webhook imzosi, 5 daqiqalik tolerance, 3 kunlik qayta yuborish: https://docs.stripe.com/webhooks (2026-10-28 da o'qildi)
- Stripe valyutalari (UZS — ikki kasrli): https://docs.stripe.com/currencies (2026-10-28)
- Stripe mavjud mamlakatlar: https://stripe.com/global (2026-10-28; O'zbekiston ro'yxatda yo'q)
- Stripe nol kasrli valyutalar: https://docs.stripe.com/currencies#zero-decimal
- stripe-mock: https://github.com/stripe/stripe-mock
- Stripe CLI
stripe listen: https://docs.stripe.com/stripe-cli — sinalmagan - Kanon (IlmHamroh, 2026-10-27): stripe 23.0.0, stripe-mock 0.206.0, express 5.3.0; haqiqiy Stripe hisobi — sinalmagan. Webhook imzosi va testlar — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!