@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
JavaScript
"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;