UNPKG

@ideal-photography/shared

Version:

Shared MongoDB and utility logic for Ideal Photography PWAs: users, products, services, bookings, orders/cart, galleries, reviews, notifications, campaigns, settings, audit logs, minimart items/orders, and push notification subscriptions.

372 lines (334 loc) 13.2 kB
import { models } from '../mongoDB/index.js'; import OrderService from './OrderService.js'; class PaymentService { constructor() { this.orderService = OrderService; } /** * Create payment record * @param {string} orderId - Order ID * @param {Object} paymentData - Payment data * @returns {Promise<Object>} Created payment */ async createPayment(orderId, paymentData) { try { // Get order details const orderResult = await this.orderService.getOrderById(orderId, paymentData.userId); if (!orderResult.success) { throw new Error('Order not found'); } const order = orderResult.order; // Generate payment reference const paymentReference = models.Payment.generatePaymentReference(); // Create payment record const payment = await models.Payment.create({ paymentReference, orderId, userId: paymentData.userId, amount: order.total, currency: paymentData.currency || 'NGN', paymentMethod: paymentData.paymentMethod || 'paystack', provider: paymentData.provider || 'paystack', description: paymentData.description || `Payment for order ${order.orderNumber}`, metadata: paymentData.metadata || new Map() }); return { success: true, payment: { id: payment._id, paymentReference: payment.paymentReference, amount: payment.amount, currency: payment.currency, paymentMethod: payment.paymentMethod, status: payment.status, createdAt: payment.createdAt } }; } catch (error) { console.error('Error creating payment:', error); throw error; } } /** * Process payment with Paystack * @param {string} paymentId - Payment ID * @param {Object} paystackData - Paystack response data * @returns {Promise<Object>} Updated payment */ async processPaystackPayment(paymentId, paystackData) { try { const payment = await models.Payment.findById(paymentId); if (!payment) { throw new Error('Payment not found'); } // Update payment with Paystack data payment.providerReference = paystackData.reference; payment.providerResponse = new Map(Object.entries(paystackData)); payment.status = paystackData.status === 'success' ? 'successful' : 'failed'; payment.updatedAt = new Date(); if (payment.status === 'successful') { payment.verified = true; payment.verifiedAt = new Date(); // Update order payment status await this.orderService.updatePaymentStatus( payment.orderId, 'paid', payment.providerReference ); } await payment.save(); return { success: true, payment: { id: payment._id, paymentReference: payment.paymentReference, status: payment.status, providerReference: payment.providerReference, verified: payment.verified, updatedAt: payment.updatedAt } }; } catch (error) { console.error('Error processing Paystack payment:', error); throw error; } } /** * Verify payment * @param {string} paymentId - Payment ID * @param {string} verifiedBy - User ID who verified * @returns {Promise<Object>} Updated payment */ async verifyPayment(paymentId, verifiedBy) { try { const payment = await models.Payment.findById(paymentId); if (!payment) { throw new Error('Payment not found'); } payment.verified = true; payment.verifiedAt = new Date(); payment.verifiedBy = verifiedBy; payment.status = 'successful'; payment.updatedAt = new Date(); await payment.save(); // Update order payment status await this.orderService.updatePaymentStatus( payment.orderId, 'paid', payment.providerReference ); return { success: true, payment: { id: payment._id, paymentReference: payment.paymentReference, status: payment.status, verified: payment.verified, verifiedAt: payment.verifiedAt, updatedAt: payment.updatedAt } }; } catch (error) { console.error('Error verifying payment:', error); throw error; } } /** * Process refund * @param {string} paymentId - Payment ID * @param {number} amount - Refund amount * @param {string} reason - Refund reason * @param {string} refundedBy - User ID who processed refund * @returns {Promise<Object>} Updated payment */ async processRefund(paymentId, amount, reason, refundedBy) { try { const payment = await models.Payment.findById(paymentId); if (!payment) { throw new Error('Payment not found'); } if (payment.status !== 'successful') { throw new Error('Can only refund successful payments'); } if (amount > payment.amount) { throw new Error('Refund amount cannot exceed payment amount'); } payment.processRefund(amount, reason, refundedBy); await payment.save(); // Update order status if full refund if (amount >= payment.amount) { await this.orderService.updateOrderStatus( payment.orderId, 'cancelled', refundedBy, `Refunded: ${reason}` ); } return { success: true, payment: { id: payment._id, paymentReference: payment.paymentReference, status: payment.status, refundAmount: payment.refundAmount, refundReason: payment.refundReason, refundedAt: payment.refundedAt, updatedAt: payment.updatedAt } }; } catch (error) { console.error('Error processing refund:', error); throw error; } } /** * Get payment by ID * @param {string} paymentId - Payment ID * @param {string} userId - User ID (for authorization) * @returns {Promise<Object>} Payment data */ async getPaymentById(paymentId, userId) { try { const payment = await models.Payment.findOne({ _id: paymentId, userId }); if (!payment) { throw new Error('Payment not found'); } return { success: true, payment: { id: payment._id, paymentReference: payment.paymentReference, orderId: payment.orderId, amount: payment.amount, currency: payment.currency, paymentMethod: payment.paymentMethod, status: payment.status, provider: payment.provider, providerReference: payment.providerReference, verified: payment.verified, refundAmount: payment.refundAmount, refundReason: payment.refundReason, createdAt: payment.createdAt, updatedAt: payment.updatedAt } }; } catch (error) { console.error('Error getting payment:', error); throw error; } } /** * Get user's payments * @param {string} userId - User ID * @param {Object} filters - Filter options * @returns {Promise<Object>} Payments and pagination info */ async getUserPayments(userId, filters = {}) { try { const query = { userId }; if (filters.status) { query.status = filters.status; } if (filters.paymentMethod) { query.paymentMethod = filters.paymentMethod; } const [payments, total] = await Promise.all([ models.Payment.find(query) .sort({ createdAt: -1 }) .skip(filters.skip || 0) .limit(filters.limit || 20) .lean(), models.Payment.countDocuments(query) ]); return { success: true, payments, pagination: { total, page: Math.floor((filters.skip || 0) / (filters.limit || 20)) + 1, limit: filters.limit || 20, pages: Math.ceil(total / (filters.limit || 20)) } }; } catch (error) { console.error('Error getting user payments:', error); throw error; } } /** * Get payment statistics * @param {string} userId - User ID (optional, for user-specific stats) * @returns {Promise<Object>} Payment statistics */ async getPaymentStatistics(userId = null) { try { const query = userId ? { userId } : {}; const [ totalPayments, successfulPayments, failedPayments, pendingPayments, totalRevenue, totalRefunds ] = await Promise.all([ models.Payment.countDocuments(query), models.Payment.countDocuments({ ...query, status: 'successful' }), models.Payment.countDocuments({ ...query, status: 'failed' }), models.Payment.countDocuments({ ...query, status: 'pending' }), models.Payment.aggregate([ { $match: { ...query, status: 'successful' } }, { $group: { _id: null, total: { $sum: '$amount' } } } ]), models.Payment.aggregate([ { $match: { ...query, status: 'refunded' } }, { $group: { _id: null, total: { $sum: '$refundAmount' } } } ]) ]); return { success: true, statistics: { totalPayments, successfulPayments, failedPayments, pendingPayments, totalRevenue: totalRevenue[0]?.total || 0, totalRefunds: totalRefunds[0]?.total || 0 } }; } catch (error) { console.error('Error getting payment statistics:', error); throw error; } } /** * Generate payment link (for external payment providers) * @param {string} paymentId - Payment ID * @param {Object} options - Payment options * @returns {Promise<Object>} Payment link data */ async generatePaymentLink(paymentId, options = {}) { try { const payment = await models.Payment.findById(paymentId); if (!payment) { throw new Error('Payment not found'); } // This would integrate with your payment provider (Paystack, etc.) // For now, return a placeholder const paymentLink = { url: `https://checkout.paystack.com/${payment.paymentReference}`, reference: payment.paymentReference, amount: payment.amount, currency: payment.currency, expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours }; return { success: true, paymentLink }; } catch (error) { console.error('Error generating payment link:', error); throw error; } } } export default new PaymentService();