ibc-payment-gateway
Version:
A modular payment gateway for Node.js applications with PostgreSQL and Sequelize
366 lines (306 loc) • 11 kB
JavaScript
const RazorpayProvider = require('./providers/RazorpayProvider');
const { Sequelize } = require("sequelize");
class PaymentService {
constructor(models, sequelize, webhookUpdateQuery) {
this.models = models;
this.providers = {};
this.sequelize = sequelize;
this.webhookUpdateQuery = webhookUpdateQuery;
}
async initializeProviders() {
const providers = await this.models.PaymentProvider.findAll({
where: { is_active: true }
});
for (const provider of providers) {
switch (provider.provider) {
case 'razorpay':
this.providers[provider.name] = new RazorpayProvider(provider.config);
break;
// Add more providers here in the future
default:
console.warn(`Unknown provider: ${provider.provider}`);
}
}
}
async listPaymentProviders() {
try {
const providers = await this.models.PaymentProvider.findAll({
order: [['createdAt', 'DESC']] // latest first, optional
});
return providers;
} catch (error) {
throw new Error(`Failed to list payment providers: ${error.message}`);
}
}
async createPaymentProvider(providerData) {
const transaction = await this.models.sequelize.transaction();
try {
// If new provider should be active → deactivate others first
if (providerData.is_active) {
await this.models.PaymentProvider.update(
{ is_active: false },
{ where: { is_active: true }, transaction }
);
}
// Create new provider
const provider = await this.models.PaymentProvider.create(providerData, { transaction });
// Commit transaction
await transaction.commit();
// Initialize the provider instance
if (providerData.name === 'razorpay') {
this.providers[provider.name] = new RazorpayProvider(providerData.config);
}
return provider;
} catch (error) {
await transaction.rollback();
throw new Error(`Failed to create payment provider: ${error.message}`);
}
}
async updatePaymentProvider(providerName, updateData) {
const transaction = await this.models.sequelize.transaction();
try {
// If activating this provider → deactivate others
if (updateData.is_active) {
await this.models.PaymentProvider.update(
{ is_active: false },
{ where: { is_active: true, name: { [this.models.Sequelize.Op.ne]: providerName } }, transaction }
);
}
// Update provider
const [rowsUpdated, [updatedProvider]] = await this.models.PaymentProvider.update(
updateData,
{ where: { name: providerName }, returning: true, transaction }
);
if (rowsUpdated === 0) {
throw new Error('Payment provider not found');
}
await transaction.commit();
// Re-init provider instance
if (updatedProvider.name === 'razorpay') {
this.providers[updatedProvider.name] = new RazorpayProvider(updatedProvider.config);
}
return updatedProvider;
} catch (error) {
await transaction.rollback();
throw new Error(`Failed to update payment provider: ${error.message}`);
}
}
async deletePaymentProvider(providerName) {
const transaction = await this.models.sequelize.transaction();
try {
const provider = await this.models.PaymentProvider.findByPk(providerName, { transaction });
if (!provider) {
throw new Error('Payment provider not found');
}
await provider.destroy({ transaction });
await transaction.commit();
// Remove from in-memory map
if (this.providers[provider.name]) {
delete this.providers[provider.name];
}
return { message: 'Payment provider deleted successfully' };
} catch (error) {
await transaction.rollback();
throw new Error(`Failed to delete payment provider: ${error.message}`);
}
}
async initiatePayment(paymentData) {
try {
const { name, amount, currency = 'INR', customer_info, metadata, order_id } = paymentData;
let provider = null;
if (name) {
// Try to get the provider by Name
provider = await this.models.PaymentProvider.findByPk(name);
}
if (!provider) {
// Fallback: get the latest active provider
provider = await this.models.PaymentProvider.findOne({
where: { is_active: true },
order: [['updatedAt', 'DESC']]
});
}
if (!provider) {
throw new Error('No active payment provider found');
}
const provider_name = provider.name;
// Create transaction record
const transaction = await this.models.PaymentTransaction.create({
order_id,
provider_name,
amount,
currency,
status: 'pending',
customer_info,
metadata
});
// Initialize provider if not already done
if (!this.providers[provider_name]) {
await this.initializeProviders();
}
// Create order with provider
const providerInstance = this.providers[provider_name];
const providerOrder = await providerInstance.createOrder({
order_id: transaction.order_id,
amount: amount * 100, // Convert to paisa for Razorpay
currency,
customer_info
});
// Update transaction with provider order details
await transaction.update({
provider_order_id: providerOrder.id,
provider_response: providerOrder
});
return {
success: true,
transaction: {
id: transaction.id,
order_id: transaction.order_id,
provider_order_id: providerOrder.id,
amount: transaction.amount,
currency: transaction.currency,
status: transaction.status
},
provider_data: providerOrder,
provider_key_id: provider.config?.key_id || null
};
} catch (error) {
console.error(error);
throw new Error(`Payment initiation failed: ${error.message}`);
}
}
async handleWebhook(provider_name, signature, payload) {
try {
console.log("Handle Webhook");
// Log webhook
const webhookLog = await this.models.PaymentWebhookLog.create({
provider_name,
event_type: payload.event || 'unknown',
payload,
signature,
processed: false
});
// Get provider
const provider = await this.models.PaymentProvider.findByPk(provider_name);
if (!provider) {
throw new Error('Payment provider not found');
}
// Initialize provider if not already done
if (!this.providers[provider_name]) {
await this.initializeProviders();
}
// Verify webhook signature
const providerInstance = this.providers[provider_name];
const isValid = providerInstance.verifyWebhook(signature, payload, provider.config.webhook_secret);
if (!isValid) {
throw new Error('Invalid webhook signature');
}
// Process webhook based on event type
const result = await this.processWebhookEvent(payload, provider_name);
// Update webhook log
await webhookLog.update({
processed: true,
transaction_id: result.transaction_id
});
return result;
} catch (error) {
console.error('Webhook processing error:', error);
throw error;
}
}
async processWebhookEvent(payload, provider_name) {
const event_type = payload.event;
switch (event_type) {
case 'payment.authorized':
case 'payment.captured':
return await this.handlePaymentSuccess(payload, provider_name);
case 'payment.failed':
return await this.handlePaymentFailure(payload, provider_name);
default:
console.log(`Unhandled event type: ${event_type}`);
return { processed: false };
}
}
async handlePaymentSuccess(payload, provider_name) {
const paymentEntity = payload.payload.payment.entity;
// Find transaction by provider order ID
const transaction = await this.models.PaymentTransaction.findOne({
where: {
provider_order_id: paymentEntity.order_id,
provider_name
}
});
if (!transaction) {
throw new Error('Transaction not found for webhook');
}
// Update transaction
await transaction.update({
status: 'success',
provider_payment_id: paymentEntity.id,
payment_method: paymentEntity.method,
webhook_received: true,
webhook_data: payload
});
// Update Payment Status
await this.updateMainPaymentStaus('success', transaction.order_id);
return {
processed: true,
transaction_id: transaction.id,
status: 'success'
};
}
async handlePaymentFailure(payload, provider_name) {
const paymentEntity = payload.payload.payment.entity;
// Find transaction by provider order ID
const transaction = await this.models.PaymentTransaction.findOne({
where: {
provider_order_id: paymentEntity.order_id,
provider_name
}
});
if (!transaction) {
throw new Error('Transaction not found for webhook');
}
// Update transaction
await transaction.update({
status: 'failed',
provider_payment_id: paymentEntity.id,
payment_method: paymentEntity.method,
failure_reason: paymentEntity.error_description,
webhook_received: true,
webhook_data: payload
});
// Update Payment Status
await this.updateMainPaymentStaus('failed', transaction.order_id);
return {
processed: true,
transaction_id: transaction.id,
status: 'failed'
};
}
async getTransactions(order_id) {
const transaction = await this.models.PaymentTransaction.findAll({
where: { order_id },
include: []
});
return transaction;
}
async getTransactionsByStatus(status) {
const transactions = await this.models.PaymentTransaction.findAll({
where: { status },
include: [this.models.PaymentProvider]
});
return transactions;
}
async updateMainPaymentStaus(status, id) {
try {
const [result, metadata] = await this.sequelize.query(this.webhookUpdateQuery, {
replacements: { status: status, id: id },
type: Sequelize.QueryTypes.UPDATE
});
console.log("payment status updated in the Main Table")
} catch (err) {
console.log(err);
}
}
}
module.exports = PaymentService;