UNPKG

ibc-payment-gateway

Version:

A modular payment gateway for Node.js applications with PostgreSQL and Sequelize

366 lines (306 loc) 11 kB
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;