WisarWisar
Dasturlash kitobi/8-QISM — NestJS30 daqiqa

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 yuragiobuna 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

text
  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'rsatkichi

Bir 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)

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

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

text
  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) yoki cancelled; active cancelled (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)

text
  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

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

Upgrade/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)

text
  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_due eslatma (email — "kartangizni yangilang" — 8.10) qayta urinish (3/5/7 kun — 8.20 retry) tugasa cancelled + 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)

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

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

Idempotency 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)

typescript
// 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 reja

Entitlement (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).

text
  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'laydi checkout.session.completed webhook — 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).

text
  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 (bu invoice.payment_failed bo'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.

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

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

text
  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.paid webhook 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.

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

Mintaqaviy 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.

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

text
   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

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

4. Batafsil kod namunalari

Misol 1 — Plan va Subscription entity (2.2, 2.4)

typescript
@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)

typescript
@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)

typescript
@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)

typescript
// 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)

typescript
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)

typescript
@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)

typescript
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)

typescript
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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

typescript
// 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)

text
 cron + token + holat o'zingiz (murakkab, xato — 2.5)
 Stripe Billing (webhook — Stripe boshqaradi)

2) To'lov o'tmasa darrov bekor

text
 past_due  darrov cancel (vaqtinchalik muammo — 2.7)
 dunning (qayta urinish)

3) Bekorda darrov kesish

text
 bekor  darrov bloklash (to'lagani uchun — 2.6)
 davr oxirigacha xizmat

4) Upgrade proration'siz

text
 to'liq oy qayta (adolatsiz — 2.6)
 proration (qolgan kun farqi)

5) Holat tekshirmasdan xizmat

text
 cancelled tenant ishlaydi (2.9)
 entitlement guard (holat + reja)

6) Kartani o'z serveringizda yig'ish (PCI mas'uliyat)

text
 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

text
 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

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

  1. Plan/Subscription entity: state machine (Misol 1, 2.2, 2.4).
  2. Trial: boshlash + cron kuzatuv (Misol 2, 3, 2.3).
  3. Stripe subscription: davriy (Misol 4, 2.5).
  4. Upgrade/downgrade: proration (Misol 5, 2.6).
  5. Webhook: holat sinxron (Misol 6, 2.8).
  6. Bekor: davr oxiri (Misol 7, 2.6).
  7. Entitlement: reja guard (Misol 8, 2.9).
  8. Limit: reja cheklovi (Misol 9, 2.2).
  9. Dunning: past_due qayta urinish 2.7-bob.
  10. Metrika: MRR/churn (Misol 10, 2.1).
  11. Checkout + Portal: Stripe-hosted (Misol 11, 12, 2.11).
  12. Invoice DB: invoice.paid buxgalteriya yozuvi 2.15-bob.
  13. 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!
8.29-bob: Subscription va davriy to'lov (recurring billing) — Wisar