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.

411 lines (364 loc) 14.8 kB
import { models } from '../mongoDB/index.js'; import OrderService from './OrderService.js'; class RentalService { constructor() { this.orderService = OrderService; } /** * Validate rental referee details * @param {Object} refereeDetails - Referee details * @returns {Object} Validation result */ validateRefereeDetails(refereeDetails) { const errors = []; // Required fields const requiredFields = [ 'refereeName', 'refereePhone', 'refereeEmail', 'refereeAddress', 'refereeIdType', 'refereeIdNumber' ]; for (const field of requiredFields) { if (!refereeDetails[field] || refereeDetails[field].trim() === '') { errors.push(`${field} is required`); } } // Email validation if (refereeDetails.refereeEmail) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(refereeDetails.refereeEmail)) { errors.push('Invalid email format'); } } // Phone validation (basic) if (refereeDetails.refereePhone) { const phoneRegex = /^[\+]?[0-9\s\-\(\)]{10,}$/; if (!phoneRegex.test(refereeDetails.refereePhone)) { errors.push('Invalid phone number format'); } } // ID type validation const validIdTypes = ['national_id', 'passport', 'drivers_license', 'voters_card']; if (refereeDetails.refereeIdType && !validIdTypes.includes(refereeDetails.refereeIdType)) { errors.push('Invalid ID type'); } return { valid: errors.length === 0, errors }; } /** * Create rental from order * @param {string} orderId - Order ID * @param {Object} rentalData - Rental data * @returns {Promise<Object>} Created rental */ async createRentalFromOrder(orderId, rentalData) { try { // Get order details const orderResult = await this.orderService.getOrderById(orderId, rentalData.userId); if (!orderResult.success) { throw new Error('Order not found'); } const order = orderResult.order; // Find rental items in the order const rentalItems = order.items.filter(item => item.productSnapshot.category === 'rental' ); if (rentalItems.length === 0) { throw new Error('No rental items found in order'); } // Validate referee details const refereeValidation = this.validateRefereeDetails(rentalData.refereeDetails); if (!refereeValidation.valid) { throw new Error(`Referee validation failed: ${refereeValidation.errors.join(', ')}`); } // Validate rental dates if (!rentalData.startDate || !rentalData.endDate) { throw new Error('Start date and end date are required'); } const startDate = new Date(rentalData.startDate); const endDate = new Date(rentalData.endDate); if (startDate >= endDate) { throw new Error('End date must be after start date'); } if (startDate < new Date()) { throw new Error('Start date cannot be in the past'); } // Create rental records for each item const rentals = []; for (const item of rentalItems) { const rental = await models.CartItem.create({ productId: item.productId, quantity: item.quantity, productSnapshot: item.productSnapshot, subtotal: item.subtotal, options: item.options, rentalDetails: { startDate, endDate, refereeName: rentalData.refereeDetails.refereeName, refereePhone: rentalData.refereeDetails.refereePhone, refereeEmail: rentalData.refereeDetails.refereeEmail, refereeAddress: rentalData.refereeDetails.refereeAddress, refereeIdType: rentalData.refereeDetails.refereeIdType, refereeIdNumber: rentalData.refereeDetails.refereeIdNumber } }); rentals.push(rental); } return { success: true, rentals: rentals.map(rental => ({ id: rental._id, productName: rental.productSnapshot.name, startDate: rental.rentalDetails.startDate, endDate: rental.rentalDetails.endDate, refereeName: rental.rentalDetails.refereeName, status: 'pending_confirmation' })) }; } catch (error) { console.error('Error creating rental from order:', error); throw error; } } /** * Get rental by ID * @param {string} rentalId - Rental ID * @param {string} userId - User ID (for authorization) * @returns {Promise<Object>} Rental data */ async getRentalById(rentalId, userId) { try { const rental = await models.CartItem.findOne({ _id: rentalId, 'rentalDetails.refereeEmail': { $exists: true } }).populate('productId'); if (!rental) { throw new Error('Rental not found'); } // Check if user has access to this rental // This would need to be implemented based on your authorization logic return { success: true, rental: { id: rental._id, productName: rental.productSnapshot.name, productId: rental.productId, quantity: rental.quantity, subtotal: rental.subtotal, startDate: rental.rentalDetails.startDate, endDate: rental.rentalDetails.endDate, refereeName: rental.rentalDetails.refereeName, refereePhone: rental.rentalDetails.refereePhone, refereeEmail: rental.rentalDetails.refereeEmail, refereeAddress: rental.rentalDetails.refereeAddress, refereeIdType: rental.rentalDetails.refereeIdType, refereeIdNumber: rental.rentalDetails.refereeIdNumber, createdAt: rental.createdAt, updatedAt: rental.updatedAt } }; } catch (error) { console.error('Error getting rental:', error); throw error; } } /** * Get user's rentals * @param {string} userId - User ID * @param {Object} filters - Filter options * @returns {Promise<Object>} Rentals and pagination info */ async getUserRentals(userId, filters = {}) { try { // This would need to be implemented based on how you link rentals to users // For now, we'll search by referee email or other identifier const query = { 'rentalDetails.refereeEmail': { $exists: true } }; if (filters.status) { // Add status filtering logic here } const [rentals, total] = await Promise.all([ models.CartItem.find(query) .sort({ 'rentalDetails.startDate': -1 }) .skip(filters.skip || 0) .limit(filters.limit || 20) .lean(), models.CartItem.countDocuments(query) ]); return { success: true, rentals: rentals.map(rental => ({ id: rental._id, productName: rental.productSnapshot.name, startDate: rental.rentalDetails.startDate, endDate: rental.rentalDetails.endDate, refereeName: rental.rentalDetails.refereeName, status: 'pending_confirmation' // This would need proper status tracking })), 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 rentals:', error); throw error; } } /** * Confirm rental * @param {string} rentalId - Rental ID * @param {string} confirmedBy - User ID who confirmed * @returns {Promise<Object>} Updated rental */ async confirmRental(rentalId, confirmedBy) { try { const rental = await models.CartItem.findById(rentalId); if (!rental) { throw new Error('Rental not found'); } if (!rental.rentalDetails) { throw new Error('Not a rental item'); } // Update rental status (you might want to add a status field) rental.updatedAt = new Date(); await rental.save(); return { success: true, rental: { id: rental._id, productName: rental.productSnapshot.name, startDate: rental.rentalDetails.startDate, endDate: rental.rentalDetails.endDate, status: 'confirmed', updatedAt: rental.updatedAt } }; } catch (error) { console.error('Error confirming rental:', error); throw error; } } /** * Check rental availability * @param {string} productId - Product ID * @param {Date} startDate - Start date * @param {Date} endDate - End date * @returns {Promise<Object>} Availability status */ async checkRentalAvailability(productId, startDate, endDate) { try { // Check if product exists and is available const product = await models.Product.findById(productId); if (!product || !product.isActive) { return { available: false, reason: 'Product not available' }; } // Check for overlapping rentals const overlappingRentals = await models.CartItem.find({ productId, 'rentalDetails.startDate': { $lte: endDate }, 'rentalDetails.endDate': { $gte: startDate } }); const totalRentedQuantity = overlappingRentals.reduce( (sum, rental) => sum + rental.quantity, 0 ); const availableQuantity = product.stock - totalRentedQuantity; return { available: availableQuantity > 0, availableQuantity, totalStock: product.stock, rentedQuantity: totalRentedQuantity }; } catch (error) { console.error('Error checking rental availability:', error); return { available: false, reason: 'Error checking availability' }; } } /** * Get rental statistics * @param {string} userId - User ID (optional, for user-specific stats) * @returns {Promise<Object>} Rental statistics */ async getRentalStatistics(userId = null) { try { const query = { 'rentalDetails.refereeEmail': { $exists: true } }; const [ totalRentals, activeRentals, completedRentals ] = await Promise.all([ models.CartItem.countDocuments(query), models.CartItem.countDocuments({ ...query, 'rentalDetails.startDate': { $lte: new Date() }, 'rentalDetails.endDate': { $gte: new Date() } }), models.CartItem.countDocuments({ ...query, 'rentalDetails.endDate': { $lt: new Date() } }) ]); return { success: true, statistics: { totalRentals, activeRentals, completedRentals } }; } catch (error) { console.error('Error getting rental statistics:', error); throw error; } } /** * Get upcoming rentals * @param {number} days - Number of days ahead to check * @returns {Promise<Object>} Upcoming rentals */ async getUpcomingRentals(days = 7) { try { const startDate = new Date(); const endDate = new Date(); endDate.setDate(endDate.getDate() + days); const rentals = await models.CartItem.find({ 'rentalDetails.refereeEmail': { $exists: true }, 'rentalDetails.startDate': { $gte: startDate, $lte: endDate } }).sort({ 'rentalDetails.startDate': 1 }).lean(); return { success: true, rentals: rentals.map(rental => ({ id: rental._id, productName: rental.productSnapshot.name, startDate: rental.rentalDetails.startDate, endDate: rental.rentalDetails.endDate, refereeName: rental.rentalDetails.refereeName, refereePhone: rental.rentalDetails.refereePhone })) }; } catch (error) { console.error('Error getting upcoming rentals:', error); throw error; } } } export default new RentalService();