stripe-payment-utils
Version:
A comprehensive Stripe payment utility package for handling payment intents, splits, refunds, and transfers
572 lines (503 loc) • 15.7 kB
JavaScript
const stripe = require('stripe');
class StripePaymentUtils {
constructor(secretKey) {
if (!secretKey) {
throw new Error('Stripe secret key is required');
}
this.stripe = stripe(secretKey);
}
/**
* Create Payment Intent
* @param {Object} options - Payment intent options
* @param {number} options.amount - Amount in cents
* @param {string} options.currency - Currency code (default: 'usd')
* @param {string} options.customerId - Stripe customer ID
* @param {string} options.paymentMethodId - Payment method ID
* @param {Object} options.metadata - Additional metadata
* @returns {Promise<Object>} Payment intent object
*/
async createPaymentIntent(options) {
try {
const {
amount,
currency = 'usd',
customerId,
paymentMethodId,
metadata = {},
captureMethod = 'manual',
setupFutureUsage = 'off_session'
} = options;
if (!amount || amount <= 0) {
throw new Error('Amount must be greater than 0');
}
if (!customerId) {
throw new Error('Customer ID is required');
}
if (!paymentMethodId) {
throw new Error('Payment method ID is required');
}
const paymentIntent = await this.stripe.paymentIntents.create({
amount: Math.round(amount),
currency,
customer: customerId,
payment_method: paymentMethodId,
capture_method: captureMethod,
setup_future_usage: setupFutureUsage,
metadata
});
return {
success: true,
data: paymentIntent
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Capture Payment with Split
* @param {Object} options - Capture options
* @param {string} options.paymentIntentId - Payment intent ID
* @param {number} options.totalAmount - Total amount in cents
* @param {string} options.receiverId - Receiver's Stripe account ID
* @param {string} options.adminAccountId - Admin's Stripe account ID
* @param {Object} options.splitConfig - Split configuration
* @param {number} options.splitConfig.receiverPercentage - Percentage for receiver
* @param {number} options.splitConfig.adminPercentage - Percentage for admin
* @param {Object} options.metadata - Additional metadata
* @returns {Promise<Object>} Capture result
*/
async capturePaymentWithSplit(options) {
try {
const {
paymentIntentId,
totalAmount,
receiverId,
adminAccountId,
splitConfig = { receiverPercentage: 80, adminPercentage: 20 },
metadata = {}
} = options;
if (!paymentIntentId) {
throw new Error('Payment intent ID is required');
}
if (!totalAmount || totalAmount <= 0) {
throw new Error('Total amount must be greater than 0');
}
if (!receiverId) {
throw new Error('Receiver ID is required');
}
if (!adminAccountId) {
throw new Error('Admin account ID is required');
}
// Validate split percentages
const { receiverPercentage, adminPercentage } = splitConfig;
if (receiverPercentage + adminPercentage !== 100) {
throw new Error('Split percentages must equal 100%');
}
// Capture the payment intent
const capturedPayment = await this.stripe.paymentIntents.capture(
paymentIntentId,
{
amount_to_capture: Math.round(totalAmount)
}
);
if (capturedPayment.status !== 'succeeded') {
throw new Error('Payment capture failed');
}
// Calculate split amounts
const receiverAmount = Math.round(totalAmount * (receiverPercentage / 100));
const adminAmount = Math.round(totalAmount * (adminPercentage / 100));
// Process transfers
let receiverTransfer = null;
let adminTransfer = null;
try {
// Transfer to receiver
receiverTransfer = await this.stripe.transfers.create({
amount: receiverAmount,
currency: 'usd',
destination: receiverId,
description: `Payment for booking`,
metadata: {
...metadata,
type: 'receiver_payment',
percentage: receiverPercentage.toString()
}
});
// Transfer to admin
adminTransfer = await this.stripe.transfers.create({
amount: adminAmount,
currency: 'usd',
destination: adminAccountId,
description: `Admin split for booking`,
metadata: {
...metadata,
type: 'admin_split',
percentage: adminPercentage.toString()
}
});
return {
success: true,
data: {
paymentIntentId,
receiverTransferId: receiverTransfer.id,
adminTransferId: adminTransfer.id,
receiverAmount,
adminAmount,
totalAmount,
status: capturedPayment.status,
isSplit: true
}
};
} catch (transferError) {
// If transfers fail, still return captured payment but mark as not split
return {
success: true,
data: {
paymentIntentId,
receiverAmount,
adminAmount,
totalAmount,
status: capturedPayment.status,
isSplit: false,
transferError: transferError.message
}
};
}
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Handle No-Show Payment
* @param {Object} options - No-show options
* @param {string} options.paymentIntentId - Payment intent ID
* @param {number} options.totalAmount - Total amount in cents
* @param {string} options.receiverId - Receiver's Stripe account ID
* @param {number} options.noShowPercentage - Percentage for receiver (default: 15)
* @param {number} options.refundPercentage - Percentage to refund (default: 85)
* @param {Object} options.metadata - Additional metadata
* @returns {Promise<Object>} No-show result
*/
async handleNoShow(options) {
try {
const {
paymentIntentId,
totalAmount,
receiverId,
noShowPercentage = 15,
refundPercentage = 85,
metadata = {}
} = options;
if (!paymentIntentId) {
throw new Error('Payment intent ID is required');
}
if (!totalAmount || totalAmount <= 0) {
throw new Error('Total amount must be greater than 0');
}
if (!receiverId) {
throw new Error('Receiver ID is required');
}
if (noShowPercentage + refundPercentage !== 100) {
throw new Error('No-show and refund percentages must equal 100%');
}
// Calculate amounts
const noShowAmount = Math.round(totalAmount * (noShowPercentage / 100));
const refundAmount = Math.round(totalAmount * (refundPercentage / 100));
// Capture no-show amount
const capturedPayment = await this.stripe.paymentIntents.capture(
paymentIntentId,
{
amount_to_capture: noShowAmount
}
);
if (capturedPayment.status !== 'succeeded') {
throw new Error('Payment capture failed');
}
// Transfer no-show amount to receiver (100% of captured amount)
const receiverTransfer = await this.stripe.transfers.create({
amount: noShowAmount,
currency: 'usd',
destination: receiverId,
description: `No-show payment for booking`,
metadata: {
...metadata,
type: 'no_show_payment',
percentage: noShowPercentage.toString()
}
});
// Refund remaining amount to customer
const refund = await this.stripe.refunds.create({
payment_intent: paymentIntentId,
amount: refundAmount,
reason: 'requested_by_customer',
metadata: {
...metadata,
type: 'no_show_refund',
percentage: refundPercentage.toString()
}
});
return {
success: true,
data: {
paymentIntentId,
receiverTransferId: receiverTransfer.id,
refundId: refund.id,
receiverAmount: noShowAmount,
refundAmount,
totalAmount,
status: capturedPayment.status,
noShowPercentage,
refundPercentage
}
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Process Refund
* @param {Object} options - Refund options
* @param {string} options.paymentIntentId - Payment intent ID
* @param {number} options.amount - Amount to refund in cents (optional)
* @param {string} options.reason - Refund reason
* @param {Object} options.metadata - Additional metadata
* @returns {Promise<Object>} Refund result
*/
async processRefund(options) {
try {
const {
paymentIntentId,
amount,
reason = 'requested_by_customer',
metadata = {}
} = options;
if (!paymentIntentId) {
throw new Error('Payment intent ID is required');
}
const refundParams = {
payment_intent: paymentIntentId,
reason,
metadata
};
// Add amount if specified (otherwise full refund)
if (amount && amount > 0) {
refundParams.amount = Math.round(amount);
}
const refund = await this.stripe.refunds.create(refundParams);
return {
success: true,
data: {
refundId: refund.id,
amount: refund.amount,
status: refund.status,
reason: refund.reason
}
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Cancel Payment Intent
* @param {Object} options - Cancel options
* @param {string} options.paymentIntentId - Payment intent ID
* @param {string} options.reason - Cancellation reason
* @returns {Promise<Object>} Cancellation result
*/
async cancelPaymentIntent(options) {
try {
const {
paymentIntentId,
reason = 'requested_by_customer'
} = options;
if (!paymentIntentId) {
throw new Error('Payment intent ID is required');
}
const cancelledPayment = await this.stripe.paymentIntents.cancel(
paymentIntentId,
{
cancellation_reason: reason
}
);
return {
success: true,
data: {
paymentIntentId,
status: cancelledPayment.status,
reason
}
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Get Payment Intent Details
* @param {string} paymentIntentId - Payment intent ID
* @returns {Promise<Object>} Payment intent details
*/
async getPaymentIntentDetails(paymentIntentId) {
try {
if (!paymentIntentId) {
throw new Error('Payment intent ID is required');
}
const paymentIntent = await this.stripe.paymentIntents.retrieve(paymentIntentId);
return {
success: true,
data: paymentIntent
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Create Transfer
* @param {Object} options - Transfer options
* @param {number} options.amount - Amount in cents
* @param {string} options.destination - Destination account ID
* @param {string} options.currency - Currency code (default: 'usd')
* @param {string} options.description - Transfer description
* @param {Object} options.metadata - Additional metadata
* @returns {Promise<Object>} Transfer result
*/
async createTransfer(options) {
try {
const {
amount,
destination,
currency = 'usd',
description = 'Transfer',
metadata = {}
} = options;
if (!amount || amount <= 0) {
throw new Error('Amount must be greater than 0');
}
if (!destination) {
throw new Error('Destination account ID is required');
}
const transfer = await this.stripe.transfers.create({
amount: Math.round(amount),
currency,
destination,
description,
metadata
});
return {
success: true,
data: transfer
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Get Transfer Details
* @param {string} transferId - Transfer ID
* @returns {Promise<Object>} Transfer details
*/
async getTransferDetails(transferId) {
try {
if (!transferId) {
throw new Error('Transfer ID is required');
}
const transfer = await this.stripe.transfers.retrieve(transferId);
return {
success: true,
data: transfer
};
} catch (error) {
return {
success: false,
error: {
type: error.type,
message: error.message,
code: error.code
}
};
}
}
/**
* Validate Split Configuration
* @param {Object} splitConfig - Split configuration
* @returns {boolean} Validation result
*/
validateSplitConfig(splitConfig) {
const { receiverPercentage, adminPercentage } = splitConfig;
if (typeof receiverPercentage !== 'number' || typeof adminPercentage !== 'number') {
return false;
}
if (receiverPercentage < 0 || adminPercentage < 0) {
return false;
}
if (receiverPercentage + adminPercentage !== 100) {
return false;
}
return true;
}
/**
* Calculate Split Amounts
* @param {number} totalAmount - Total amount in cents
* @param {Object} splitConfig - Split configuration
* @returns {Object} Split amounts
*/
calculateSplitAmounts(totalAmount, splitConfig) {
if (!this.validateSplitConfig(splitConfig)) {
throw new Error('Invalid split configuration');
}
const { receiverPercentage, adminPercentage } = splitConfig;
return {
receiverAmount: Math.round(totalAmount * (receiverPercentage / 100)),
adminAmount: Math.round(totalAmount * (adminPercentage / 100)),
totalAmount
};
}
}
module.exports = StripePaymentUtils;