Mundarija (18)
- Bu darsda
- 1. Nega bu kerak?
- 2. Holat mashinasi
- 2.1 Hodisa → holat + amallar
- 3. Oy qo'shish tuzog'i
- 4. Dunning: to'lov o'tmasa
- 5. Proratsiya: davr o'rtasida reja almashsa
- 6. Bekor qilish: ikki xil
- 7. Karta tokeni bilan davriy yechish
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): holat qaysi?
- 2-mashq (o'rta): huquqBormi
- 3-mashq (qiyin): langar bilan davrlar
- 10. Loyiha qadami: obuna.ts
- 11. Real ishda
- Xulosa
- Manbalar
Obuna va davriy to'lovlar: sinov, dunning va proratsiya
Qisqacha: Obuna — bir martalik to'lov emas, vaqt bo'ylab yashaydigan holat mashinasi: sinov (trial) → faol → to'lov o'tmasa qarzdor → qayta urinishlar (dunning) → tiklanadi yoki bekor. Kanon uni sof funksiyalar bilan yozdi:
hodisa(obuna, hodisa, vaqt)yangi holatni va bajariladigan amallar ro'yxatini (undirish, xat, qaytarish) qaytaradi — pulni o'zi yechmaydi. Ikki nozik joy: oyga oy qo'shish (31-yanvar + 1 oy = 28-fevral, 3-mart emas) va davr o'rtasida rejani almashtirganda proratsiya — millisekund aniqligida,BigIntbilan. Karta tokeni bilan haqiqiy yechish (Payme Subscribe, Click, Uzum) — hujjat bo'yicha, sinalmagan.
Bu darsda
- Obuna holatlarini va o'tishlarini chizasiz.
Datebilan oy qo'shish tuzog'ini ko'rasiz va to'g'rioyQoshyozasiz.- Dunning jadvalini (1, 3, 7 kun) va huquq (entitlement) qoidasini bilasiz.
- Upgrade va downgrade proratsiyasini hisoblaysiz.
- "Holat + amallar" naqshi nega test qilish uchun qulay ekanini tushunasiz.
obuna.tsmodulini yozasiz.
Oldin bilishingiz kerak: Stripe, Uzum va boshqa mahalliy to'lov tizimlari (tokenlash), Pul bilan ishlash JS'da, Date va vaqt zonalari, Vaqt va tasodifni boshqarish.
Oldingi darslarda har to'lov — bitta buyurtma. Obunada bitta mijoz har oy to'laydi, ba'zan to'lov o'tmaydi, ba'zan reja o'zgaradi. Endi vaqt ham hisobning bir qismi.
1. Nega bu kerak?
SaaS, onlayn kurs, musiqa xizmati, "Pro" rejali bot — hammasi obuna. Obunadagi xatolar to'g'ridan pulga uradi:
- to'lov o'tmadi, lekin xizmat davom etyapti — oylab tekin foydalanish;
- yoki aksincha: bir martalik bank xatosi tufayli sodiq mijozning xizmati darhol o'chdi va u ketib qoldi;
- 31-yanvarda obuna bo'lgan mijoz martda "3-mart" sanasi bilan hisob oldi;
- oy o'rtasida "Pro" ga o'tgan mijozdan to'liq oy narxi olindi.
Obuna kodi ko'pincha cron (rejali vazifa) ichida, "if" larga to'la holda yoziladi va testsiz qoladi. Kanon boshqa yo'l tanladi: biznes mantiq — sof funksiya, vaqt — parametr.
2. Holat mashinasi
| Holat | Ma'nosi | Xizmat (huquq) |
|---|---|---|
sinov |
bepul sinov davri (trial) | bor, davr oxirigacha |
faol |
to'langan davr | bor, davr oxirigacha |
qarzdor |
to'lov o'tmadi, qayta urinilmoqda (past due) | bor (imtiyozli davr — grace) |
bekor |
urinishlar tugadi yoki darhol bekor | yo'q |
tugagan |
"davr oxirida bekor" bajarildi | yo'q |
stateDiagram-v2
[*] --> sinov: 14 kun
[*] --> faol: sinovsiz
sinov --> faol: to'lov o'tdi
sinov --> qarzdor: to'lov o'tmadi
faol --> faol: yangi davr to'landi
faol --> qarzdor: to'lov o'tmadi
qarzdor --> faol: qayta urinish o'tdi
qarzdor --> bekor: 4-muvaffaqiyatsizlik
faol --> tugagan: davr oxirida bekor
faol --> bekor: darhol bekor
bekor --> [*]
tugagan --> [*]qarzdor holatida ham xizmat bor — bu biznes qarori (kanon izohida shunday yozilgan). Sabab: kartada vaqtincha mablag' yo'q, bank texnik nosozligi — bu mijozning aybi emas. Bir necha kun imtiyoz berib, qayta urinish yo'qotilgan mijozlardan ko'ra arzon.
2.1 Hodisa → holat + amallar
Kanon funksiyasi imzosi:
export function hodisa(
o: Obuna, h: Hodisa, hozir: Date,
): { obuna: Obuna; amallar: Amal[] }Hodisalar: davr_tugadi, tolov_otdi, tolov_otmadi, urinish_vaqti, bekor_qil, reja_ozgartir. Amallar: { tur: "undir", summa, sabab }, { tur: "xat", shablon }, { tur: "qaytar", summa }.
Funksiya hech narsani bajarmaydi: pul yechmaydi, xat yubormaydi, bazaga yozmaydi. U faqat "nima qilish kerak" ni aytadi. Amallarni boshqa qatlam bajaradi: undir — provayderning karta tokeni API'si, xat — email navbati, natija (tolov_otdi yoki tolov_otmadi) esa yana hodisa ga qaytadi.
Bir davr almashuvi shunday aylanadi — diagrammada hodisa() faqat o'rtadagi qutida ishlaydi, pul esa o'ngda yechiladi:
sequenceDiagram
participant C as Cron
participant H as hodisa()
participant B as Bajaruvchi
participant P as Provayder
C->>H: davr_tugadi
H-->>B: amal undir (narx)
B->>P: token bilan yechish
P-->>B: o'tdi yoki o'tmadi
B->>H: tolov_otdi / tolov_otmadi
H-->>B: yangi holat, amallar"Bajaruvchi" — bazaga yozadigan, provayderni chaqiradigan va navbatga xat qo'yadigan oddiy kod. Unda biznes qarori yo'q, shuning uchun u kichik bo'lib qoladi.
Bu naqshning foydasi testda ko'rinadi: kanonning beshta obuna testi bazasiz, tarmoqsiz va soatsiz — har biri millisekundning bir qismida o'tadi. 12 oylik obunani sinash uchun 12 oy kutish shart emas: hozir parametrini berasiz (Vaqt va tasodifni boshqarish).
3. Oy qo'shish tuzog'i
"Bir oydan keyin" — kalendar tushunchasi, millisekund emas. Date bilan oddiy yo'l xato beradi:
const yanvar = new Date("2026-01-31T10:00:00Z");
const oddiy = new Date(yanvar);
oddiy.setUTCMonth(oddiy.getUTCMonth() + 1);
console.log(oddiy.toISOString().slice(0, 10));Konsolda:
2026-03-0331-fevral yo'q — Date uni 3-martga "toshirib" yubordi. Mijoz fevral uchun hisob olmadi. Kanon oyQosh oyning oxiriga qisadi (clamp):
function oyQosh(d, n) {
const y = d.getUTCFullYear();
const m = d.getUTCMonth() + n;
const kun = d.getUTCDate();
const oxirgi = new Date(Date.UTC(y, m + 1, 0)).getUTCDate();
return new Date(Date.UTC(y, m, Math.min(kun, oxirgi),
d.getUTCHours(), d.getUTCMinutes()));
}
const d = (s) => new Date(s);
const sana = (x) => x.toISOString().slice(0, 16);
console.log(sana(oyQosh(d("2026-01-31T10:00Z"), 1)));
console.log(sana(oyQosh(d("2028-01-31T10:00Z"), 1)));
console.log(sana(oyQosh(d("2026-12-15T00:00Z"), 1)));
const zanjir = oyQosh(oyQosh(d("2026-01-31T10:00Z"), 1), 1);
console.log(sana(zanjir), sana(oyQosh(d("2026-01-31T10:00Z"), 2)));Konsolda:
2026-02-28T10:00
2028-02-29T10:00
2027-01-15T00:00
2026-03-28T10:00 2026-03-31T10:00Date.UTC(y, m + 1, 0)— keyingi oyning "nolinchi" kuni, ya'ni shu oyning oxirgi kuni. 2026 — 28, 2028 (kabisa) — 29.- Dekabr + 1 → keyingi yil yanvari:
Date.UTCoy raqami 12 ni o'zi to'g'rilaydi. - Hammasi UTC'da (Date va vaqt zonalari): server qaysi zonada ishlashidan qat'i nazar, natija bir xil. (Bu ixcham nusxada soniya va millisekund tashlangan; kanon
oyQoshularni ham saqlaydi.)
Oxirgi qatorga e'tibor bering: 31-yanvardan ketma-ket ikki marta oy qo'shish 28-martni beradi, birdaniga ikki oy esa 31-martni. Kanon har yangi davrni oldingi davr oxiridan hisoblaydi (davrOxiri(bosh, reja)), ya'ni zanjir usuli: 31-yanvarda boshlangan obunaning sanasi fevraldan keyin 28 ga "yopishib" qoladi. Ko'p tizimlar buning o'rniga birinchi sanani — hisob langari (billing anchor) — saqlaydi va har davrni undan hisoblaydi: oyQosh(langar, davrRaqami). Qaysi biri to'g'ri — siyosat; muhimi, tanlovni bilib qilish.
4. Dunning: to'lov o'tmasa
Dunning — to'lov o'tmagan mijozdan qayta-qayta undirish jarayoni: qayta urinishlar va eslatma xatlari. Kanon jadvali — DUNNING_KUN = [1, 3, 7]: birinchi muvaffaqiyatsizlikdan keyin 1 kun, ikkinchisidan 3, uchinchisidan 7 kun kutib qayta urinish; to'rtinchi muvaffaqiyatsizlik — bekor.
const DUNNING_KUN = [1, 3, 7];
const KUN = 86_400_000;
function tolovOtmadi(o, hozir) {
const n = o.urinishlar + 1;
const kun = DUNNING_KUN[n - 1];
if (kun === undefined) {
return { ...o, holat: "bekor", urinishlar: n, keyingi: null };
}
const keyingi = new Date(hozir.getTime() + kun * KUN);
return { ...o, holat: "qarzdor", urinishlar: n, keyingi };
}
let o = { holat: "faol", urinishlar: 0, keyingi: null };
let hozir = new Date("2026-11-01T00:00:00Z");
for (let i = 0; i <= DUNNING_KUN.length; i++) {
o = tolovOtmadi(o, hozir);
const k = o.keyingi?.toISOString().slice(0, 10) ?? "-";
console.log(`${o.holat}:${o.urinishlar}:${k}`);
if (o.keyingi) hozir = o.keyingi;
}Konsolda:
qarzdor:1:2026-11-02
qarzdor:2:2026-11-05
qarzdor:3:2026-11-12
bekor:4:-Bu kanon testidagi iz bilan aynan bir xil (qarzdor:1:2026-11-02 … bekor:4:-). Kanonda har tolov_otmadi bilan birga { tur: "xat", shablon: "tolov-otmadi" } amali chiqadi, bekor bo'lganda — obuna-bekor.
Qayta urinish o'zi boshlanmaydi: rejali vazifa (cron) vaqti-vaqti bilan urinish_vaqti hodisasini yuboradi. Kanon funksiyasi vaqt kelmagan bo'lsa bo'sh amallar ro'yxatini, kelgan bo'lsa undir … qayta-urinish ni qaytaradi. Rejali vazifalar — Cron va rejali vazifalar darsida.
To'lov qarzdorlikdan keyin o'tsa, yangi davr to'lov sanasidan boshlanadi (kanon qarori): 1-noyabrda davr tugadi, 2-noyabrda qayta urinish o'tdi → yangi davr 2-noyabr — 2-dekabr. Boshqa variant (davr eski sanadan davom etadi) ham keng tarqalgan — u holda mijoz qarzdorlik kunlari uchun ham to'laydi.
5. Proratsiya: davr o'rtasida reja almashsa
Mijoz oy o'rtasida "Oddiy" (30 000 so'm) dan "Pro" (90 000 so'm) ga o'tdi. Qancha olish kerak? Proratsiya (proration) — davrning qolgan qismi uchun eski reja kreditini qaytarib, yangi reja narxini olish:
farq = yangi narx × qolgan / jami − eski narx × qolgan / jamiKanon buni millisekund aniqligida va BigInt bilan hisoblaydi (millisekund × tiyin ko'paytmasi 2^53 dan oshishi mumkin):
function proratsiya(eskiNarx, yangiNarx, bosh, oxir, hozir) {
const jami = BigInt(oxir - bosh);
const qolgan = BigInt(Math.max(0, oxir - hozir));
const kredit = (BigInt(eskiNarx) * qolgan) / jami;
const yangiQism = (BigInt(yangiNarx) * qolgan) / jami;
return Number(yangiQism - kredit);
}
const d = (s) => new Date(s);
const bosh = d("2026-10-01T00:00Z");
const oxir = d("2026-10-31T00:00Z");
const orta = d("2026-10-16T00:00Z");
console.log(proratsiya(3_000_000, 9_000_000, bosh, oxir, orta));
console.log(proratsiya(9_000_000, 3_000_000, bosh, oxir, orta));
console.log(proratsiya(3_000_000, 9_000_000, bosh, oxir, oxir));
const qisqa = d("2026-10-04T00:00Z");
console.log(proratsiya(3_000_000, 9_000_000, bosh, qisqa,
d("2026-10-02T00:00Z")));Konsolda:
3000000
-3000000
0
4000000- 30 kunlik davrning o'rtasi: farq 60 000 so'mning yarmi — 30 000 so'm undiriladi.
- Teskari (Pro → Oddiy): manfiy — 30 000 so'm qaytariladi (kanon
qaytaramali beradi). Ko'p xizmatlar pulni qaytarmaydi, keyingi hisobga kredit qiladi — bu ham siyosat. - Davr oxirida — 0: almashtirish keyingi davrdan.
- 3 kunlik davrning 2/3 qismi qolgan — 40 000 so'm.
Date ni ayirish millisekund beradi — shuning uchun hisob millisekund aniqligida (kanon kodidagi izohda "sekund" deyilgan, lekin kod millisekundni ishlatadi). BigInt bo'lish kesadi (pastga): farq tiyindan kichik va doim bir tomonga.
Sinov davrida reja almashtirilsa — proratsiya yo'q: pul olinmagan, keyingi to'lov yangi narx bilan (kanon).
6. Bekor qilish: ikki xil
| Usul | Nima bo'ladi | Kanon |
|---|---|---|
| davr oxirida | davrOxiridaBekor = true, xizmat davr oxirigacha; davr_tugadi da tugagan |
huquq davr oxirigacha |
| darhol | bekor, qolgan kunlar uchun qaytarish (faol bo'lsa) |
qaytar + obuna-bekor xati |
Kanon testi: 1-oktyabrda boshlangan oylik obuna 16-oktyabr soat 12:00 da darhol bekor — qaytarish 1 500 000 tiyin (15 000 so'm, oyning yarmi). Bekor qilingan obunaga keyin kech tolov_otdi kelsa (provayder kechikdi) — kanon qaytar amalini beradi: xizmat yo'q, pul ham ushlanmaydi.
Tekshirib ko'ring: Nega
huquqBormifaqat holatga emas,davrOxiriga ham qaraydi?
Javob
faol holat "hozir to'langan davr ichidamiz" degani faqat hozir < davrOxiri bo'lsa. davr_tugadi hodisasi cron bilan keladi va kechikishi mumkin (cron ishlamay qoldi). Holat hali faol, lekin davr tugagan — kanon bunday paytda huquq bermaydi. Holat va vaqt — ikkalasi birga tekshiriladi.
7. Karta tokeni bilan davriy yechish
Obunani avtomatik yechish uchun mijoz kartasi token sifatida saqlanadi (karta raqami emas — Uzum va boshqa mahalliy to'lov tizimlari darsidagi tokenlash). Har davrda server tokenni provayder API'iga beradi va summa yechiladi. O'zbekistonda: Payme Subscribe API (karta tokenlari), Click va Uzum'ning tokenlash API'lari; Stripe'da — Customer va saqlangan to'lov usuli. Kanon bularning hech birini sinamagan — hujjat bo'yicha. Holat mashinasi esa provayderdan mustaqil: unga faqat "o'tdi" yoki "o'tmadi" kerak.
Ikki amaliy qoida (avvalgi darslardan):
- Har
undiramalining idempotentlik kaliti — obuna va davrdan (obuna-7-2026-11), shunda cron ikki marta ishga tushsa ham bitta yechish. - Provayder javobi yo'qolsa —
tolov_otmadideb shoshilmang: avval holatni so'rang ("noma'lum" — eng xavfli holat).
8. Ko'p uchraydigan xatolar
| Xato | Oqibati | To'g'risi |
|---|---|---|
setUTCMonth(+1) |
31-yanvar → 3-mart | oy oxiriga qisish |
davrOxiri = bosh + 30 kun |
fevral va 31 kunli oylar noto'g'ri | kalendar oyi |
| birinchi xatoda bekor | sodiq mijoz yo'qoladi | dunning + imtiyozli davr |
| cheksiz qayta urinish | oylab tekin xizmat | jadval tugasa — bekor |
| proratsiya float'da | tiyin farqlari | BigInt, butun tiyin |
| cron ichida biznes mantiq | testlab bo'lmaydi | sof hodisa(), hozir parametr |
undir kalitsiz |
cron takrori — ikki yechish | obuna + davr kaliti |
9. Mashqlar
Mashqlar kurs/mashqlar/29/33-obuna/ da.
1-mashq (oson): holat qaysi?
- Sinov davridagi obunaning to'lovi o'tmadi.
- Qarzdor obunaning ikkinchi qayta urinishi o'tdi.
- Faol obunaga "davr oxirida bekor" bosildi, davr hali tugamagan.
- Uchinchi qayta urinish ham o'tmadi.
Yechim
qarzdor(urinish 1). 2.faol, urinishlar 0, yangi davr to'lov sanasidan. 3. Hamonfaol(davrOxiridaBekor = true), huquq davr oxirigacha. 4.bekor— bu to'rtinchi muvaffaqiyatsizlik (birinchi xato + uchta qayta urinish).
2-mashq (o'rta): huquqBormi
Kanon qoidasini yozing: sinov va faol — hozir < davrOxiri bo'lsa true; qarzdor — doim true; qolgani — false. To'rt holatni sinang.
Yechim
function huquqBormi(o, hozir) {
if (o.holat === "sinov" || o.holat === "faol") {
return hozir < o.davrOxiri;
}
return o.holat === "qarzdor";
}
const oxir = new Date("2026-11-01T00:00:00Z");
const ob = (holat) => ({ holat, davrOxiri: oxir });
const ichida = new Date("2026-10-20T00:00:00Z");
const keyin = new Date("2026-11-02T00:00:00Z");
console.log(huquqBormi(ob("faol"), ichida));
console.log(huquqBormi(ob("faol"), keyin));
console.log(huquqBormi(ob("qarzdor"), keyin));
console.log(huquqBormi(ob("tugagan"), ichida));Konsolda:
true
false
true
falseIkkinchi qator — cron kechikkan holat: holat faol, lekin davr tugagan — huquq yo'q.
3-mashq (qiyin): langar bilan davrlar
«Oy qo'shish tuzog'i» bo'limida zanjir usuli 31-yanvar → 28-fevral → 28-mart berdi. Hisob langari usulini yozing: davrlar(langar, soni) — oyQosh(langar, i) bilan soni ta davr boshini qaytarsin (YYYY-MM-DD). oyQosh ni darsdan oling. 2026-01-31 dan 4 davr va 2026-03-31 dan 3 davr chiqaring.
Yechim
function oyQosh(d, n) {
const y = d.getUTCFullYear();
const m = d.getUTCMonth() + n;
const kun = d.getUTCDate();
const oxirgi = new Date(Date.UTC(y, m + 1, 0)).getUTCDate();
return new Date(Date.UTC(y, m, Math.min(kun, oxirgi),
d.getUTCHours(), d.getUTCMinutes()));
}
function davrlar(langar, soni) {
return Array.from({ length: soni }, (_, i) =>
oyQosh(langar, i).toISOString().slice(0, 10));
}
console.log(davrlar(new Date("2026-01-31T00:00Z"), 4).join(" "));
console.log(davrlar(new Date("2026-03-31T00:00Z"), 3).join(" "));Konsolda:
2026-01-31 2026-02-28 2026-03-31 2026-04-30
2026-03-31 2026-04-30 2026-05-31Langar bilan mart yana 31 ga qaytdi — mijoz har oy "oxirgi kun" da to'laydi. Zanjirda bitta qisqa oy hamma keyingi sanalarni siljitib yuborardi. Langar usuliga o'tish uchun obuna yozuviga langar va davrRaqami maydonlari kerak bo'ladi — kanon modelida ular yo'q.
10. Loyiha qadami: obuna.ts
To'liq modul (formatlangan nusxa, ma'nosi kanondagidek):
// obuna.ts — obuna (subscription) holat mashinasi: sinov (trial),
// faol, qarzdor (past_due, dunning), bekor, tugagan;
// upgrade/downgrade proratsiyasi; huquq (entitlement).
// Sof funksiyalar: hodisa + vaqt → yangi holat + bajariladigan
// amallar (undirish, xat). Pul olish — provayder (karta tokeni: Payme
// Subscribe API / Click / Uzum — hujjat bo'yicha, SINALMAGAN).
import type { Tiyin } from "./pul.ts";
export type Reja = { kod: string; narx: Tiyin; davr: "oy" | "yil" };
export type Holat =
| "sinov"
| "faol"
| "qarzdor"
| "bekor"
| "tugagan";
export type Obuna = {
holat: Holat;
reja: Reja;
davrBoshi: Date;
davrOxiri: Date;
davrOxiridaBekor: boolean;
urinishlar: number;
keyingiUrinish: Date | null;
};
export type Amal =
| {
tur: "undir";
summa: Tiyin;
sabab: "yangi-davr" | "qayta-urinish" | "proratsiya";
}
| {
tur: "xat";
shablon: "tolov-otmadi" | "obuna-bekor" | "sinov-tugayapti";
}
| { tur: "qaytar"; summa: Tiyin };
export type Hodisa =
| { tur: "davr_tugadi" }
| { tur: "tolov_otdi" }
| { tur: "tolov_otmadi" }
| { tur: "urinish_vaqti" }
| { tur: "bekor_qil"; darhol: boolean }
| { tur: "reja_ozgartir"; reja: Reja };
/**
* Dunning: muvaffaqiyatsiz to'lovdan keyin qayta urinishlar (kun) —
* keyin bekor.
*/
export const DUNNING_KUN = [1, 3, 7] as const;
const KUN = 86_400_000;
/**
* Kalendar oy qo'shish, oy oxiriga qisish: 31-yanvar + 1 oy =
* 28/29-fevral (UTC).
*/
export function oyQosh(d: Date, n: number): Date {
const y = d.getUTCFullYear();
const m = d.getUTCMonth() + n;
const kun = d.getUTCDate();
const oxirgi = new Date(Date.UTC(y, m + 1, 0)).getUTCDate();
return new Date(
Date.UTC(
y,
m,
Math.min(kun, oxirgi),
d.getUTCHours(),
d.getUTCMinutes(),
d.getUTCSeconds(),
d.getUTCMilliseconds(),
),
);
}
const davrOxiri = (bosh: Date, r: Reja) =>
oyQosh(bosh, r.davr === "oy" ? 1 : 12);
export function yangiObuna(
reja: Reja,
hozir: Date,
sinovKun = 14,
): Obuna {
return sinovKun > 0
? {
holat: "sinov",
reja,
davrBoshi: hozir,
davrOxiri: new Date(hozir.getTime() + sinovKun * KUN),
davrOxiridaBekor: false,
urinishlar: 0,
keyingiUrinish: null,
}
: {
holat: "faol",
reja,
davrBoshi: hozir,
davrOxiri: davrOxiri(hozir, reja),
davrOxiridaBekor: false,
urinishlar: 0,
keyingiUrinish: null,
};
}
/**
* Proratsiya: joriy davrning qolgan qismi uchun eski reja krediti va
* yangi reja narxi; natija — hozir undiriladigan (musbat) yoki
* qaytariladigan (manfiy).
* Sekund aniqligida, tiyinga yaxlitlash pastga (mijoz foydasiga emas,
* yo'qolish ham yo'q — farq keyingi davrda emas, shu yerda).
*/
export function proratsiya(
eski: Reja,
yangi: Reja,
bosh: Date,
oxir: Date,
hozir: Date,
): Tiyin {
const jami = oxir.getTime() - bosh.getTime();
const qolgan = Math.max(0, oxir.getTime() - hozir.getTime());
const kredit = Number(
(BigInt(eski.narx) * BigInt(qolgan)) / BigInt(jami),
);
const yangiQism = Number(
(BigInt(yangi.narx) * BigInt(qolgan)) / BigInt(jami),
);
return yangiQism - kredit;
}
export function huquqBormi(o: Obuna, hozir: Date): boolean {
if (o.holat === "sinov" || o.holat === "faol")
return hozir < o.davrOxiri;
// dunning davrida xizmat davom etadi (grace) — biznes qarori
if (o.holat === "qarzdor") return true;
return false;
}
export function hodisa(
o: Obuna,
h: Hodisa,
hozir: Date,
): { obuna: Obuna; amallar: Amal[] } {
const x = { ...o };
switch (h.tur) {
case "davr_tugadi": {
if (
o.holat === "bekor" ||
o.holat === "tugagan" ||
o.holat === "qarzdor"
)
return { obuna: x, amallar: [] };
if (o.davrOxiridaBekor)
return {
obuna: { ...x, holat: "tugagan" },
amallar: [{ tur: "xat", shablon: "obuna-bekor" }],
};
return {
obuna: x,
amallar: [
{ tur: "undir", summa: o.reja.narx, sabab: "yangi-davr" },
],
};
}
case "tolov_otdi": {
if (o.holat === "bekor" || o.holat === "tugagan")
return {
obuna: x,
amallar: [{ tur: "qaytar", summa: o.reja.narx }],
}; // kech kelgan to'lov
const bosh =
o.holat === "qarzdor" || hozir >= o.davrOxiri
? hozir > o.davrOxiri
? hozir
: o.davrOxiri
: o.davrOxiri;
return {
obuna: {
...x,
holat: "faol",
davrBoshi: bosh,
davrOxiri: davrOxiri(bosh, o.reja),
urinishlar: 0,
keyingiUrinish: null,
},
amallar: [],
};
}
case "tolov_otmadi": {
const n = o.urinishlar + 1;
const kun = DUNNING_KUN[n - 1];
if (kun === undefined)
return {
obuna: {
...x,
holat: "bekor",
urinishlar: n,
keyingiUrinish: null,
},
amallar: [{ tur: "xat", shablon: "obuna-bekor" }],
};
return {
obuna: {
...x,
holat: "qarzdor",
urinishlar: n,
keyingiUrinish: new Date(hozir.getTime() + kun * KUN),
},
amallar: [{ tur: "xat", shablon: "tolov-otmadi" }],
};
}
case "urinish_vaqti":
return o.holat === "qarzdor" &&
o.keyingiUrinish &&
hozir >= o.keyingiUrinish
? {
obuna: x,
amallar: [
{
tur: "undir",
summa: o.reja.narx,
sabab: "qayta-urinish",
},
],
}
: { obuna: x, amallar: [] };
case "bekor_qil":
if (h.darhol) {
// darhol bekor: qolgan kunlar uchun qaytarish (siyosat) —
// sinovda pul olinmagan
const qaytar =
o.holat === "faol"
? -proratsiya(
o.reja,
{ ...o.reja, narx: 0 },
o.davrBoshi,
o.davrOxiri,
hozir,
)
: 0;
return {
obuna: { ...x, holat: "bekor", keyingiUrinish: null },
amallar: [
...(qaytar > 0
? [{ tur: "qaytar" as const, summa: qaytar }]
: []),
{ tur: "xat", shablon: "obuna-bekor" },
],
};
}
return {
obuna: { ...x, davrOxiridaBekor: true },
amallar: [],
};
case "reja_ozgartir": {
if (o.holat !== "faol")
// sinovda — keyingi to'lovdan yangi narx
return { obuna: { ...x, reja: h.reja }, amallar: [] };
const farq = proratsiya(
o.reja,
h.reja,
o.davrBoshi,
o.davrOxiri,
hozir,
);
const amallar: Amal[] =
farq > 0
? [{ tur: "undir", summa: farq, sabab: "proratsiya" }]
: farq < 0
? [{ tur: "qaytar", summa: -farq }]
: [];
return { obuna: { ...x, reja: h.reja }, amallar };
}
}
}Unda yuqorida ko'rmaganimiz: yangiObuna (sinov kunlari 0 bo'lsa — darhol faol), davr_tugadi (davr oxirida bekor belgilangan bo'lsa tugagan, aks holda undir … yangi-davr), reja_ozgartir (faol bo'lsa proratsiya, aks holda keyingi to'lovdan).
Testlar — tolov/test/obuna.test.ts, beshta: oyQosh (oy oxiri, kabisa, yil o'tishi, 12 oy), sinov → faol, dunning izi, proratsiya, ikki xil bekor. Biz ishga tushirdik (kanon nusxasi, Node 24.21.0). Beshta ✔ qatoriga va fail 0 ga qarang:
✔ oyQosh: oy oxiriga qisish, kabisa yili (3.1491ms)
✔ sinov → davr tugadi → undirish → to'lov o'tdi → faol (yangi davr) (1.8197ms)
✔ dunning: 1, 3, 7 kun — 4-muvaffaqiyatsizlikda bekor; qarzdorlikda huquq bor; to'lov o'tsa tiklanadi (0.6037ms)
✔ proratsiya: davr o'rtasida oddiy → pro, pro → oddiy (qaytarish) (0.4563ms)
✔ bekor: davr oxirida (huquq oxirigacha) va darhol (qolgan qism qaytariladi) (0.42ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 340.16Nima bo'ldi? Beshala test o'tdi va hammasi millisekundning bir qismida tugadi (har qatordagi 0.42ms kabi). Bu "holat + amallar" naqshining natijasi: bazaga, tarmoqqa va haqiqiy soatga murojaat yo'q.
Commit xabari:
M='feat(tolov): obuna holat mashinasi — dunning,'
M+=' proratsiya, oyQosh, huquq'
git commit -m "$M"11. Real ishda
- Stripe Billing, Paddle, Chargebee — obuna holat mashinasini xizmat sifatida beradi (Stripe'da
past_due,canceled,trialingholatlari, "Smart Retries"). Kichik loyihada o'zingiz yozasiz, katta xalqaro SaaS'da ko'pincha shularni ishlatadi. - Sinov tugashidan oldin xat — kanon
Amalturidasinov-tugayaptishabloni bor, lekin uni chiqaradigan hodisa hali yo'q: o'zingiz qo'shishingiz mumkin bo'lgan joy. - Qonun: ko'p mamlakatlarda obunani bekor qilish oson bo'lishi va avtomatik yangilanish oldidan ogohlantirish talab qilinadi — mahsulot qarori yuridik maslahat bilan.
- Intervyuda: "31-yanvarga bir oy qo'shsangiz nima chiqadi?", "Upgrade proratsiyasini qanday hisoblaysiz?"
Xulosa
- Obuna — holat mashinasi: sinov, faol, qarzdor, bekor, tugagan; qarzdorlikda xizmat davom etadi (kanon qarori).
- Kanon naqshi:
hodisa(obuna, hodisa, hozir)→ yangi holat + amallar; funksiya sof, testlar soatsiz. - Oy qo'shish — oy oxiriga qisish; zanjir sanani siljitadi, hisob langari saqlaydi.
- Dunning: 1, 3, 7 kun, keyin bekor; qarzdorlikdan keyin yangi davr to'lov sanasidan.
- Proratsiya — qolgan vaqt ulushi, millisekund aniqligida,
BigInt; teskari yo'nalishda qaytarish. - Karta tokeni bilan yechish — provayder API'si (sinalmagan); har
undir— idempotentlik kaliti bilan.
Keyingi dars: Kiruvchi webhook'larni qabul qilish — Standard Webhooks imzosi, vaqt oynasi, takrorlar va "tez 200 + navbat".
Manbalar
- Stripe obuna holatlari: https://docs.stripe.com/billing/subscriptions/overview
- Stripe proratsiya: https://docs.stripe.com/billing/subscriptions/prorations
Date.UTC: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC- Payme Subscribe API (karta tokenlari): https://developer.help.paycom.uz — sinalmagan
- Kanon (IlmHamroh, 2026-10-27):
tolov/src/obuna.ts, Node 24.21.0; karta tokeni bilan yechish — sinalmagan. Testlar — biz ishga tushirdik (2026-10-28).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!