@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
JavaScript
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();