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.

201 lines (168 loc) 7.01 kB
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; } }