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