UNPKG

@vegaci_shared/vsplit-payment-gateway

Version:

A B2B payment gateway integration package for Stripe with split payment capabilities

421 lines (420 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VSplitPaymentGateway = void 0; const stripe_js_1 = require("@stripe/stripe-js"); const api_client_1 = require("./utils/api-client"); const event_emitter_1 = require("./utils/event-emitter"); const payment_timer_1 = require("./utils/payment-timer"); /** * Main VSplit Payment Gateway class */ class VSplitPaymentGateway { constructor(config) { this.stripe = null; this.currentSession = null; this.config = { currency: 'usd', defaultTimeout: 600, // 10 minutes environment: 'production', ...config, }; this.apiClient = new api_client_1.ApiClient(this.config); this.eventEmitter = new event_emitter_1.EventEmitter(); this.paymentTimer = new payment_timer_1.PaymentTimer(); this.initialize(); } /** * Initialize Stripe */ async initialize() { try { this.stripe = await (0, stripe_js_1.loadStripe)(this.config.stripePublishableKey); if (!this.stripe) { throw new Error('Failed to load Stripe'); } } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Get Stripe instance */ getStripe() { return this.stripe; } /** * Create payment elements */ createElements(clientSecret) { if (!this.stripe) { throw new Error('Stripe not initialized'); } return this.stripe.elements({ clientSecret, appearance: { theme: 'stripe', variables: { colorPrimary: this.config.theme?.primaryColor || '#0066cc', borderRadius: this.config.theme?.borderRadius || '4px', fontFamily: this.config.theme?.fontFamily || 'system-ui, sans-serif', }, }, }); } /** * Initialize a single payment session */ async initializePayment(config) { try { const response = await this.apiClient.post('/payment/initialize', { ...config, currency: config.currency || this.config.currency, }); if (!response.success || !response.data) { throw new Error(response.error || 'Failed to initialize payment'); } this.currentSession = response.data; return { success: true, paymentId: response.data.id, status: response.data.status, data: response.data, }; } catch (error) { const result = { success: false, paymentId: '', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; this.eventEmitter.emit('payment:failed', result); return result; } } /** * Process a single payment */ async processPayment(paymentIntentId, paymentMethod) { try { if (!this.stripe) { throw new Error('Stripe not initialized'); } const { paymentIntent, error } = await this.stripe.confirmPayment({ elements: paymentMethod.elements, confirmParams: { payment_method: paymentMethod.id || 'auto', return_url: paymentMethod.returnUrl || window.location.href, }, redirect: 'if_required', }); if (error) { const result = { success: false, paymentId: paymentIntentId, status: 'failed', error: error.message, }; this.eventEmitter.emit('payment:failed', result); return result; } const result = { success: paymentIntent?.status === 'succeeded', paymentId: paymentIntent?.id || paymentIntentId, status: paymentIntent?.status || 'failed', data: paymentIntent, }; if (result.success) { this.eventEmitter.emit('payment:success', result); } else if (paymentIntent?.status === 'requires_action') { this.eventEmitter.emit('payment:requires_action', result); } else { this.eventEmitter.emit('payment:failed', result); } return result; } catch (error) { const result = { success: false, paymentId: paymentIntentId, status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; this.eventEmitter.emit('payment:failed', result); return result; } } /** * Initialize split payment session */ async initializeSplitPayment(config) { try { const timeout = config.timeout || this.config.defaultTimeout || 600; const response = await this.apiClient.post('/payment/split/initialize', { ...config, timeout, }); if (!response.success || !response.data) { throw new Error(response.error || 'Failed to initialize split payment'); } this.currentSession = response.data; // Start timeout timer this.paymentTimer.startTimer(timeout * 1000, () => { this.handleSplitPaymentTimeout(response.data?.sessionId || ''); }); return response.data; } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Process a split payment step */ async processSplitPayment(sessionId, splitIndex, paymentMethod) { try { const session = this.currentSession; if (!session || session.sessionId !== sessionId) { throw new Error('Invalid session'); } const paymentIntent = session.paymentIntents[splitIndex]; if (!paymentIntent) { throw new Error('Invalid split index'); } const result = await this.processPayment(paymentIntent.id, paymentMethod); if (result.success) { // Update session status session.completedPayments += 1; // Check if all payments are completed if (session.completedPayments === session.paymentIntents.length) { session.status = 'succeeded'; this.paymentTimer.clearTimer(); this.eventEmitter.emit('split:completed', session); } else { session.status = 'partial'; this.eventEmitter.emit('split:partial', session); } } else { session.failedPayments += 1; } return result; } catch (error) { const result = { success: false, paymentId: sessionId, status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; this.eventEmitter.emit('payment:failed', result); return result; } } /** * Handle split payment timeout */ async handleSplitPaymentTimeout(sessionId) { try { const session = this.currentSession; if (!session || session.sessionId !== sessionId) { return; } session.status = 'canceled'; // Refund any completed payments await this.refundPartialPayments(session); this.eventEmitter.emit('split:timeout', session); } catch (error) { this.eventEmitter.emit('error', error); } } /** * Refund partial payments in a split payment session */ async refundPartialPayments(session) { const refundPromises = session.paymentIntents .filter((pi) => pi.status === 'succeeded') .map((pi) => this.refundPayment({ paymentIntentId: pi.id, reason: 'other', metadata: { reason: 'split_payment_timeout', sessionId: session.sessionId, }, })); await Promise.allSettled(refundPromises); } /** * Cancel current payment */ async cancelPayment() { try { if (!this.currentSession) { return; } this.paymentTimer.clearTimer(); if ('sessionId' in this.currentSession) { // Split payment session const splitSession = this.currentSession; await this.apiClient.post(`/payment/split/${splitSession.sessionId}/cancel`); await this.refundPartialPayments(splitSession); } else { // Single payment await this.apiClient.post(`/payment/${this.currentSession.id}/cancel`); } const result = { success: true, paymentId: ('sessionId' in this.currentSession ? this.currentSession.sessionId : this.currentSession.id), status: 'canceled', }; this.eventEmitter.emit('payment:canceled', result); this.currentSession = null; } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Refund a payment */ async refundPayment(request) { try { const response = await this.apiClient.post('/payment/refund', request); if (!response.success || !response.data) { throw new Error(response.error || 'Failed to process refund'); } return response.data; } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Verify a payment */ async verifyPayment(request) { try { const response = await this.apiClient.post('/payment/verify', request); if (!response.success || !response.data) { throw new Error(response.error || 'Failed to verify payment'); } return response.data; } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Get payment status */ async getPaymentStatus(paymentId) { try { const response = await this.apiClient.get(`/payment/${paymentId}/status`); if (!response.success || !response.data) { throw new Error(response.error || 'Failed to get payment status'); } return response.data.status; } catch (error) { this.eventEmitter.emit('error', error); throw error; } } /** * Subscribe to events */ on(event, callback) { this.eventEmitter.on(event, callback); } /** * Unsubscribe from events */ off(event, callback) { this.eventEmitter.off(event, callback); } /** * Get current session */ getCurrentSession() { return this.currentSession; } // ======================================== // MERCHANT REVENUE SPLITTING (OPTIONAL) // ======================================== /** * Initialize payment with merchant revenue splitting * This processes customer payment and then splits revenue between recipients */ async initializePaymentWithMerchantSplits(config) { try { // First, initialize the regular payment const paymentResult = await this.initializePayment(config); if (!paymentResult.success) { return paymentResult; } // If merchant splits are provided, process them after successful payment if (config.merchantSplits && config.merchantSplits.length > 0) { // Store the merchant splits for processing after payment confirmation paymentResult.data.merchantSplits = config.merchantSplits; } return paymentResult; } catch (error) { const result = { success: false, paymentId: '', status: 'failed', error: error instanceof Error ? error.message : 'Unknown error', }; this.eventEmitter.emit('payment:failed', result); return result; } } /** * Process merchant revenue splits after successful payment */ async processMerchantSplits(paymentIntentId, splits) { try { const response = await this.apiClient.post('/payment/merchant-splits/process', { paymentIntentId, splits, }); if (!response.success) { throw new Error(response.error || 'Failed to process merchant splits'); } return { success: true, splits: response.data?.splits || [], }; } catch (error) { return { success: false, splits: [], error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Destroy the gateway instance */ destroy() { this.paymentTimer.clearTimer(); this.eventEmitter.removeAllListeners(); this.currentSession = null; } } exports.VSplitPaymentGateway = VSplitPaymentGateway;