Takroriy To'lovlar Misoli
Saqlangan kartalar bilan takroriy to'lovlarni implementatsiya qilishning to'liq misoli.
Umumiy Ko'rinish
Ushbu misol quyidagilarni ko'rsatadi:
- Mijoz kartalarini saqlash
- Takroriy to'lovlarni qayta ishlash
- Obunalarni boshqarish
- To'lov muvaffaqiyatsizliklarini ishlash
Talablar
bash
bun add @joyida/paymeMa'lumotlar BazasiSxemasi
sql
-- Foydalanuvchilar jadvali
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Saqlangan kartalar jadvali
CREATE TABLE IF NOT EXISTS saved_cards (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
token TEXT UNIQUE NOT NULL,
card_number TEXT NOT NULL,
card_expire TEXT NOT NULL,
verified BOOLEAN DEFAULT FALSE,
is_default BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- Obunalar jadvali
CREATE TABLE IF NOT EXISTS subscriptions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
plan_id TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT DEFAULT 'active',
card_token TEXT NOT NULL,
next_billing_date DATE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- To'lovlar jadvali
CREATE TABLE IF NOT EXISTS payments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
subscription_id INTEGER NOT NULL,
receipt_id TEXT NOT NULL,
amount INTEGER NOT NULL,
status TEXT DEFAULT 'pending',
paid_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (subscription_id) REFERENCES subscriptions(id)
);
-- Indekslar
CREATE INDEX idx_saved_cards_user_id ON saved_cards(user_id);
CREATE INDEX idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX idx_subscriptions_next_billing_date ON subscriptions(next_billing_date);
CREATE INDEX idx_payments_subscription_id ON payments(subscription_id);To'liq Implementatsiya
typescript
import { PaymeSubscribe, RECEIPT_STATES, PaymeError, ValidationError } from '@joyida/payme';
import { Database } from 'bun:sqlite';
// Klientlarni ishga tushirish
const subscribeClient = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!
}, 'client');
const subscribeServer = new PaymeSubscribe({
merchantId: process.env.PAYME_MERCHANT_ID!,
password: process.env.PAYME_PASSWORD!
}, 'server');
const db = new Database('app.db');
// ==================== KARTA BOSHQRUVI ====================
/**
* Mijoz kartasini saqlash
*/
async function saveCustomerCard(
userId: number,
cardNumber: string,
cardExpiry: string
): Promise<string> {
console.log(`💳 Foydalanuvchi ${userId} uchun kartani saqlash...`);
try {
// 1. Karta tokenini yaratish
const created = await subscribeClient.cardsCreate({
card: { number: cardNumber, expire: cardExpiry },
save: true,
account: { user_id: userId.toString() }
});
console.log('✅ Karta tokeni yaratildi:', created.card.token);
// 2. SMS tasdiqlash so'rash
const sms = await subscribeClient.cardsGetVerifyCode({
token: created.card.token
});
console.log(`📱 SMS yuborildi ${sms.phone}`);
// 3. SMS kodini foydalanuvchidan olish (sizning implementatsiyangiz)
const smsCode = await promptUserForSMSCode();
// 4. Kartani tasdiqlash
const verified = await subscribeClient.cardsVerify({
token: created.card.token,
code: smsCode
});
if (!verified.card.verify) {
throw new Error('Karta tasdiqlash muvaffaqiyatsiz');
}
console.log('✅ Karta tasdiqlandi');
// 5. Ma'lumotlar bazasiga saqlash
db.run(`
INSERT INTO saved_cards
(user_id, token, card_number, card_expire, verified, is_default)
VALUES (?, ?, ?, ?, ?, ?)
`, [
userId,
verified.card.token,
verified.card.number,
verified.card.expire,
true,
true // Standart kartasifatida belgilash
]);
console.log('✅ Karta ma\'lumotlar bazasiga saqlandi');
return verified.card.token;
} catch (error) {
if (error instanceof PaymeError) {
console.error('Payme xatosi:', error.code, error.message);
}
throw error;
}
}
/**
* Foydalanuvchining standart kartasini olish
*/
function getUserDefaultCard(userId: number) {
return db.query(`
SELECT * FROM saved_cards
WHERE user_id = ? AND verified = TRUE AND is_default = TRUE
LIMIT 1
`).get(userId);
}
/**
* Foydalanuvchining barcha kartalarini olish
*/
function getUserCards(userId: number) {
return db.query(`
SELECT * FROM saved_cards
WHERE user_id = ? AND verified = TRUE
ORDER BY is_default DESC, created_at DESC
`).all(userId);
}
/**
* Saqlangan kartani o'chirish
*/
async function removeCard(userId: number, cardToken: string) {
console.log(`🗑️ Karta ${cardToken} ni o'chirish...`);
try {
// Payme dan o'chirish
await subscribeServer.cardsRemove({
token: cardToken
});
// Ma'lumotlar bazasidan o'chirish
db.run(`
DELETE FROM saved_cards
WHERE user_id = ? AND token = ?
`, [userId, cardToken]);
console.log('✅ Karta o\'chirildi');
} catch (error) {
console.error('Kartani o\'chirish muvaffaqiyatsiz:', error);
throw error;
}
}
// ==================== OBUNA BOSHQRUVI ====================
/**
* Obuna yaratish
*/
function createSubscription(
userId: number,
planId: string,
amount: number,
cardToken: string
) {
const nextBillingDate = new Date();
nextBillingDate.setMonth(nextBillingDate.getMonth() + 1);
const result = db.run(`
INSERT INTO subscriptions
(user_id, plan_id, amount, status, card_token, next_billing_date)
VALUES (?, ?, ?, ?, ?, ?)
`, [
userId,
planId,
amount,
'active',
cardToken,
nextBillingDate.toISOString().split('T')[0]
]);
return result.lastInsertRowid;
}
/**
* To'lov uchun yetuk obunalarni olish
*/
function getSubscriptionsDueForBilling() {
const today = new Date().toISOString().split('T')[0];
return db.query(`
SELECT s.*, u.email, u.name
FROM subscriptions s
JOIN users u ON s.user_id = u.id
WHERE s.status = 'active' AND s.next_billing_date <= ?
`).all(today);
}
/**
* Obuna to'lov sanasini yangilash
*/
function updateSubscriptionBillingDate(subscriptionId: number) {
const nextBillingDate = new Date();
nextBillingDate.setMonth(nextBillingDate.getMonth() + 1);
db.run(`
UPDATE subscriptions
SET next_billing_date = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [nextBillingDate.toISOString().split('T')[0], subscriptionId]);
}
/**
* Obunani bekor qilish
*/
function cancelSubscription(subscriptionId: number) {
db.run(`
UPDATE subscriptions
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [subscriptionId]);
}
// ==================== TO'LOVNI QAYTA ISHLASH ====================
/**
* Takroriy to'lovni qayta ishlash
*/
async function processRecurringPayment(subscription: any) {
console.log(`\n💰 Obuna ${subscription.id} uchun to'lovni qayta ishlash...`);
console.log(` Foydalanuvchi: ${subscription.name} (${subscription.email})`);
console.log(` Suma: ${subscription.amount} tiyin`);
try {
// 1. Karta holatini tekshirish
console.log('🔍 Karta holatini tekshirish...');
const cardCheck = await subscribeServer.cardsCheck({
token: subscription.card_token
});
if (!cardCheck.card.verify) {
throw new Error('Karta tasdiqlanmagan');
}
if (!cardCheck.card.recurrent) {
throw new Error('Karta takroriy to'lovlarni qo'llab-quvvatlamaydi');
}
console.log('✅ Karta to\'g\'ri');
// 2. Chek yaratish
console.log('📝 Chek yaratish...');
const receipt = await subscribeServer.receiptsCreate({
amount: subscription.amount,
account: {
subscription_id: subscription.id.toString(),
user_id: subscription.user_id.toString()
},
description: `Obuna to\'lovi - ${subscription.plan_id}`
});
console.log('✅ Chek yaratildi:', receipt.receipt._id);
// 3. To'lov yozuvini saqlash
const paymentId = db.run(`
INSERT INTO payments
(subscription_id, receipt_id, amount, status)
VALUES (?, ?, ?, ?)
`, [
subscription.id,
receipt.receipt._id,
subscription.amount,
'pending'
]).lastInsertRowid;
// 4. Karta tokenni bilan to'lash
console.log('💳 To\'lovni qayta ishlash...');
const payment = await subscribeServer.receiptsPay({
id: receipt.receipt._id,
token: subscription.card_token
});
if (payment.receipt.state === RECEIPT_STATES.PAID) {
console.log('✅ To\'lov muvaffaqiyatli!');
// 5. To'lov yozuvini yangilash
db.run(`
UPDATE payments
SET status = 'paid', paid_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [paymentId]);
// 6. Keyingi to'lov sanasini yangilash
updateSubscriptionBillingDate(subscription.id);
// 7. Tasdiqlash email yuborish (sizning implementatsiyangiz)
await sendPaymentConfirmation(subscription.email, {
amount: subscription.amount,
receiptId: receipt.receipt._id
});
return {
success: true,
paymentId,
receiptId: receipt.receipt._id
};
} else {
throw new Error('To\'lov muvaffaqiyatsiz');
}
} catch (error) {
console.error('❌ To\'lov muvaffaqiyatsiz:', error.message);
// To'lov statusini yangilash
db.run(`
UPDATE payments
SET status = 'failed'
WHERE subscription_id = ? AND status = 'pending'
`, [subscription.id]);
// Muvaffaqiyatsizlik xabari yuborish
await sendPaymentFailureNotification(subscription.email, {
reason: error.message
});
// 3 ta muvaffaqiyatsiz urinishdan keyin obunani to'xtatish
const failedPayments = db.query(`
SELECT COUNT(*) as count FROM payments
WHERE subscription_id = ? AND status = 'failed'
`).get(subscription.id);
if (failedPayments.count >= 3) {
console.log('⚠️ 3 ta muvaffaqiyatsiz urinishdan keyin obunani to\'xtatish');
db.run(`
UPDATE subscriptions
SET status = 'suspended', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [subscription.id]);
}
throw error;
}
}
/**
* Barcha yetuk obunalarni qayta ishlash
*/
async function processDueSubscriptions() {
console.log('🔄 Yetuk obunalarni qayta ishlash...\n');
const subscriptions = getSubscriptionsDueForBilling();
console.log(`To'lov uchun yetuk ${subscriptions.length} ta obuna topildi`);
const results = {
successful: 0,
failed: 0,
errors: []
};
for (const subscription of subscriptions) {
try {
await processRecurringPayment(subscription);
results.successful++;
} catch (error) {
results.failed++;
results.errors.push({
subscriptionId: subscription.id,
error: error.message
});
}
}
console.log('\n📊 Qayta ishlash yakunlandi:');
console.log(` ✅ Muvaffaqiyatli: ${results.successful}`);
console.log(` ❌ Muvaffaqiyatsiz: ${results.failed}`);
return results;
}
// ==================== YORDAMCHI FUNKTSIYALAR ====================
function promptUserForSMSCode(): Promise<string> {
// SMS kodini foydalanuvchidan olish uchun sizning implementatsiyangiz
// Masalan, veb forma, mobil ilova va boshqalar orqali.
return Promise.resolve('666666'); // Test kodi
}
async function sendPaymentConfirmation(email: string, data: any) {
// Email yuborish uchun sizning implementatsiyangiz
console.log(`📧 Tasdiqlash xabari yuborish ${email}`);
}
async function sendPaymentFailureNotification(email: string, data: any) {
// Email yuborish uchun sizning implementatsiyangiz
console.log(`📧 Muvaffaqiyatsizlik xabari yuborish ${email}`);
}
// ==================== MISOL FOYDALANISH ====================
async function main() {
// 1. Test foydalanuvchi yaratish
const userId = db.run(`
INSERT INTO users (email, name) VALUES (?, ?)
`, ['user@example.com', 'Test User']).lastInsertRowid;
console.log(`Foydalanuvchi yaratildi: ${userId}`);
// 2. Karta saqlash
const cardToken = await saveCustomerCard(
userId,
'8600069195406311',
'0399'
);
// 3. Obuna yaratish
const subscriptionId = createSubscription(
userId,
'premium-monthly',
500000, // 5000 so'm
cardToken
);
console.log(`\n✅ Obuna yaratildi: ${subscriptionId}`);
// 4. To'lovni darhol qayta ishlash (test uchun)
const subscription = db.query(`
SELECT s.*, u.email, u.name
FROM subscriptions s
JOIN users u ON s.user_id = u.id
WHERE s.id = ?
`).get(subscriptionId);
await processRecurringPayment(subscription);
// 5. Kunlik to'lov ishlarini rejalashtirish
console.log('\n⏰ Kunlik to\'lov ishlarini sozlash...');
setInterval(async () => {
await processDueSubscriptions();
}, 24 * 60 * 60 * 1000); // Kunlik ishga tushirish
}
// Misolni ishga tushirish
main().catch(console.error);To'lov uchun Cron Ish
typescript
// billing-cron.ts
import { processDueSubscriptions } from './recurring-payments';
async function runBillingJob() {
console.log(`\n🕐 To'lov ishini ishga tushirish ${new Date().toISOString()}`);
try {
const results = await processDueSubscriptions();
// Natijalarni jurnalga yozish
console.log('To\'lov ishi yakunlandi:', results);
// Agar muvaffaqiyatsizliklar bo'lsa, admin xabari yuborish
if (results.failed > 0) {
await sendAdminNotification({
subject: 'To\'lov Ishi - Muvaffaqiyatsiz To\'lovlar',
failed: results.failed,
errors: results.errors
});
}
} catch (error) {
console.error('To\'lov ishi muvaffaqiyatsiz:', error);
await sendAdminNotification({
subject: 'To\'lov Ishi - Kritik Xato',
error: error.message
});
}
}
// Darhol ishga tushirish
runBillingJob();
// Kuniga 2:00 da ishga tushirish uchun rejalashtirish
const schedule = require('node-cron');
schedule.schedule('0 2 * * *', runBillingJob);Eng Yaxshi Amaliyotlar
✅ Kerakli ishlar
- Saqlashdan oldin kartalarni tasdiqlang
- To'lovdan oldin karta holatini tekshiring
- To'lov muvaffaqiyatsizliklarini yoqimli ishlash
- To'lov tasdiqlashlarini yuboring
- Bir nechta muvaffaqiyatsizliklardan keyin to'xtating
- Audit izini saqlang
- To'lov ishlarini kam yuk paytlarida ishga tushiring
❌ Kerakli emaslar
- Tasdiqlashsiz to'lamang
- Muvaffaqiyatsiz to'lovlarni e'tiborsiz qoldirmang
- Darhol qayta urinmang
- To'lov yozuvlarini yo'qotmang
- Foydalanuvchilarni xabardor qilishni unutmang
Keyingi Qadamlar
- Chek Boshqaruvi haqida bilib oling
- Ma'lumotlar Bazasi Integratsiyasi ni o'rganing
- Xatolarni Ishlash ni tekshiring
- Testlash Qo'llanmasini ko'ring