Recurring Payments Example
Complete example of implementing recurring payments with saved cards.
Overview
This example demonstrates:
- Saving customer cards
- Processing recurring payments
- Managing subscriptions
- Handling payment failures
Prerequisites
bash
bun add @joyida/paymeDatabase Schema
sql
-- Users table
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
);
-- Saved cards table
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)
);
-- Subscriptions table
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)
);
-- Payments table
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)
);
-- Indexes
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);Complete Implementation
typescript
import { PaymeSubscribe, RECEIPT_STATES, PaymeError, ValidationError } from '@joyida/payme';
import { Database } from 'bun:sqlite';
// Initialize clients
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');
// ==================== CARD MANAGEMENT ====================
/**
* Save customer card
*/
async function saveCustomerCard(
userId: number,
cardNumber: string,
cardExpiry: string
): Promise<string> {
console.log(`💳 Saving card for user ${userId}...`);
try {
// 1. Create card token
const created = await subscribeClient.cardsCreate({
card: { number: cardNumber, expire: cardExpiry },
save: true,
account: { user_id: userId.toString() }
});
console.log('✅ Card token created:', created.card.token);
// 2. Request SMS verification
const sms = await subscribeClient.cardsGetVerifyCode({
token: created.card.token
});
console.log(`📱 SMS sent to ${sms.phone}`);
// 3. Get SMS code from user (your implementation)
const smsCode = await promptUserForSMSCode();
// 4. Verify card
const verified = await subscribeClient.cardsVerify({
token: created.card.token,
code: smsCode
});
if (!verified.card.verify) {
throw new Error('Card verification failed');
}
console.log('✅ Card verified');
// 5. Save to database
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 // Set as default card
]);
console.log('✅ Card saved to database');
return verified.card.token;
} catch (error) {
if (error instanceof PaymeError) {
console.error('Payme error:', error.code, error.message);
}
throw error;
}
}
/**
* Get user's default card
*/
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);
}
/**
* Get all user cards
*/
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);
}
/**
* Remove saved card
*/
async function removeCard(userId: number, cardToken: string) {
console.log(`🗑️ Removing card ${cardToken}...`);
try {
// Remove from Payme
await subscribeServer.cardsRemove({
token: cardToken
});
// Remove from database
db.run(`
DELETE FROM saved_cards
WHERE user_id = ? AND token = ?
`, [userId, cardToken]);
console.log('✅ Card removed');
} catch (error) {
console.error('Failed to remove card:', error);
throw error;
}
}
// ==================== SUBSCRIPTION MANAGEMENT ====================
/**
* Create subscription
*/
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;
}
/**
* Get active subscriptions due for billing
*/
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);
}
/**
* Update subscription billing date
*/
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]);
}
/**
* Cancel subscription
*/
function cancelSubscription(subscriptionId: number) {
db.run(`
UPDATE subscriptions
SET status = 'cancelled', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [subscriptionId]);
}
// ==================== PAYMENT PROCESSING ====================
/**
* Process recurring payment
*/
async function processRecurringPayment(subscription: any) {
console.log(`\n💰 Processing payment for subscription ${subscription.id}...`);
console.log(` User: ${subscription.name} (${subscription.email})`);
console.log(` Amount: ${subscription.amount} tiyin`);
try {
// 1. Check card status
console.log('🔍 Checking card status...');
const cardCheck = await subscribeServer.cardsCheck({
token: subscription.card_token
});
if (!cardCheck.card.verify) {
throw new Error('Card not verified');
}
if (!cardCheck.card.recurrent) {
throw new Error('Card does not support recurring payments');
}
console.log('✅ Card is valid');
// 2. Create receipt
console.log('📝 Creating receipt...');
const receipt = await subscribeServer.receiptsCreate({
amount: subscription.amount,
account: {
subscription_id: subscription.id.toString(),
user_id: subscription.user_id.toString()
},
description: `Subscription payment - ${subscription.plan_id}`
});
console.log('✅ Receipt created:', receipt.receipt._id);
// 3. Save payment record
const paymentId = db.run(`
INSERT INTO payments
(subscription_id, receipt_id, amount, status)
VALUES (?, ?, ?, ?)
`, [
subscription.id,
receipt.receipt._id,
subscription.amount,
'pending'
]).lastInsertRowid;
// 4. Pay with card token
console.log('💳 Processing payment...');
const payment = await subscribeServer.receiptsPay({
id: receipt.receipt._id,
token: subscription.card_token
});
if (payment.receipt.state === RECEIPT_STATES.PAID) {
console.log('✅ Payment successful!');
// 5. Update payment record
db.run(`
UPDATE payments
SET status = 'paid', paid_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [paymentId]);
// 6. Update next billing date
updateSubscriptionBillingDate(subscription.id);
// 7. Send confirmation email (your implementation)
await sendPaymentConfirmation(subscription.email, {
amount: subscription.amount,
receiptId: receipt.receipt._id
});
return {
success: true,
paymentId,
receiptId: receipt.receipt._id
};
} else {
throw new Error('Payment failed');
}
} catch (error) {
console.error('❌ Payment failed:', error.message);
// Update payment status
db.run(`
UPDATE payments
SET status = 'failed'
WHERE subscription_id = ? AND status = 'pending'
`, [subscription.id]);
// Send failure notification
await sendPaymentFailureNotification(subscription.email, {
reason: error.message
});
// Suspend subscription after 3 failed attempts
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('⚠️ Suspending subscription after 3 failed attempts');
db.run(`
UPDATE subscriptions
SET status = 'suspended', updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`, [subscription.id]);
}
throw error;
}
}
/**
* Process all due subscriptions
*/
async function processDueSubscriptions() {
console.log('🔄 Processing due subscriptions...\n');
const subscriptions = getSubscriptionsDueForBilling();
console.log(`Found ${subscriptions.length} subscriptions due for billing`);
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📊 Processing complete:');
console.log(` ✅ Successful: ${results.successful}`);
console.log(` ❌ Failed: ${results.failed}`);
return results;
}
// ==================== HELPER FUNCTIONS ====================
function promptUserForSMSCode(): Promise<string> {
// Your implementation to get SMS code from user
// For example, via web form, mobile app, etc.
return Promise.resolve('666666'); // Test code
}
async function sendPaymentConfirmation(email: string, data: any) {
// Your implementation to send email
console.log(`📧 Sending confirmation to ${email}`);
}
async function sendPaymentFailureNotification(email: string, data: any) {
// Your implementation to send email
console.log(`📧 Sending failure notification to ${email}`);
}
// ==================== EXAMPLE USAGE ====================
async function main() {
// 1. Create test user
const userId = db.run(`
INSERT INTO users (email, name) VALUES (?, ?)
`, ['user@example.com', 'Test User']).lastInsertRowid;
console.log(`Created user: ${userId}`);
// 2. Save card
const cardToken = await saveCustomerCard(
userId,
'8600069195406311',
'0399'
);
// 3. Create subscription
const subscriptionId = createSubscription(
userId,
'premium-monthly',
500000, // 5000 UZS
cardToken
);
console.log(`\n✅ Subscription created: ${subscriptionId}`);
// 4. Process payment immediately (for testing)
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. Schedule daily billing job
console.log('\n⏰ Setting up daily billing job...');
setInterval(async () => {
await processDueSubscriptions();
}, 24 * 60 * 60 * 1000); // Run daily
}
// Run example
main().catch(console.error);Cron Job for Billing
typescript
// billing-cron.ts
import { processDueSubscriptions } from './recurring-payments';
async function runBillingJob() {
console.log(`\n🕐 Running billing job at ${new Date().toISOString()}`);
try {
const results = await processDueSubscriptions();
// Log results
console.log('Billing job completed:', results);
// Send admin notification if there were failures
if (results.failed > 0) {
await sendAdminNotification({
subject: 'Billing Job - Failed Payments',
failed: results.failed,
errors: results.errors
});
}
} catch (error) {
console.error('Billing job failed:', error);
await sendAdminNotification({
subject: 'Billing Job - Critical Error',
error: error.message
});
}
}
// Run immediately
runBillingJob();
// Schedule to run daily at 2 AM
const schedule = require('node-cron');
schedule.schedule('0 2 * * *', runBillingJob);Best Practices
✅ Do's
- Verify cards before saving
- Check card status before payment
- Handle payment failures gracefully
- Send payment confirmations
- Suspend after multiple failures
- Keep audit trail
- Run billing jobs at off-peak hours
❌ Don'ts
- Don't charge without verification
- Don't ignore failed payments
- Don't retry immediately
- Don't lose payment records
- Don't forget to notify users
Next Steps
- Learn about Receipt Management
- Explore Database Integration
- Check Error Handling
- See Testing Guide