Building SaaS Website #11: Payment Integration
In this installment, we'll explore how to integrate payment systems into our TotalGPT application. By adding payment capabilities, we'll transform our website from a simple landing page into a fully functional SaaS platform capable of handling subscriptions and processing payments.
Understanding Payment Processors
We'll be implementing two different payment processors, each serving specific geographical needs:
Ligdicash
- Specializes in African payment methods
- Supports mobile money services (Orange Money, MTN Mobile Money, Moov Money)
- Handles local credit card payments
- Perfect for West African markets
- Processes payments in XOF (West African CFA franc)
- Popular in countries like Burkina Faso, Niger, Togo, CΓ΄te d'Ivoire, and Senegal
Stripe
- Global payment solution
- Primarily handles credit card payments
- Available in 40+ countries
- Supports multiple currencies
- Provides extensive developer tools
- Ideal for international customers
- Known for robust security and ease of use
Why Two Payment Processors?
Having multiple payment processors helps us:
- Reach more customers by supporting their preferred payment methods
- Provide backup payment options if one system is unavailable
- Offer local payment methods where they're most relevant
- Handle different currencies efficiently
What We'll Cover
In this guide, we'll walk through:
- Setting up both payment processors
- Creating payment workflows
- Handling successful and failed payments
- Managing subscriptions
- Sending payment notifications via WhatsApp
Let's begin with The Payment Flow...
The Payment Flow - A User's Journey
Step 1: Choosing a Plan
When a user visits TotalGPT, they:
- Browse available plans on the pricing page
- Click on their preferred plan
- Get taken to the plan details page
Step 2: Starting Payment
On the plan details page, the user:
- Enters their phone number
- Selects subscription duration (1 month, 3 months, etc.)
- Clicks the "Pay Now" button
Step 3: Payment Process
After clicking "Pay Now":
- User receives a WhatsApp message with their payment link
- Gets redirected to the payment gateway (Ligdicash or Stripe)
- Completes payment using their preferred method (mobile money or card)
Step 4: Completion
After payment:
- User gets redirected back to TotalGPT website
- Receives confirmation on WhatsApp if payment is successful
- Their subscription is automatically activated
Step 5: If Something Goes Wrong
If payment fails:
- User sees a failure message
- Gets option to try again
- Can contact support if needed
Behind the Scenes
Here's what happens in the background:
- TotalGPT creates a payment invoice
- Sends it to the payment provider (Ligdicash/Stripe)
- Records the order in the database
- Waits for payment confirmation
- Updates user's subscription when payment succeeds
- Sends notifications via WhatsApp
Important Features
- WhatsApp notifications keep users informed
- Automatic subscription activation
- Support for different subscription durations
- Secure payment processing
- Error handling if something goes wrong
Let's start with Ligdicash integration.
Setting Up Ligdicash
1. Create Definition File
First, let's create a definition file for Ligdicash integration:
// /definitions/ligdicash.js
// Import the Ligdicash package
const Ligdicash = require('ligdicash');
// Initialize Ligdicash client with configuration
// CONF is a global object containing our application settings
var client = new Ligdicash({
apiKey: CONF.ligdicash_apikey, // Your Ligdicash API key
authToken: CONF.ligdicash_apisecret, // Your Ligdicash secret key
platform: 'live', // 'live' for production, 'test' for testing
});
// Store the client instance in MAIN for global access
// MAIN is a global object for storing application-wide data
MAIN.ligdicash = client;
2. Controller Setup
Next, let's implement the payment routes in our controller:
// /controllers/default.js
exports.install = function() {
// Route for initiating checkout
ROUTE('POST /checkout/', checkout);
// Route for handling payment callbacks
ROUTE('POST /callback/', callback);
};
// Checkout handler function
async function checkout() {
var controller = this;
// Get data from request body
var data = controller.body;
// Format phone number for WhatsApp
var chatid = data.phone + '@c.us';
// Fetch user and plan details from database
var user = await DATA.read('nosql/account').id(chatid).promise();
var plan = await DATA.read('nosql/plans').id(data.plan).promise();
// Validate user exists
if (!user) {
// Redirect to pricing page with error if user not found
controller.redirect('/pricing/' + data.plan + '?plan=' + data.plan + '&error=404');
return;
}
// Handle free plan subscription
if (plan.id == 'free') {
// Send WhatsApp confirmation for free plan
await FUNC.sendWhatsAppMessage(chatid,
'@(Your free subscription has been activated successfully. For a better experience with Tass, choose a paid subscription: Click: https://totalgpt.com/pricing)');
controller.redirect('/success');
return;
}
// Create Ligdicash invoice
let invoice = MAIN.ligdicash.Invoice({
currency: "xof", // Currency code
description: plan.name, // Plan description
customer_firstname: data.phone, // Customer phone
customer_lastname: user.name || chatid, // Customer name
customer_email: chatid, // Customer email/ID
store_name: "TotalGPT", // Store name
store_website_url: "https://chat.totalgpt.com", // Store URL
});
// Add subscription item to invoice
invoice.addItem({
name: plan.name,
description: 'subscription for ' + data.duration + ' months',
quantity: data.duration,
unit_price: plan.price2
});
// Create order record
var order = {
id: GUID(35), // Generate unique ID
userid: user.id, // User ID
planid: plan.id, // Plan ID
price: plan.price, // Plan price
duration: data.duration, // Subscription duration
amount: data.duration * plan.price, // Total amount
status: 'pending', // Initial status
date: NOW.format('dd-MM-yyyy'), // Current date
time: NOW.format('HH:mm'), // Current time
expire: NOW.add(data.duration + ' months').format('dd-MM-yyyy'), // Expiry date
dtcreated: NOW // Creation timestamp
};
// Initialize payment with Ligdicash
const response = await invoice.payWithRedirection({
return_url: "https://totalgpt.com/success", // Success URL
cancel_url: "https://totalgpt.com/fail", // Failure URL
callback_url: "https://totalgpt.com/callback", // Callback URL
custom_data: { // Custom data
orderid: order.id,
userid: order.userid,
planid: order.planid
}
});
// Get payment URL from response
const payment_url = response.response_text;
// Update order with payment details
order.paymentid = response.token;
order.paylink = payment_url;
await DATA.insert('nosql/order', order).promise();
// Handle response
if (payment_url) {
// Redirect to payment page and notify user
controller.redirect(payment_url);
await FUNC.sendWhatsAppMessage(chatid,
'@(Your payment link has been generated successfully. \n\nYou will be redirected to a payment platform. π.\n\n Warning: if you did not request this action, ignore this message)');
} else {
// Handle error
await FUNC.sendWhatsAppMessage(chatid,
'@(An error occurred while generating your payment link.π)');
controller.redirect('/error');
}
}
// Callback handler function
async function callback() {
var self = this;
var payload = self.body;
var custom = {};
// Extract custom data from payload
for (var d of payload.custom_data)
custom[d.keyof_customdata] = d.valueof_customdata;
// Fetch related data
var plan = await DATA.read('nosql/packs')
.where('id', custom.planid).promise();
var user = await DATA.read('nosql/account')
.where('id', custom.userid).promise();
var order = await DATA.read('nosql/order')
.where('id', custom.orderid).promise();
// Prepare updates
var update = {
paid_amount: parseInt(payload.montant),
data: JSON.stringify(payload),
status: payload.status,
description: plan.name + ' via ' + payload.operator_name,
expire: NOW.add(order.duration + ' months').format('dd-MM-yyyy')
};
// Update order status
await DATA.update('nosql/order', update)
.id(custom.orderid).promise();
// Handle successful payment
if (update.status == 'completed') {
// Update user's plan
var userupdate = { plans: [plan.id] };
await DATA.update('nosql/account', userupdate)
.id(custom.userid).promise();
}
// Send notifications
PUB('gpt_reply', {
chatid: custom.chatid,
content: 'Success!! +{0}. Thank you'.format(plan.count)
});
FUNC.notify_admin(
'Success!! +{0}. https://wa.me/{1}'.format(
payload.montant,
custom.userid.split('@')[0]
)
);
self.success();
}
Key Components Explained
- Definition File Setup
- Creates a singleton Ligdicash client
- Stores client in MAIN for global access
- Handles configuration via CONF object
- Checkout Process
- Validates user and plan
- Creates invoice with plan details
- Generates payment URL
- Creates order record
- Handles success/failure scenarios
- Callback Handling
- Processes payment notifications
- Updates order status
- Updates user subscriptions
- Sends notifications
- Database Integration
- Uses Total.js DATA object for database operations
- Maintains order records
- Updates user subscriptions
- Notification System
- Sends WhatsApp messages to users
- Notifies administrators of successful payments
- Handles error scenarios
Adding Stripe Integration
First, we need to install Stripe and set up a definition file, similar to how we did with Ligdicash.
1. Create Stripe Definition File
Create /definitions/stripe.js
:
// DO not forget to install stripe via NPMINSTALL() or in your terminal `npm install stripe`
const stripe = require('stripe')(CONF.stripe_secret);
MAIN.stripe = stripe;
2. Update Controller
In /controllers/default.js
, add new route handlers:
// Handle Stripe Checkout
async function stripe_checkout() {
var self = this;
var data = self.body;
var chatid = data.phone + '@c.us';
// Fetch user and plan info
var user = await DATA.read('nosql/account').id(chatid).promise();
var plan = await DATA.read('nosql/plans').id(data.plan).promise();
// Calculate amount
var amount = plan.price * data.duration;
// Create Stripe session
const session = await MAIN.stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [{
price_data: {
currency: 'usd',
product_data: {
name: plan.name,
description: `${data.duration} months subscription`
},
unit_amount: amount * 100 // Stripe uses cents
},
quantity: 1
}],
mode: 'payment',
success_url: 'https://totalgpt.com/success',
cancel_url: 'https://totalgpt.com/fail',
metadata: {
orderid: GUID(35),
userid: user.id,
planid: plan.id,
duration: data.duration
}
});
// Store order info
var order = {
id: session.metadata.orderid,
userid: user.id,
planid: plan.id,
duration: data.duration,
amount: amount,
status: 'pending',
dtcreated: NOW
};
await DATA.insert('nosql/order', order).promise();
// Redirect to Stripe
self.redirect(session.url);
}
// Handle Stripe Webhook
async function stripe_webhook() {
var self = this;
var event = self.body;
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Update order status
var order = await DATA.read('nosql/order')
.id(session.metadata.orderid)
.promise();
if (order) {
// Update order
await DATA.modify('nosql/order', {
status: 'completed',
stripe_payment_id: session.payment_intent
}).id(order.id).promise();
// Update user subscription
var user = await DATA.read('nosql/account')
.id(session.metadata.userid)
.promise();
if (user) {
await DATA.modify('nosql/account', {
plans: [session.metadata.planid]
}).id(user.id).promise();
// Send notification
await FUNC.sendWhatsAppMessage(user.id,
'@(Your subscription has been activated successfully!)');
}
}
}
self.success();
}
3. Add Routes
Add these routes to your exports.install:
ROUTE('POST /stripe/checkout/', stripe_checkout);
ROUTE('POST /stripe/webhook', stripe_webhook);
Give users the option to choose their payment method:
<div class="payment-methods">
<button onclick="submitPayment('ligdicash')">Pay with Mobile Money</button>
<button onclick="submitPayment('stripe')">Pay with Card</button>
</div>
Key Differences from Ligdicash
- Currency: Stripe uses cents (multiply by 100)
- Webhook Handling: Different event structure
- Payment Methods: Cards only (unless you add specific local payment methods)
- Session-based: Uses Stripe Checkout Sessions
- International Focus: Better for global payments
Important Notes
- Testing: Use Stripe test keys during development
- Webhooks: Set up Stripe webhook endpoint for live updates
- Error Handling: Implement proper error handling for failed payments
- Currency Conversion: Handle currency conversion if needed
- SSL: Ensure your website uses HTTPS
What's Next?
In the next blog post, we'll cover Stripe integration and compare the implementation differences between Ligdicash and Stripe. We'll also explore how to create a unified payment interface that works with multiple payment providers.
Stay tuned for more Total.js development insights! π