8.29-bob: Subscription va davriy to'lov (recurring billing)
8-QISM — NestJS (chuqur) · 29-mavzu · Amaliy real mavzu
1. Kirish va motivatsiya
Payment 8.19-bob da bir martalik to'lovni ko'rdik; multi-tenancy 8.25-bob da SaaS arxitekturasini. Endi ikkalasini bog'laydigan, SaaS biznesining moliyaviy yuragi — obuna va davriy to'lov (subscription, recurring billing) — ni o'rganamiz. SaaS, kurs platforma, premium ilova, streaming — barchasi takrorlanuvchi daromad (oylik/yillik to'lov) modeliga asoslanadi. Bu — bir martalik sotuvdan 8.19-bob farqli: mijoz bir marta to'lamaydi, muntazam (har oy) to'laydi, va siz xizmatni davom ettirasiz. Bu model — eng barqaror, eng qadrli biznes (takrorlanuvchi daromad).
Davriy to'lov bir martalik to'lovdan murakkabroq: rejalar (bepul/pro/enterprise — narx farqi), trial (sinov muddati — bepul boshlash), davriy yechish (har oy avtomatik — saqlangan karta), upgrade/downgrade (reja o'zgartirish — proration), bekor qilish (muddat oxirigacha amal), muddati o'tgan to'lov (dunning — qayta urinish), xizmatni cheklash (to'lamasa — bloklash). Har bir holat to'g'ri ishlashi kerak (mijoz pulini, sizning daromadingizni belgilaydi). Bu — payment'ning eng murakkab, lekin eng qadrli qismi.
Bu bob: subscription modeli (bir martalik vs davriy), rejalar (plan/tier), trial, subscription holatlari (state machine), davriy yechish (avtomatik — saqlangan to'lov usuli), upgrade/downgrade (proration), bekor qilish (muddat oxiri), dunning (muddati o'tgan — qayta urinish), webhook (to'lov tizimidan — 8.20), va xizmatni cheklash (entitlement). Bu bob 8.19 (payment), 8.20 (webhook), 8.25 (multi-tenant), 8.18 (cron) bilan bog'liq. Subscription — barqaror SaaS daromadining asosi.
O'xshatish: subscription — sport zali abonementi (bir martalik chipta — 8.19 — bir kelish; abonement — muntazam). Mijoz reja tanlaydi (oddiy/VIP — narx farqi), ba'zan bepul sinov (trial — bir hafta). Keyin har oy avtomatik to'lov (saqlangan karta — qayta-qayta so'rashsiz). Mijoz rejani o'zgartirishi mumkin (VIP'ga — qo'shimcha to'lov; oddiyga — keyingi oydan). Bekor qilsa — oy oxirigacha kiradi (to'lagani uchun). To'lov o'tmasa (karta muddati) — eslatma, qayta urinish; bo'lmasa — abonement to'xtaydi (kirish bloklanadi). Subscription kodi — shu abonement mantig'ining dasturlashtirilgani.
Nega muhim?
- Barqaror daromad — takrorlanuvchi (eng qadrli biznes modeli).
- SaaS yuragi — multi-tenancy 8.25-bob + obuna = to'liq SaaS.
- Murakkab mantiq — trial, proration, dunning (to'g'ri ishlashi kerak).
- Yuqori qadr — billing biladigan dasturchi kam, qimmat.
2. Nazariya — chuqur tushuntirish
2.1. Bir martalik vs davriy to'lov
BIR MARTALIK 8.19-bob: DAVRIY (subscription):
Mijoz bir marta to'laydi Mijoz MUNTAZAM to'laydi (har oy)
Mahsulot/buyurtma Xizmat davomiyligi (premium, SaaS)
Oddiy oqim Murakkab (trial, yangilash, bekor)
Daromad — bir martalik Daromad — takrorlanuvchi (MRR)
Davriy — saqlangan to'lov usuli (har oy avtomatik — qayta so'ramasdan)
MRR (Monthly Recurring Revenue) — SaaS asosiy ko'rsatkichiBir martalik vs davriy: bir martalik 8.19-bob — mijoz bir marta to'laydi (mahsulot); davriy — muntazam (har oy — xizmat davomiyligi — premium/SaaS). Davriy murakkabroq (trial, yangilash, bekor). Takrorlanuvchi daromad (MRR — Monthly Recurring Revenue) — SaaS asosiy ko'rsatkichi (barqaror, prognoz qilinadigan). Saqlangan to'lov usuli (har oy avtomatik — qayta so'ramasdan — 2.5). Bu — eng qadrli model.
2.2. Rejalar (plan/tier)
REJALAR (tier — narx/imkoniyat farqi):
┌────────────┬──────────┬─────────────────────────────────┐
│ Free │ 0 so'm │ cheklangan (5 loyiha, reklama) │
│ Pro │ 99k/oy │ kengaytirilgan (cheksiz, support)│
│ Enterprise │ kelishuv │ to'liq (SLA, alohida DB — 8.25) │
└────────────┴──────────┴─────────────────────────────────┘
Har reja:
- Narx (oylik/yillik — yillik arzonroq)
- Imkoniyatlar (entitlement — nima mumkin — 2.9)
- Cheklovlar (limit — 5 loyiha, 10GB)
Reja = narx + imkoniyat to'plami (mijoz tanlaydi)Rejalar (plan/tier): narx va imkoniyat farqi (Free — cheklangan; Pro — kengaytirilgan; Enterprise — to'liq). Har reja: narx (oylik/yillik — yillik arzon — oldindan to'lov), imkoniyatlar (entitlement — nima mumkin — 2.9), cheklovlar (limit — 5 loyiha, 10GB). Multi-tenant'da 8.25-bob — tenant rejasi. Reja dizayni — biznes qarori (narxlash strategiyasi). Kod: reja ruxsat/limit 2.9-bob. Reja — subscription'ning asosi.
2.3. Trial (bepul sinov)
TRIAL — bepul sinov muddati (mijozni jalb qilish):
- Card-required: karta kerak (trial oxirida avtomatik to'lov)
- Card-free: kartasiz (trial oxirida to'lov so'raladi — konversiya kam)
Oqim:
1. Ro'yxat trial boshlanadi (14 kun — state: trialing)
2. Trial davomida to'liq imkoniyat (mijoz sinaydi)
3. Trial tugashidan oldin eslatma (email — 8.10)
4. Trial oxiri to'lov (active) yoki bekor (cancelled)
Trial tugashi cron bilan kuzatiladi (8.18)Trial (bepul sinov — mijozni jalb qilish): card-required (karta kerak — trial oxirida avtomatik to'lov — yuqori konversiya); card-free (kartasiz — konversiya kam, lekin ko'p ro'yxat). Oqim: ro'yxat trial (14 kun —
trialing) eslatma (tugashidan oldin — 8.10) to'lov (active) yoki bekor. Trial tugashi cron bilan kuzatiladi (8.18 — har kun tekshirish). Trial — SaaS o'sishining muhim vositasi (sinab ko'rish sotib olish).
2.4. Subscription holatlari (state machine)
SUBSCRIPTION HOLATLARI (state machine — 8.19: 2.6):
trialing (sinov) ── active (faol, to'langan)
╲ │
╲ ├── past_due (to'lov o'tmadi — dunning 2.7)
╲ │ ├── active (qayta to'landi)
╲ │ └── cancelled (urinishlar tugadi)
╲ ├── cancelled (mijoz bekor qildi — 2.6)
╲ └── expired (muddat tugadi)
╲ cancelled
Holat xizmat (active/trialing — kirish; cancelled/expired — bloklash 2.9)Subscription state machine (8.19: 2.6 kabi):
trialing(sinov)active(faol)past_due(to'lov o'tmadi — dunning — 2.7)active(qayta) yokicancelled;activecancelled(mijoz bekor — 2.6) /expired. Holat xizmatni belgilaydi (active/trialing — kirish ochiq; cancelled/expired — bloklash — 2.9). Holat o'tishlari nazorat qilinadi (noto'g'ri o'tish — xato). To'g'ri state machine — ishonchli billing.
2.5. Davriy yechish (avtomatik — saqlangan usul)
DAVRIY YECHISH — har oy avtomatik (qayta so'ramasdan):
USUL 1 — To'lov tizimi boshqaradi (Stripe Subscriptions — tavsiya):
- Stripe rejani biladi har oy avtomatik yechadi webhook yuboradi
- Siz faqat webhook'ni ishlaysiz 8.20-bob — eng oson, ishonchli
USUL 2 — O'zingiz boshqarasiz (mahalliy — Payme/Click):
- Saqlangan karta token cron 8.18-bob har oy yechish
- Holat, qayta urinish — o'zingiz (murakkabroq)
Iloji bo'lsa — to'lov tizimi boshqaradi (Stripe Billing)Davriy yechish (avtomatik): Usul 1 (to'lov tizimi boshqaradi — Stripe Subscriptions — reja Stripe'da, har oy avtomatik yechadi, webhook yuboradi — siz faqat webhook'ni ishlaysiz — 8.20 — eng oson/ishonchli); Usul 2 (o'zingiz — mahalliy Payme/Click — saqlangan karta token + cron har oy — 8.18 — murakkabroq: holat, qayta urinish o'zingiz). Iloji bo'lsa — to'lov tizimi boshqarsin (Stripe Billing — murakkablikni o'z zimmasiga oladi). Mahalliy — token + cron.
2.6. Upgrade/downgrade va bekor qilish
UPGRADE (Pro Enterprise) — darrov (qo'shimcha imkoniyat):
- Proration: qolgan kun uchun farq hisoblanadi (adolatli)
- Darrov yangi imkoniyat 2.9-bob
DOWNGRADE (Pro Free) — keyingi davrdan (to'lagani amal qiladi):
- Joriy davr oxirigacha Pro, keyin Free
BEKOR QILISH (cancel):
- Cancel at period end: muddat oxirigacha xizmat (to'lagani uchun) — tavsiya
- Immediate: darrov (refund bilan — kamroq)
Mijoz to'lagani uchun xizmat oladi (adolat) — darrov kesish tavsiya etilmaydiUpgrade/downgrade/bekor: upgrade (yuqoriga — darrov + proration — qolgan kun uchun farq adolatli hisoblanadi); downgrade (pastga — keyingi davrdan — to'lagani amal qiladi); bekor (
cancel at period end— muddat oxirigacha xizmat — to'lagani uchun — adolatli/tavsiya; yoki immediate — refund bilan). Mijoz to'lagani uchun xizmat oladi (darrov kesib tashlamaslik — adolat, ishonch). Proration — billing'ning nozik qismi.
2.7. Dunning (muddati o'tgan to'lov)
DUNNING — to'lov o'tmaganda qayta urinish (karta muddati, mablag' yo'q):
1. To'lov o'tmadi state: past_due 2.4-bob
2. Mijozga eslatma (email — "kartangizni yangilang" — 8.10)
3. Qayta urinish (3 kun, 5 kun, 7 kun — exponential — 8.20)
4. Urinishlar tugadi cancelled + xizmat bloklash (2.9)
Darrov bekor qilmaslik kerak (vaqtinchalik muammo — karta, mablag')
Dunning = mijozni saqlab qolish (revenue recovery)Dunning (muddati o'tgan to'lov — revenue recovery): to'lov o'tmasa (karta muddati, mablag' yo'q)
past_dueeslatma (email — "kartangizni yangilang" — 8.10) qayta urinish (3/5/7 kun — 8.20 retry) tugasacancelled+ bloklash. Darrov bekor qilmaslik kerak (vaqtinchalik muammo bo'lishi mumkin — karta yangilanadi). Dunning — yo'qotilgan daromadni tiklash (mijozni saqlab qolish). Stripe Billing dunning'ni avtomatik 2.5-bob. Muhim moliyaviy jarayon.
2.8. Webhook (to'lov tizimidan — 8.20)
// Subscription hodisalari to'lov tizimidan webhook (8.20)
@Post("billing/webhook")
@HttpCode(200)
async webhook(@Req() req: RawBodyRequest<Request>, @Headers("stripe-signature") sig: string) {
const event = this.verify(req.rawBody, sig); // imzo (8.20: 2.4)
switch (event.type) {
case "invoice.paid": // davriy to'lov o'tdi
await this.subService.davrYangilandi(event.data.object); break;
case "invoice.payment_failed": // to'lov o'tmadi dunning (2.7)
await this.subService.pastDue(event.data.object); break;
case "customer.subscription.deleted": // bekor
await this.subService.bekorQilindi(event.data.object); break;
}
return { received: true };
}Webhook 8.20-bob: subscription hodisalari to'lov tizimidan webhook orqali keladi:
invoice.paid(davriy to'lov o'tdi davr yangilandi),invoice.payment_failed(o'tmadi dunning — 2.7),subscription.deleted(bekor),subscription.updated(reja o'zgardi). Imzo (8.20: 2.4) + idempotency (8.20: 2.5). Stripe Billing'da subscription holatini webhook boshqaradi (siz tinglaysiz). Bu — davriy to'lovning asosiy aloqasi (to'lov tizimi sizning DB).
Muhim webhook hodisalari — to'liq ro'yxat. Subscription hayotiy sikli davomida to'lov tizimi bir nechta hodisa yuboradi, va har biri lokal DB'da aniq bir o'zgarishga mos keladi. To'g'ri billing tizimi shu hodisalarni to'liq qamrab olishi kerak — aks holda DB va to'lov tizimi orasida nomuvofiqlik (drift) paydo bo'ladi. Asosiy hodisalar:
HODISA LOKAL DB O'ZGARISHI
───────────────────────────────── ──────────────────────────────────
checkout.session.completed Checkout tugadi subscription yaratildi
(stripeSubId'ni bog'lash — 2.11)
customer.subscription.created Subscription boshlandi (trialing/active)
customer.subscription.updated Reja/holat o'zgardi (upgrade, past_due
active, cancel_at_period_end o'rnatildi)
customer.subscription.deleted Butunlay bekor holat = cancelled
invoice.paid Davriy to'lov o'tdi davrOxiri yangilandi
invoice.payment_failed To'lov o'tmadi past_due (dunning — 2.7)
invoice.upcoming Keyingi to'lov yaqin (eslatma — 8.10)
customer.subscription.trial_will_end Trial 3 kun qoldi (eslatma — 2.3)
HAR hodisa idempotent bo'lishi kerak (8.20: 2.5) — Stripe bir hodisani
bir necha marta yuborishi mumkin (at-least-once yetkazish)
customer.subscription.updated — eng ko'p ma'lumot beruvchi hodisa:
holat, reja, davr, cancel flag — barchasi shu yerda keladiIdempotency bu yerda alohida muhim (8.20: 2.5): Stripe hodisalarni kamida bir marta (at-least-once) yetkazadi, ya'ni bitta invoice.paid ikki marta kelishi mumkin. Agar davrni ikki marta yangilasangiz — muammo bo'lmaydi (idempotent), lekin agar har invoice.paidda mijozga rahmat emaili yuborsangiz — mijoz ikkita email oladi. Shuning uchun event.id bo'yicha tekshirib (birinchi martami?), takroriy hodisani e'tiborsiz qoldirish shart. Bu — 8.20-bobning idempotency naqshining aynan qo'llanilishi.
2.9. Xizmatni cheklash (entitlement)
// Reja ruxsat/limit (subscription holatiga qarab xizmat)
@Injectable()
export class EntitlementGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const sub = await this.subService.tenantSubscription(req.tenantId); // (8.25)
// Faol obuna kerak (active/trialing)
if (!["active", "trialing"].includes(sub.holat)) {
throw new ForbiddenException("Obunangiz faol emas. Iltimos, to'lovni yangilang");
}
// Reja imkoniyati (feature flag)
const kerakliReja = this.reflector.get("plan", context.getHandler());
if (kerakliReja && !this.rejaYetadimi(sub.reja, kerakliReja)) {
throw new ForbiddenException(`Bu imkoniyat ${kerakliReja} rejada mavjud`);
}
return true;
}
}
// @RequirePlan("pro") @Post("export") — faqat Pro rejaEntitlement (xizmatni cheklash): subscription holati va rejasi ruxsat/limit. Faol obuna kerak (active/trialing — aks holda bloklash — 2.4); reja imkoniyati (Pro funksiya — feature flag —
@RequirePlan("pro")); limit (5 loyiha — Free; cheksiz — Pro — 2.2). Guard 8.6-bob bilan tekshiriladi. To'lamasa/reja yetmasa bloklash + upgrade taklifi. Bu — subscription'ni biznesga bog'laydi (to'lov imkoniyat).
2.11. Checkout Session va Customer Portal (Stripe-hosted)
Subscription yaratishning ikki asosiy usuli bor: (1) API orqali to'g'ridan-to'g'ri (stripe.subscriptions.create — 2.5, Misol 4) — bu holda karta ma'lumotini o'zingiz yig'asiz (PCI mas'uliyati yuqori); (2) Stripe-hosted sahifalar (Checkout Session, Customer Portal) — bu holda mijoz Stripe'ning o'z sahifasiga yo'naltiriladi, karta u yerda kiritiladi (PCI mas'uliyati Stripe zimmasida — 8.19: 2.7 kabi). Ko'pchilik SaaS uchun ikkinchi usul tavsiya etiladi (kam kod, kam xavf).
CHECKOUT SESSION (subscription mode) — obunani boshlash:
1. Backend: stripe.checkout.sessions.create({ mode: "subscription", ... })
session.url qaytadi
2. Frontend: mijozni session.url'ga yo'naltiring (Stripe sahifasi)
3. Mijoz karta kiritadi, to'laydi (Stripe sahifasida — PCI Stripe'da)
4. Muvaffaqiyat success_url'ga qaytadi
5. Webhook: checkout.session.completed subscription bog'lanadi 2.8-bob
CUSTOMER PORTAL — mijoz o'zi boshqaradi (self-service):
- Reja o'zgartirish (upgrade/downgrade — 2.6)
- Karta yangilash (dunning'da muhim — 2.7)
- Invoice tarixini ko'rish/yuklab olish 2.15-bob
- Obunani bekor qilish 2.6-bob
stripe.billingPortal.sessions.create({ customer, return_url })
portal.url'ga yo'naltiring (Stripe boshqaradi — siz UI yozmaysiz)
Portal — dunning uchun eng muhim: mijoz muddati o'tgan kartani
o'zi yangilaydi (past_due active)Checkout Session va Customer Portal: Stripe-hosted sahifalar (karta ma'lumoti Stripe'da — PCI Stripe zimmasida). Checkout Session (
mode: "subscription") — obunani boshlash (mijoz Stripe sahifasida to'laydicheckout.session.completedwebhook — 2.8). Customer Portal — mijoz o'zi boshqaradi (reja o'zgartirish, karta yangilash, invoice, bekor — hech qanday UI yozmaysiz). Portal — dunning uchun eng qulay (mijoz muddati o'tgan kartani o'zi yangilaydi — 2.7). Bu usul — kam kod, kam PCI xavf, tavsiya etiladi.
2.12. To'lov usulini saqlash (SetupIntent)
Trial card-required 2.3-bob yoki keyinroq to'lov uchun kartani oldindan saqlash kerak bo'lganda — SetupIntent ishlatiladi. Farqi: PaymentIntent 8.19-bob — hozir pul yechadi; SetupIntent — pul yechmaydi, faqat kartani kelajakdagi davriy to'lovlar uchun tasdiqlaydi va saqlaydi (off-session — mijoz yo'qligida yechish uchun).
SetupIntent — kartani kelajakdagi davriy to'lov uchun saqlash:
1. Backend: stripe.setupIntents.create({ customer, usage: "off_session" })
client_secret qaytadi
2. Frontend: karta tasdiqlanadi (pul yechilmaydi — faqat saqlash)
3. payment_method customer'ning default usuli bo'ladi
4. Keyin: har oy off-session avtomatik yechish (mijoz yo'q — 2.5)
usage: "off_session" — davriy to'lov uchun majburiy
(mijoz brauzerda bo'lmasa ham yechish mumkin bo'lishi kerak)
3D Secure: ba'zi kartalar tasdiqlashda mijoz ishtirokini talab qiladi —
off_session'da bu invoice.payment_failed'ga olib kelishi mumkin (2.7)SetupIntent (to'lov usulini saqlash): PaymentIntent 8.19-bob hozir pul yechadi; SetupIntent pul yechmaydi — faqat kartani kelajakdagi davriy to'lov uchun saqlaydi (
usage: "off_session"— mijoz yo'qligida yechish uchun majburiy). Card-required trial va davriy billing uchun asos (2.3, 2.5). 3D Secure — ba'zi kartalar off-session'da qo'shimcha tasdiq talab qiladi (buinvoice.payment_failedbo'lishi mumkin — dunning'da mijoz portal orqali qayta tasdiqlaydi — 2.7, 2.11).
2.13. Coupon, discount va soliq (tax)
SaaS billing'da narx faqat reja narxidan iborat emas: chegirmalar (promo kod, referral, yillik chegirma) va soliq (VAT/QQS — mintaqaga bog'liq) ni ham hisobga olish kerak.
COUPON / DISCOUNT (chegirma):
- Coupon: chegirma qoidasi (10% yoki 50k so'm; bir marta yoki N oy)
- Promotion code: mijoz kiritadigan kod (WELCOME10) coupon'ga bog'lanadi
- Subscription'ga qo'llash: discount avtomatik invoice'da aks etadi
TAX (soliq — VAT/QQS):
- Stripe Tax: joylashuvga qarab avtomatik soliq hisoblaydi
- O'zbekiston QQS, EU VAT — har mamlakat stavkasi har xil
- Invoice'da alohida qator (subtotal + tax = total — 2.15)
Soliq — huquqiy talab (noto'g'ri hisoblash — jarima)
B2B: mijoz soliq raqamini (VAT ID) kiritsa — reverse charge (EU)Coupon/discount va tax: Coupon — chegirma qoidasi (10% yoki qat'iy summa; bir marta yoki bir necha oy); promotion code — mijoz kiritadigan kod (masalan, WELCOME10) coupon'ga bog'lanadi; subscription'ga qo'llanganda chegirma invoice'da avtomatik aks etadi. Tax (soliq — QQS/VAT) — joylashuvga qarab hisoblanadi (Stripe Tax avtomatik qiladi); invoice'da alohida qator (subtotal + soliq = total — 2.15). Soliq — huquqiy talab (noto'g'ri hisoblash jarimaga olib keladi); B2B'da mijoz VAT ID kiritsa — reverse charge qoidasi qo'llanishi mumkin.
2.14. Metered / usage-based billing (foydalanishga qarab)
Barcha SaaS oddiy oylik tarif (flat) bilan cheklanmaydi. Ba'zilari foydalanishga qarab to'lov oladi: API chaqiruvlari soni, saqlangan GB, yuborilgan SMS, ishlatilgan hisoblash daqiqasi. Bu — metered (o'lchanadigan) yoki usage-based billing.
BILLING MODELLARI:
- Flat 2.2-bob: qat'iy oylik narx (Pro = 99k/oy — soddaligi)
- Metered: foydalanishga qarab (1000 API = 10k so'm — davr oxirida)
- Hybrid: baza + oshib ketgan foydalanish (Pro 99k + 5k/1000 API ortiq)
METERED oqim (Stripe):
1. Price'ni metered qilib yarating (recurring.usage_type = "metered")
2. Foydalanish sodir bo'lganda: usage record yuboring
(stripe.billing.meterEvents.create — miqdorni qayd qiling)
3. Davr oxirida Stripe jami foydalanishni hisoblab invoice yaratadi
4. invoice.paid davr yangilanadi (2.8)
Idempotency 8.20-bob: usage record'ni ikki marta yubormang
(identifier bilan — aks holda mijoz ortiqcha to'laydi)
Lokal DB'da ham foydalanishni kuzating (quota — 2.9 bilan bog'liq)Metered / usage-based billing: flat (qat'iy oylik — 2.2) dan farqli — foydalanishga qarab (API soni, GB, SMS). Stripe'da metered price yaratiladi, foydalanish sodir bo'lganda usage record yuboriladi, davr oxirida Stripe jami bo'yicha invoice tuzadi. Hybrid — baza + oshib ketgan foydalanish. Usage record idempotent bo'lishi shart (identifier bilan — ikki marta yuborilsa mijoz ortiqcha to'laydi — 8.20). Lokal DB'da ham foydalanishni kuzating (quota bilan bog'liq — 2.9). Metered — infratuzilma, AI, kommunikatsiya SaaS'lari uchun tabiiy model.
2.15. Invoice, receipt va hisobot
Har davriy to'lov invoice (hisob-faktura) hosil qiladi — bu mijoz uchun hujjat va sizning buxgalteriyangiz uchun yozuv. To'lov o'tgach — receipt (kvitansiya). SaaS'da bu hujjatlarni mijozga ko'rsatish (Portal — 2.11) va o'zingizning hisobotingiz uchun saqlash kerak.
INVOICE HAYOTIY SIKLI (Stripe):
draft open (to'lov kutilmoqda) paid (to'landi) / uncollectible (yig'ilmadi)
- Stripe har davr uchun invoice avtomatik yaratadi (subscription'da)
- invoice.paid webhook 2.8-bob lokal DB'ga yozib qo'ying (buxgalteriya)
- PDF: invoice.invoice_pdf (Stripe tayyor PDF beradi)
yoki o'zingiz generatsiya qiling (8.21 — PDF/Excel)
- Receipt: to'lov tasdig'i (email — Stripe yuboradi yoki o'zingiz — 8.10)
HISOBOT (accounting):
- Oylik daromad (MRR — 2.1), soliq yig'indisi 2.13-bob
- Invoice ro'yxati eksport (Excel/CSV — 8.21) — buxgalteriya uchun
- Reconciliation: lokal DB Stripe (mos kelishini tekshirish — 8.19: 2.15)Invoice, receipt va hisobot: har davriy to'lov invoice (hisob-faktura) hosil qiladi (draft open paid); to'lovdan keyin receipt (kvitansiya). Stripe subscription uchun invoice'ni avtomatik yaratadi;
invoice.paidwebhook 2.8-bob kelganda lokal DB'ga yozing (buxgalteriya yozuvi). PDF — Stripe tayyor beradi (invoice_pdf) yoki o'zingiz generatsiya qilasiz 8.21-bob. Hisobot — oylik daromad (MRR — 2.1), soliq yig'indisi 2.13-bob, invoice eksporti (Excel/CSV — 8.21). Reconciliation — lokal DB va Stripe mos kelishini muntazam tekshiring (8.19: 2.15).
2.16. Mintaqaviy kontekst — Payme/Click va davriy to'lov
Stripe O'zbekistonda to'g'ridan-to'g'ri ishlamaydi, shuning uchun mahalliy SaaS'lar ko'pincha Payme yoki Click (mintaqaviy to'lov tizimlari — atamalar sifatida saqlanadi) ni ishlatadi. Ammo bu tizimlar dizayni bir martalik to'lovga 8.19-bob qaratilgan — Stripe Billing kabi to'liq subscription infratuzilmasi ularda yo'q. Bu — mahalliy kontekstda muhim cheklov.
MAHALLIY DAVRIY TO'LOV — cheklovlar va yechim:
MUAMMO: Payme/Click'da "subscription" tushunchasi Stripe kabi emas
- Avtomatik davriy yechish (off-session) — cheklangan yoki yo'q
- Trial, proration, dunning — o'zingiz qurishingiz kerak (2.5 Usul 2)
YECHIM:
- Karta token saqlash: agar tizim qo'llab-quvvatlasa (kartani bog'lash)
- Cron 8.18-bob: har oy saqlangan token orqali yechishga urinish
- Holat mashinasi, dunning, proration — o'zingiz (2.4, 2.6, 2.7)
- Muqobil: har oy mijozga to'lov havolasini yuborish (yarim avtomatik)
Off-session yechish (mijoz yo'qligida) — bu mintaqada asosiy qiyinchilik
Huquqiy: kartani saqlash — mahalliy qonun va tizim shartlariga rioyaMintaqaviy kontekst — Payme/Click: O'zbekistonda mahalliy SaaS'lar Payme/Click (mintaqaviy to'lov tizimlari) ni ishlatadi, ammo ular bir martalik to'lovga 8.19-bob mo'ljallangan — Stripe Billing kabi to'liq subscription infratuzilmasi yo'q. Cheklov: avtomatik off-session davriy yechish cheklangan; trial/proration/dunning ni o'zingiz qurishingiz kerak (2.5 Usul 2). Yechim: karta token saqlash (agar qo'llab-quvvatlansa) + cron 8.18-bob har oy yechishga urinish + holat mashinasini o'zingiz boshqarish; yoki har oy to'lov havolasini yuborish (yarim avtomatik). Kartani saqlash — mahalliy qonun va tizim shartlariga rioya qilib bajariladi.
2.17. Test qilish (Stripe test mode, webhook CLI)
Billing'ni ishlab chiqishda haqiqiy pul ishlatmasdan sinash muhim. Buning uchun test rejimi va lokal webhook sinashi ishlatiladi.
TEST MODE:
- Stripe test kalitlari (sk_test_..., pk_test_...) — haqiqiy pul yo'q
- Test kartalari: 4242 4242 4242 4242 (muvaffaqiyat),
4000 0000 0000 0341 (davriy to'lov o'tmaydi — dunning sinovi — 2.7)
- Test clock: vaqtni oldinga suradi (bir oylik davrni sekundlarda sinash)
WEBHOOK LOKAL SINOVI (CLI):
- stripe listen --forward-to localhost:3000/billing/webhook
(Stripe hodisalarni lokal serveringizga yo'naltiradi)
- stripe trigger invoice.paid (hodisani qo'lda ishga tushirish)
- Imzo tekshiruvi (8.20: 2.4) test secret bilan ishlaydi
Test va production kalitlarini adashtirmang (env — 8.19)
Har webhook handler'ni alohida sinang (invoice.paid, payment_failed, ...)Test qilish: billing haqiqiy pul ishlatmasdan sinaladi. Test mode — test kalitlari (
sk_test_...), test kartalari (muvaffaqiyat va ataylab o'tmaydigan — dunning sinovi uchun — 2.7), test clock (vaqtni oldinga surib bir oylik davrni tez sinash). Webhook lokal sinovi — CLI hodisalarni lokal serverga yo'naltiradi (listen --forward-to) va hodisalarni qo'lda ishga tushiradi (trigger). Test va production kalitlarini adashtirmang (env — 8.19); har webhook handler'ni alohida sinang.
2.18. Best practices (subscription)
Stripe Billing (davriy — to'lov tizimi boshqaradi — eng oson — 2.5)
State machine (trialing/active/past_due/cancelled — 2.4)
Trial (jalb qilish — cron kuzatuv — 2.3, 8.18)
Proration (upgrade adolatli — 2.6); bekor — davr oxiri 2.6-bob
Dunning (darrov bekor qilmaslik — qayta urinish — 2.7)
Webhook (holat sinxron — imzo + idempotency — 8.20, 2.8)
Entitlement (reja imkoniyat/limit — guard — 2.9)
Eslatmalar (trial tugashi, to'lov o'tmadi — email — 8.10)
MRR/churn kuzatuv (biznes ko'rsatkich — 2.1)
Stripe-hosted (Checkout + Portal — kam PCI xavf — 2.11)
SetupIntent (off-session karta saqlash — davriy uchun — 2.12)
Metered — idempotent usage record (ortiqcha to'lov yo'q — 2.14)
Invoice DB'ga yozish + reconciliation (buxgalteriya — 2.15)
Test mode + webhook CLI (haqiqiy pulsiz sinov — 2.17)3. Sintaksis — tez ma'lumotnoma
HOLATLAR 2.4-bob: trialing | active | past_due | cancelled | expired
REJA 2.2-bob: plan (free/pro/enterprise) narx + imkoniyat + limit
DAVRIY 2.5-bob: Stripe Subscriptions (webhook) yoki cron + saqlangan token
WEBHOOK 2.8-bob: invoice.paid | invoice.payment_failed | subscription.updated/deleted | checkout.session.completed
ENTITLEMENT 2.9-bob: @RequirePlan + EntitlementGuard (holat + reja)
HOSTED 2.11-bob: checkout.sessions.create(mode:"subscription") | billingPortal.sessions.create
KARTA 2.12-bob: setupIntents.create(usage:"off_session") — davriy uchun
METERED 2.14-bob: billing.meterEvents.create(identifier) — foydalanishga qarab
TEST 2.17-bob: sk_test_... | stripe listen --forward-to | stripe trigger4. Batafsil kod namunalari
Misol 1 — Plan va Subscription entity (2.2, 2.4)
@Entity()
export class Plan {
@PrimaryColumn() id: string; // "free", "pro", "enterprise"
@Column() nom: string;
@Column("bigint") oylikNarx: number; // tiyin (8.19: 2.5)
@Column("jsonb") imkoniyatlar: Record<string, any>; // { maxLoyiha: 5, support: false }
@Column({ nullable: true }) stripePriceId: string; // Stripe (2.5)
}
@Entity()
export class Subscription {
@PrimaryGeneratedColumn("uuid") id: string;
@Column() tenantId: string; // (8.25)
@Column() planId: string;
@Column({ type: "enum", enum: ["trialing", "active", "past_due", "cancelled", "expired"] })
holat: string; // state machine (2.4)
@Column({ nullable: true }) trialTugashi: Date;
@Column() davrBoshi: Date;
@Column() davrOxiri: Date; // keyingi to'lov sanasi
@Column({ default: false }) davrOxiridaBekor: boolean; // cancel at period end (2.6)
@Column({ nullable: true }) stripeSubId: string; // (2.5)
@Column({ nullable: true }) stripeCustomerId: string; // Portal, off-session (2.11)
@Column({ nullable: true }) stripeItemId: string; // reja o'zgartirish uchun (2.6)
}Misol 2 — Trial boshlash (2.3)
@Injectable()
export class SubscriptionService {
async trialBoshla(tenantId: string, planId = "pro"): Promise<Subscription> {
const trialKun = 14;
const trialTugashi = new Date(Date.now() + trialKun * 24 * 3600 * 1000);
return this.repo.save({
tenantId, planId, holat: "trialing", // (2.4)
trialTugashi, davrBoshi: new Date(), davrOxiri: trialTugashi,
});
// tenant Pro imkoniyatlari bilan (trial — 2.9)
}
}Misol 3 — Trial kuzatuv (cron — 2.3, 8.18)
@Cron(CronExpression.EVERY_DAY_AT_9AM)
async trialKuzat() {
// 3 kun qolganlarga eslatma (8.10)
const yaqinda = await this.repo.find({
where: { holat: "trialing", trialTugashi: Between(new Date(), new Date(Date.now() + 3 * 86400000)) },
});
for (const sub of yaqinda) {
await this.mail.trialEslatma(sub.tenantId); // "trial 3 kundan keyin tugaydi"
}
// Tugaganlar to'lov yoki bekor
const tugagan = await this.repo.find({
where: { holat: "trialing", trialTugashi: LessThan(new Date()) },
});
for (const sub of tugagan) {
if (sub.stripeSubId) continue; // Stripe boshqaradi (2.5)
// Karta yo'q expired (yoki to'lov so'rash)
await this.repo.update(sub.id, { holat: "expired" });
await this.mail.trialTugadi(sub.tenantId);
}
}Misol 4 — Stripe Subscription (davriy — 2.5)
// Stripe Billing — davriy to'lovni Stripe boshqaradi (eng oson)
async obunaYarat(tenantId: string, planId: string, paymentMethodId: string) {
const plan = await this.planRepo.findOneBy({ id: planId });
const tenant = await this.tenantService.bitta(tenantId);
// Stripe customer + subscription
const customer = await this.stripe.customers.create({
email: tenant.email,
payment_method: paymentMethodId,
invoice_settings: { default_payment_method: paymentMethodId },
});
const stripeSub = await this.stripe.subscriptions.create({
customer: customer.id,
items: [{ price: plan.stripePriceId }], // reja narxi (2.2)
trial_period_days: 14, // trial (2.3)
});
// Lokal DB (Stripe — manba; lokal — kesh/tezlik)
return this.repo.save({
tenantId, planId, holat: "trialing",
stripeSubId: stripeSub.id, davrOxiri: new Date(stripeSub.current_period_end * 1000),
});
// har oy Stripe avtomatik yechadi webhook (Misol 6)
}Misol 5 — Upgrade + proration (2.6)
async rejaOzgartir(tenantId: string, yangiPlanId: string) {
const sub = await this.tenantSubscription(tenantId);
const yangiPlan = await this.planRepo.findOneBy({ id: yangiPlanId });
const eskiPlan = await this.planRepo.findOneBy({ id: sub.planId });
const upgrade = yangiPlan.oylikNarx > eskiPlan.oylikNarx;
if (upgrade) {
// Upgrade — DARROV + proration (Stripe avtomatik hisoblaydi)
await this.stripe.subscriptions.update(sub.stripeSubId, {
items: [{ id: sub.stripeItemId, price: yangiPlan.stripePriceId }],
proration_behavior: "always_invoice", // qolgan kun farqi (2.6)
});
await this.repo.update(sub.id, { planId: yangiPlanId }); // darrov yangi imkoniyat
} else {
// Downgrade — KEYINGI davrdan (to'lagani amal qiladi — 2.6)
await this.stripe.subscriptions.update(sub.stripeSubId, {
items: [{ id: sub.stripeItemId, price: yangiPlan.stripePriceId }],
proration_behavior: "none",
billing_cycle_anchor: "unchanged",
});
// Lokal: keyingi davrda o'zgaradi (webhook'da)
}
}Misol 6 — Webhook (holat sinxron — 2.8, 8.20)
@Post("billing/webhook")
@HttpCode(200)
async webhook(@Req() req: RawBodyRequest<Request>, @Headers("stripe-signature") sig: string) {
const event = this.stripe.webhooks.constructEvent( // imzo (8.20: 2.4)
req.rawBody, sig, this.config.get("STRIPE_WEBHOOK_SECRET"),
);
// Idempotency (8.20: 2.5)
if (!(await this.idempotency.birinchiMartami(event.id))) return { received: true };
switch (event.type) {
case "invoice.paid": { // davriy to'lov o'tdi
const sub = event.data.object.subscription;
await this.repo.update({ stripeSubId: sub }, {
holat: "active", davrOxiri: new Date(event.data.object.period_end * 1000),
});
break;
}
case "invoice.payment_failed": // dunning (2.7)
await this.repo.update({ stripeSubId: event.data.object.subscription }, { holat: "past_due" });
await this.mail.tolovOtmadi(/* tenant */); // eslatma (8.10)
break;
case "customer.subscription.deleted": // bekor
await this.repo.update({ stripeSubId: event.data.object.id }, { holat: "cancelled" });
break;
}
return { received: true };
}Misol 7 — Bekor qilish (davr oxiri — 2.6)
async bekorQil(tenantId: string, darrov = false) {
const sub = await this.tenantSubscription(tenantId);
if (darrov) {
await this.stripe.subscriptions.cancel(sub.stripeSubId); // darrov (refund bilan)
await this.repo.update(sub.id, { holat: "cancelled" });
} else {
// Davr oxirida bekor (to'lagani uchun xizmat — 2.6, tavsiya)
await this.stripe.subscriptions.update(sub.stripeSubId, { cancel_at_period_end: true });
await this.repo.update(sub.id, { davrOxiridaBekor: true });
// muddat oxirigacha active (webhook'da cancelled bo'ladi)
}
await this.mail.bekorTasdiq(tenantId); // "bekor qilindi, oxirigacha amal qiladi"
}Misol 8 — Entitlement guard (xizmat cheklash — 2.9)
export const RequirePlan = (...rejalar: string[]) => SetMetadata("plans", rejalar);
@Injectable()
export class SubscriptionGuard implements CanActivate {
constructor(private subService: SubscriptionService, private reflector: Reflector) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const sub = await this.subService.tenantSubscription(req.tenantId); // (8.25)
// 1. Faol obuna (2.4)
if (!["active", "trialing"].includes(sub.holat)) {
throw new ForbiddenException({
message: "Obunangiz faol emas",
code: "SUBSCRIPTION_INACTIVE", upgradeUrl: "/billing", // frontend upgrade taklif
});
}
// 2. Reja imkoniyati
const kerakli = this.reflector.get<string[]>("plans", context.getHandler());
if (kerakli && !kerakli.includes(sub.planId)) {
throw new ForbiddenException({ message: `Bu imkoniyat ${kerakli.join("/")} rejada`, code: "PLAN_REQUIRED" });
}
return true;
}
}
// Ishlatish
@RequirePlan("pro", "enterprise")
@Post("reports/advanced")
@UseGuards(JwtAuthGuard, SubscriptionGuard)
hisobot() { /* faqat Pro+ */ }Misol 9 — Limit tekshirish (entitlement — 2.2, 2.9)
// Reja limiti (Free: 5 loyiha; Pro: cheksiz)
async loyihaYaratishMumkinmi(tenantId: string): Promise<boolean> {
const sub = await this.tenantSubscription(tenantId);
const plan = await this.planRepo.findOneBy({ id: sub.planId });
const maxLoyiha = plan.imkoniyatlar.maxLoyiha; // -1 = cheksiz
if (maxLoyiha === -1) return true; // cheksiz (Pro)
const joriy = await this.loyihaRepo.count({ where: { tenantId } });
if (joriy >= maxLoyiha) {
throw new ForbiddenException({
message: `Free reja ${maxLoyiha} loyiha bilan cheklangan. Pro'ga o'ting`,
code: "LIMIT_REACHED",
});
}
return true;
}Misol 10 — MRR/churn kuzatuv (biznes — 2.1)
// SaaS biznes ko'rsatkichlari (dashboard)
async metrikalar() {
const faol = await this.repo.find({ where: { holat: In(["active", "trialing"]) } });
// MRR (Monthly Recurring Revenue — oylik takrorlanuvchi daromad)
const mrr = faol.reduce((s, sub) => {
const plan = this.planlar.get(sub.planId);
return s + (sub.holat === "active" ? plan.oylikNarx : 0);
}, 0);
// Churn (bekor qilganlar — oxirgi oy)
const bekorQilgan = await this.repo.count({
where: { holat: "cancelled", cancelledAt: MoreThan(new Date(Date.now() - 30 * 86400000)) },
});
return {
mrr: mrr / 100, // so'm
faolObuna: faol.length,
churnRate: (bekorQilgan / faol.length) * 100, // % (SaaS sog'lig'i)
};
}Misol 11 — Checkout Session (subscription mode — 2.11)
// Stripe-hosted checkout — mijoz Stripe sahifasida to'laydi (PCI Stripe'da)
async checkoutSessionYarat(tenantId: string, planId: string): Promise<string> {
const plan = await this.planRepo.findOneBy({ id: planId });
const tenant = await this.tenantService.bitta(tenantId);
const session = await this.stripe.checkout.sessions.create({
mode: "subscription", // obuna rejimi (2.11)
customer_email: tenant.email,
line_items: [{ price: plan.stripePriceId, quantity: 1 }], // reja narxi (2.2)
subscription_data: { trial_period_days: 14 }, // trial (2.3)
success_url: `${this.config.get("APP_URL")}/billing/success?session={CHECKOUT_SESSION_ID}`,
cancel_url: `${this.config.get("APP_URL")}/billing/cancel`,
// tenant'ni webhook'da topish uchun (checkout.session.completed — 2.8)
metadata: { tenantId },
client_reference_id: tenantId,
});
return session.url; // frontend bu URL'ga yo'naltiradi
// to'lovdan keyin checkout.session.completed webhook (Misol 13)
}Misol 12 — Customer Portal (self-service — 2.11)
// Mijoz o'zi boshqaradi: reja, karta, invoice, bekor — UI yozmaysiz
async portalHavola(tenantId: string): Promise<string> {
const sub = await this.tenantSubscription(tenantId);
const portal = await this.stripe.billingPortal.sessions.create({
customer: sub.stripeCustomerId, // Stripe customer (Misol 4)
return_url: `${this.config.get("APP_URL")}/billing`,
});
return portal.url; // Stripe boshqaradigan sahifa
// mijoz kartani yangilaydi (dunning past_due active — 2.7),
// rejani o'zgartiradi, invoice yuklaydi, bekor qiladi (2.6)
}Misol 13 — Kengaytirilgan webhook (barcha hodisalar — 2.8)
// checkout.session.completed + subscription.updated qo'shildi (2.8 to'liq ro'yxat)
switch (event.type) {
case "checkout.session.completed": { // obuna boshlandi (Misol 11)
const session = event.data.object;
const stripeSub = await this.stripe.subscriptions.retrieve(session.subscription);
await this.repo.save({
tenantId: session.metadata.tenantId, // Misol 11 metadata
stripeSubId: stripeSub.id,
stripeCustomerId: session.customer,
holat: stripeSub.status, // "trialing" yoki "active"
davrOxiri: new Date(stripeSub.current_period_end * 1000),
});
break;
}
case "customer.subscription.updated": { // reja/holat o'zgardi (2.6, 2.7)
const s = event.data.object;
await this.repo.update({ stripeSubId: s.id }, {
holat: s.status, // active/past_due/trialing
davrOxiridaBekor: s.cancel_at_period_end, // bekor rejalashtirildi (2.6)
davrOxiri: new Date(s.current_period_end * 1000),
planId: s.items.data[0].price.metadata.planId, // yangi reja (downgrade — 2.6)
});
break;
}
case "invoice.paid": // davriy to'lov o'tdi (yuqorida)
/* ... Misol 6 ... */ break;
case "invoice.payment_failed": // dunning (2.7)
/* ... Misol 6 ... */ break;
}Misol 14 — Metered usage record (foydalanishga qarab — 2.14)
// Foydalanish sodir bo'lganda: Stripe'ga qayd + lokal quota (2.9)
async foydalanishQaydEt(tenantId: string, miqdor: number, hodisaId: string) {
const sub = await this.tenantSubscription(tenantId);
// Stripe'ga qayd — idempotency identifier bilan (8.20 — ikki marta yubormaslik)
await this.stripe.billing.meterEvents.create({
event_name: "api_call",
payload: { stripe_customer_id: sub.stripeCustomerId, value: String(miqdor) },
identifier: hodisaId, // takrorlanmaslik uchun (2.14)
});
// Lokal kuzatuv — quota tekshiruvi uchun (2.9)
await this.usageRepo.increment({ tenantId, davr: this.joriyDavr() }, "miqdor", miqdor);
// davr oxirida Stripe jami bo'yicha invoice yaratadi 2.15-bob invoice.paid
}5. To'g'ri va noto'g'ri holatlar
1) Davriy to'lovni qo'lda boshqarish (Stripe bor)
cron + token + holat o'zingiz (murakkab, xato — 2.5)
Stripe Billing (webhook — Stripe boshqaradi)2) To'lov o'tmasa darrov bekor
past_due darrov cancel (vaqtinchalik muammo — 2.7)
dunning (qayta urinish)3) Bekorda darrov kesish
bekor darrov bloklash (to'lagani uchun — 2.6)
davr oxirigacha xizmat4) Upgrade proration'siz
to'liq oy qayta (adolatsiz — 2.6)
proration (qolgan kun farqi)5) Holat tekshirmasdan xizmat
cancelled tenant ishlaydi (2.9)
entitlement guard (holat + reja)6) Kartani o'z serveringizda yig'ish (PCI mas'uliyat)
karta raqamini o'z formangizda olish (PCI yuki, xavf — 2.11)
Checkout Session / Portal (karta Stripe sahifasida — 2.11)7) Faqat invoice.paid'ni tinglash
boshqa hodisalar e'tiborsiz DB drift (holat mos emas — 2.8)
updated/deleted/checkout.completed ham (to'liq sinxron — Misol 13)8) Usage record'ni idempotentsiz yuborish
takroriy usage mijoz ortiqcha to'laydi (2.14)
identifier bilan (takrorlanmaydi — 8.20)6. Keng tarqalgan xatolar va yechimlari
Xato 1 — Davriy to'lov murakkab
Sababi: qo'lda boshqarish 2.5-bob. Yechimi: Stripe Billing (webhook).
Xato 2 — Holat DB va Stripe mos emas
Sababi: webhook ishlanmagan 2.8-bob. Yechimi: webhook + idempotency + reconciliation (8.19: 2.15).
Xato 3 — To'lamagan mijoz ishlaydi
Sababi: entitlement yo'q 2.9-bob. Yechimi: subscription guard.
Xato 4 — Trial tugashi sezilmadi
Sababi: cron yo'q 2.3-bob. Yechimi: trial kuzatuv cron (Misol 3).
Xato 5 — Bekorda mijoz norozi (darrov kesildi)
Sababi: immediate cancel 2.6-bob. Yechimi: cancel at period end.
Xato 6 — Proration noto'g'ri
Sababi: qo'lda hisoblash. Yechimi: Stripe proration (avtomatik).
Xato 7 — Off-session to'lov 3D Secure'da to'xtaydi
Sababi: karta qo'shimcha tasdiq talab qiladi 2.12-bob. Yechimi: invoice.payment_failed mijozni Portal orqali qayta tasdiqlashga chaqirish (dunning — 2.7, 2.11).
Xato 8 — Soliq hisobga olinmagan
Sababi: faqat reja narxi olindi 2.13-bob. Yechimi: Stripe Tax (joylashuvga qarab avtomatik) + invoice'da alohida qator 2.15-bob.
7. Integratsiya — bu mavzu stack'ning qayerida uchraydi
- Payment 8.19-bob: to'lov asosi.
- Webhook 8.20-bob: subscription hodisalari.
- Multi-tenant 8.25-bob: tenant obunasi.
- Cron 8.18-bob: trial, dunning.
- Email 8.10-bob: eslatmalar.
- Guard (8.6, 8.9): entitlement.
- Audit 8.26-bob: billing harakatlari.
- Xavfsizlik (14): to'lov, holat.
8. Eng yaxshi amaliyotlar (best practices)
- Stripe Billing (davriy — to'lov tizimi boshqaradi — 2.5).
- State machine (trialing/active/past_due/cancelled — 2.4).
- Trial (jalb — cron kuzatuv — 2.3).
- Proration (upgrade adolatli — 2.6); bekor — davr oxiri 2.6-bob.
- Dunning (darrov bekor qilmaslik — 2.7).
- Webhook (holat sinxron — imzo + idempotency — 2.8).
- Entitlement (reja imkoniyat/limit — guard — 2.9).
- Eslatmalar (trial, dunning — email — 8.10).
- MRR/churn kuzatuv (biznes ko'rsatkich — 2.1).
- Reconciliation (DBStripe — 8.19: 2.15).
9. Amaliy loyiha: "SaaS Subscription Billing"
Subscription'ni amalda mustahkamlash.
Maqsad
To'liq SaaS billing: rejalar, trial, davriy to'lov, upgrade/bekor, dunning, entitlement.
Talablar (requirements)
- Plan/Subscription entity: state machine (Misol 1, 2.2, 2.4).
- Trial: boshlash + cron kuzatuv (Misol 2, 3, 2.3).
- Stripe subscription: davriy (Misol 4, 2.5).
- Upgrade/downgrade: proration (Misol 5, 2.6).
- Webhook: holat sinxron (Misol 6, 2.8).
- Bekor: davr oxiri (Misol 7, 2.6).
- Entitlement: reja guard (Misol 8, 2.9).
- Limit: reja cheklovi (Misol 9, 2.2).
- Dunning: past_due qayta urinish 2.7-bob.
- Metrika: MRR/churn (Misol 10, 2.1).
- Checkout + Portal: Stripe-hosted (Misol 11, 12, 2.11).
- Invoice DB: invoice.paid buxgalteriya yozuvi 2.15-bob.
- Test: test mode + webhook CLI bilan sinash 2.17-bob.
Maslahatlar (hint)
- Stripe Billing (2.5, 1-xato).
- State machine 2.4-bob.
- Dunning (darrov bekor qilmaslik — 2.7, 2-holat).
- Bekor davr oxiri (2.6, 3-holat).
- Entitlement guard (2.9, 5-holat).
- Trial cron (2.3, 4-xato).
"Tayyor" mezonlari (acceptance criteria)
- Plan/Subscription (state machine).
- Trial (cron).
- Stripe subscription.
- Upgrade (proration).
- Webhook.
- Bekor (davr oxiri).
- Entitlement.
- Limit.
- Dunning.
- Metrika (MRR).
- Checkout + Portal (Stripe-hosted).
- Invoice DB (buxgalteriya).
- Test (test mode + webhook CLI).
Yechim kodi ataylab berilmagan — bu loyihani o'zingiz yozib ko'ring.
10. Xulosa va keyingi bobga ko'prik
Bu bobda subscription va davriy to'lovni o'rgandik:
- Bir martalik vs davriy (MRR — 2.1); rejalar (tier — 2.2); trial (jalb — 2.3); state machine 2.4-bob.
- Davriy yechish (Stripe Billing — 2.5); upgrade/downgrade (proration — 2.6); dunning 2.7-bob.
- Webhook (holat sinxron — 2.8); entitlement (reja xizmat — 2.9).
- Stripe-hosted (Checkout + Portal — 2.11); SetupIntent (off-session karta — 2.12); coupon/soliq 2.13-bob.
- Metered billing (foydalanishga qarab — 2.14); invoice/hisobot 2.15-bob; mintaqaviy (Payme/Click — 2.16); test 2.17-bob.
Keyingi bob — 8.30: i18n (ko'p til) backend. Subscription'ni bildik; endi oxirgi real mavzu — i18n (internationalization — backend'da ko'p til: xato xabarlari, email, kontent, valyuta/sana formati) — ni o'rganamiz. O'zbekiston (o'zbek/rus/ingliz) va xalqaro ilovalar uchun zarur.
Foydalanilgan rasmiy/ishonchli manbalar
- Stripe Billing rasmiy hujjatlari — subscription hayotiy sikli, proration, trial, dunning
- Stripe Checkout va Customer Portal — hosted to'lov va self-service boshqaruv
- Stripe Webhooks — subscription hodisalari (invoice.paid, payment_failed, subscription.updated/deleted, checkout.session.completed) va imzo tekshiruvi
- Stripe SetupIntent va off-session to'lov — davriy billing uchun karta saqlash
- Stripe Metered/usage-based billing — foydalanishga qarab hisob
- Stripe Tax — joylashuvga bog'liq soliq hisoblash
- Stripe CLI va test mode — lokal webhook sinovi, test kartalari, test clock
- SaaS moliyaviy ko'rsatkichlari — MRR, churn, dunning bo'yicha umumiy amaliyotlar
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!