@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.
201 lines (168 loc) • 7.01 kB
JavaScript
import { models } from '../../mongoDB/index.js';
/**
* Shared Booking Service
* Contains core business logic for bookings to be used by both Admin and Client servers.
*/
export class SharedBookingService {
/**
* Calculate booking total amount
* @param {Object} bookingData - Booking data including product, duration, and typeSpecificData
* @returns {number} Total amount
*/
static calculateTotalAmount(bookingData) {
const { product, duration, typeSpecificData } = bookingData;
if (!product || !duration) {
return 0;
}
let baseAmount = product.price || product.pricing?.baseRate || 0;
let totalAmount = baseAmount * duration;
// Add type-specific calculations
if (typeSpecificData) {
// Equipment rental specific
if (bookingData.bookingType === 'equipment_rental' && typeSpecificData.rentalPeriod) {
const { securityDeposit = 0 } = typeSpecificData.rentalPeriod;
totalAmount += securityDeposit;
}
// Studio session specific
if (bookingData.bookingType === 'studio_session' && typeSpecificData.studioSessionDetails) {
const { deposit = 0, isRush = false } = typeSpecificData.studioSessionDetails;
totalAmount += deposit;
if (isRush) {
totalAmount *= 1.5; // 50% rush fee
}
}
}
return Math.round(totalAmount);
}
/**
* Check for booking conflicts
* @param {Object} bookingData - Booking data
* @param {string} excludeId - Booking ID to exclude from check (for updates)
* @returns {Promise<Object>} Conflict result
*/
static async checkBookingConflicts(bookingData, excludeId = null) {
const { product, date, time, duration, bookingType } = bookingData;
if (!product || !date || !time || !duration) {
return { hasConflict: false, conflicts: [] };
}
const startTime = new Date(`${date}T${time}`);
const endTime = new Date(startTime.getTime() + duration * 60 * 60 * 1000);
const query = {
product,
bookingType,
status: { $in: ['confirmed', 'preparing', 'in_progress'] },
$or: [
{
date: date,
$expr: {
$and: [
{ $lt: [{ $dateFromString: { dateString: { $concat: [{ $dateToString: { date: '$date' } }, 'T', '$time'] } } }, endTime] },
{ $gt: [{ $add: [{ $dateFromString: { dateString: { $concat: [{ $dateToString: { date: '$date' } }, 'T', '$time'] } } }, { $multiply: ['$duration', 3600000] }] }, startTime] }
]
}
}
]
};
if (excludeId) {
query._id = { $ne: excludeId };
}
const conflicts = await models.Booking.find(query)
.populate('client', 'name email')
.select('_id client date time duration status');
return {
hasConflict: conflicts.length > 0,
conflicts: conflicts.map(conflict => ({
id: conflict._id,
client: conflict.client?.name || 'Unknown',
date: conflict.date,
time: conflict.time,
duration: conflict.duration,
status: conflict.status
}))
};
}
/**
* Generate booking reference
* @param {string} bookingType - Type of booking
* @returns {string} Booking reference
*/
static generateBookingReference(bookingType) {
const prefix = {
'equipment_rental': 'EQ',
'studio_session': 'ST',
'event_coverage': 'EV'
}[bookingType] || 'BK';
const timestamp = Date.now().toString().slice(-6);
const random = Math.random().toString(36).substr(2, 4).toUpperCase();
return `${prefix}${timestamp}${random}`;
}
/**
* Validate booking data
* @param {Object} bookingData - Booking data to validate
* @returns {Object} Validation result { isValid, errors }
*/
static validateBookingData(bookingData) {
const errors = [];
// Required fields
const requiredFields = ['client', 'product', 'bookingType', 'date', 'time', 'duration'];
requiredFields.forEach(field => {
if (!bookingData[field]) {
errors.push(`${field} is required`);
}
});
// Validate booking type
const validBookingTypes = ['equipment_rental', 'studio_session', 'event_coverage'];
if (bookingData.bookingType && !validBookingTypes.includes(bookingData.bookingType)) {
errors.push(`Invalid booking type. Must be one of: ${validBookingTypes.join(', ')}`);
}
// Validate date
if (bookingData.date && !this.isValidDate(bookingData.date)) {
errors.push('Invalid date format. Use YYYY-MM-DD');
}
// Validate time
if (bookingData.time && !this.isValidTime(bookingData.time)) {
errors.push('Invalid time format. Use HH:MM');
}
// Validate duration
if (bookingData.duration && (isNaN(bookingData.duration) || bookingData.duration <= 0)) {
errors.push('Duration must be a positive number');
}
return {
isValid: errors.length === 0,
errors
};
}
// Helper methods
static isValidDate(dateString) {
const regex = /^\d{4}-\d{2}-\d{2}$/;
if (!regex.test(dateString)) return false;
const date = new Date(dateString);
return date instanceof Date && !isNaN(date) && dateString === date.toISOString().split('T')[0];
}
static isValidTime(timeString) {
const regex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
return regex.test(timeString);
}
/**
* Calculate late fees
* @param {Object} booking - Booking object
* @returns {number} Late fee amount
*/
static calculateLateFees(booking) {
if (booking.bookingType !== 'equipment_rental') {
return 0;
}
const { rentalPeriod } = booking.typeSpecificData || {};
if (!rentalPeriod?.endDate) {
return 0;
}
const endDate = new Date(rentalPeriod.endDate);
const now = new Date();
if (now <= endDate) {
return 0;
}
const daysLate = Math.ceil((now - endDate) / (1000 * 60 * 60 * 24));
const dailyLateFee = booking.product?.dailyLateFee || 5000; // Default ₦5000 per day
return daysLate * dailyLateFee;
}
}