Skip to content

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/payme

Ma'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

  1. Saqlashdan oldin kartalarni tasdiqlang
  2. To'lovdan oldin karta holatini tekshiring
  3. To'lov muvaffaqiyatsizliklarini yoqimli ishlash
  4. To'lov tasdiqlashlarini yuboring
  5. Bir nechta muvaffaqiyatsizliklardan keyin to'xtating
  6. Audit izini saqlang
  7. To'lov ishlarini kam yuk paytlarida ishga tushiring

❌ Kerakli emaslar

  1. Tasdiqlashsiz to'lamang
  2. Muvaffaqiyatsiz to'lovlarni e'tiborsiz qoldirmang
  3. Darhol qayta urinmang
  4. To'lov yozuvlarini yo'qotmang
  5. Foydalanuvchilarni xabardor qilishni unutmang

Keyingi Qadamlar

MIT Lizenziyasi ostida chiqarilgan.