UNPKG

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
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;