Pulni registr sifatida modellashtirish (va nima uchun billing-ni ikki marta qayta yozishim kerak bo'ldi)
Nashr etilgan 18-sentabr, 2025 Β· 5 daqiqa o'qish
Birinchi versiya qanday ko'ringan edi
Halol aytganda, uyatli edi. Ijarachi yozuvida subscription_active mantiqiy qiymati. Oyning oxirida to'lov kelmagan bo'lsa uni false-ga o'zgartiradigan cron ishi.
Birinchi ikki hafta yaxshi ishladi. Keyin qo'llab-quvvatlash xabarlari boshlandi: "To'ldirdim lekin hisobim hali ham bloklangan." "Nima uchun ikki marta to'lov oldingiz?"
Asosiy muammo oddiy edi: men pulni holat (faol / nofaol) sifatida ko'rib chiqardim, aslida pul voqealar tarixi. Mantiqiy qiymatdan nima bo'lganini qayta qurib bo'lmaydi.
Pul β bu registr
Yechim: qoldiqlarni saqlashni to'xtatib, harakatlarni saqlashni boshlash.
Pul tizimga har safar tegsa, wallet_transactions-da qatorga aylanadi. Qatorda tur, imzolangan miqdor (ijobiy = kredit, manfiy = debet), havola ID va vaqt belgisi bor.
Men ishlatiladigan turlar:
top_upβ Payme to'lovi hamyonga kreditlangansubscription_feeβ oylik avtomatik ayirishrefundβ qo'llab-quvvatdan qo'lda kreditadjustmentβ kamdan-kam qo'lda tuzatish
Istalgan vaqtdagi qoldiq shunchaki SUM so'rovidir:
SELECT SUM(amount) AS balance FROM wallet_transactions WHERE tenant_id = $1;
Bu hammasi. Bitta so'rov, har doim aniq, sinxronlashdan chiqib keta olmaydi β chunki sinxronlash kerak bo'lgan narsa yo'q.
Payme integratsiyasi
Payme Callback-asosidagi oqimdan foydalanadi. Siz JSON-RPC endpoint-ni ochasiz, Payme to'lov holati o'zgarganda uni chaqiradi.
To'g'ri bajarish kerak bo'lgan bitta narsa: idempotentlik. Payme callbacklarni qayta urinadi.
PerformTransaction uchun ishlovchim:
payme_transaction_id = Xbilantop_upqatori mavjudligini tekshiring- Ha bo'lsa β muvaffaqiyat qaytaring, hech narsa yozmang
- Yo'q bo'lsa β DB tranzaksiyasida qatorni kiriting, muvaffaqiyat qaytaring
async performTransaction(params: PaymeParams) {
return this.db.$transaction(async (tx) => {
const existing = await tx.walletTransaction.findFirst({
where: { reference: params.id, type: 'top_up' }
});
if (existing) return { result: { transaction: existing.id } };
const txn = await tx.walletTransaction.create({
data: {
tenant_id: params.account.tenant_id,
type: 'top_up',
amount: params.amount / 100,
reference: params.id,
}
});
return { result: { transaction: txn.id } };
});
}Obuna avtomatik ayirish
Har oyning birinchisi NestJS cron har faol ijarachini to'lashga harakat qiladi:
- Joriy qoldiqni o'qing (SUM)
- Qoldiq β₯ to'lov bo'lsa: manfiy
subscription_feetranzaksiyasini kiriting - Qoldiq < to'lov bo'lsa:
payment_overdue = trueqo'ying, bildirishnomani navbatga qo'ying
Qiyin yo'l bilan o'rgangan chekka holatlar
Bir vaqtda yozishlar. Agar to'ldirish va ayirish ikkalasi ham bir vaqtda qoldiqni o'qib, u 0 bo'lsa, ikkalasi ham salbiy qoldiq yaratuvchi tarzda davom etishi mumkin. Yechim: atomik tekshirish-debet kerak bo'lganda SELECT FOR UPDATE.
Vaqt mintaqasi. "Oyning birinchisi" Toshkentda (UTC+5) oyning birinchisini anglatadi. Bu xatoni bir marta qildim.
Qisman ish muvaffaqiyatsizligi. Cron 500 ta ijarachidan 400 tasini to'lov qilib qulasa, sodda restart ba'zi ijarachilardan ikki marta to'lov oladi. Idempotent paketlarda qayta ishlang.
Billing zerikarli, keyin emas. Pulni voqealar sifatida modellashtiring, idempotentlik tekshiruvlarini baxtli yo'ldan oldin yozing.