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.

886 lines (749 loc) 31.5 kB
/** * Product validation utilities for backend GraphQL operations * Provides comprehensive validation for equipment rental and product data */ import mongoose from 'mongoose'; /** * Validate complete product input data * @param {Object} input - Product input data * @param {boolean} isUpdate - Whether this is an update operation * @returns {Object} Validation result */ export const validateProductInput = (input, isUpdate = false) => { const errors = []; const warnings = []; try { // Basic field validation if (!isUpdate) { validateRequiredFields(input, errors); } // Type-specific validation if (input.type) { validateProductType(input, errors, warnings); } // Pricing validation if (input.pricing) { validatePricing(input.pricing, errors, warnings); } // Inventory validation if (input.inventory) { validateInventory(input.inventory, input.type, errors, warnings); } // Specifications validation if (input.specifications) { validateSpecifications(input.specifications, input.category, errors, warnings); } // Availability rules validation if (input.availabilityRules) { validateAvailabilityRules(input.availabilityRules, errors, warnings); } // Business logic validation validateBusinessRules(input, errors, warnings); return { isValid: errors.length === 0, errors, warnings }; } catch (error) { return { isValid: false, errors: [`Validation error: ${error.message}`], warnings: [] }; } }; /** * Validate required fields based on product type */ function validateRequiredFields(input, errors) { const requiredFields = ['name', 'description', 'type', 'category']; requiredFields.forEach(field => { if (!input[field] || (typeof input[field] === 'string' && input[field].trim() === '')) { errors.push(`${field} is required`); } }); // Name length validation if (input.name && input.name.length < 3) { errors.push('Product name must be at least 3 characters long'); } if (input.name && input.name.length > 100) { errors.push('Product name must be less than 100 characters'); } // Description length validation if (input.description && input.description.length < 10) { errors.push('Product description must be at least 10 characters long'); } if (input.description && input.description.length > 2000) { errors.push('Product description must be less than 2000 characters'); } } /** * Validate product type and related requirements */ function validateProductType(input, errors, warnings) { const validTypes = ['equipment_rental', 'mini_mart_sale', 'service_booking']; if (!validTypes.includes(input.type)) { errors.push(`Invalid product type. Must be one of: ${validTypes.join(', ')}`); return; } // Type-specific requirements switch (input.type) { case 'equipment_rental': validateEquipmentRentalRequirements(input, errors, warnings); break; case 'mini_mart_sale': validateMinimartSaleRequirements(input, errors, warnings); break; case 'service_booking': validateServiceBookingRequirements(input, errors, warnings); break; } } /** * Validate equipment rental specific requirements - MVP minimal approach */ function validateEquipmentRentalRequirements(input, errors, warnings) { // Only require baseRate for pricing - everything else can be defaulted if (input.pricing && input.pricing.baseRate !== undefined) { if (input.pricing.baseRate < 0) { errors.push('Base rate cannot be negative'); } } // Inventory validation is now optional - system will provide defaults if (input.inventory) { // Only validate if inventory data is provided if (input.inventory.totalUnits !== undefined && input.inventory.totalUnits < 1) { errors.push('Total units must be at least 1'); } if (input.inventory.availableUnits !== undefined && input.inventory.totalUnits !== undefined) { if (input.inventory.availableUnits > input.inventory.totalUnits) { errors.push('Available units cannot exceed total units'); } } } // Validate equipment categories - expanded list for broader equipment support const validEquipmentCategories = [ 'cameras', 'lenses', 'tripods', 'lighting', 'audio', 'accessories', 'stabilizers', 'filters', 'memory_cards', 'batteries', 'bags', 'other', 'monitors', 'computers', 'projectors', 'microphones', 'stands', 'cables', 'adapters', 'tools', 'cases' ]; if (input.category && !validEquipmentCategories.includes(input.category)) { errors.push(`Invalid equipment category. Must be one of: ${validEquipmentCategories.join(', ')}`); } // Specifications are completely optional for MVP // No warnings for missing specifications } /** * Validate minimart sale requirements */ function validateMinimartSaleRequirements(input, errors, warnings) { if (!input.inventory) { errors.push('Inventory information is required for minimart sales'); } else { if (input.inventory.stockQuantity === undefined) { errors.push('Stock quantity is required for minimart sales'); } if (input.inventory.stockQuantity < 0) { errors.push('Stock quantity cannot be negative'); } if (input.inventory.lowStockThreshold === undefined) { warnings.push('Low stock threshold is recommended for inventory management'); } } // Pricing should be simple for minimart items if (input.pricing && input.pricing.rateType !== 'fixed') { warnings.push('Minimart items typically use fixed pricing'); } } /** * Validate service booking requirements */ function validateServiceBookingRequirements(input, errors, warnings) { if (!input.pricing) { errors.push('Pricing information is required for service bookings'); } // Services don't need physical inventory if (input.inventory && (input.inventory.totalUnits || input.inventory.stockQuantity)) { warnings.push('Services typically do not require physical inventory tracking'); } } /** * Validate pricing information - MVP minimal approach */ function validatePricing(pricing, errors, warnings) { // Only validate baseRate if provided - it's required for equipment rentals if (pricing.baseRate !== undefined && pricing.baseRate !== null) { if (pricing.baseRate < 0) { errors.push('Base rate cannot be negative'); } if (pricing.baseRate === 0) { warnings.push('Zero base rate - confirm this is intentional'); } if (pricing.baseRate > 1000000) { warnings.push('Very high base rate - please verify'); } } // Rate type validation - only if provided const validRateTypes = ['hourly', 'daily', 'weekly', 'monthly', 'fixed']; if (pricing.rateType && !validRateTypes.includes(pricing.rateType)) { errors.push(`Invalid rate type. Must be one of: ${validRateTypes.join(', ')}`); } // Currency validation - only if provided const validCurrencies = ['NGN', 'USD', 'EUR', 'GBP']; if (pricing.currency && !validCurrencies.includes(pricing.currency)) { warnings.push(`Unusual currency: ${pricing.currency}. Supported currencies: ${validCurrencies.join(', ')}`); } // Optional advanced pricing features - only validate if provided if (pricing.securityDeposit !== undefined) { if (pricing.securityDeposit < 0) { errors.push('Security deposit cannot be negative'); } } // Discount validation - only if provided if (pricing.discounts && Array.isArray(pricing.discounts)) { pricing.discounts.forEach((discount, index) => { validateDiscount(discount, index, errors, warnings); }); } // Seasonal pricing validation - only if provided if (pricing.seasonalPricing && Array.isArray(pricing.seasonalPricing)) { pricing.seasonalPricing.forEach((season, index) => { validateSeasonalPricing(season, index, errors, warnings); }); } } /** * Validate discount information */ function validateDiscount(discount, index, errors, warnings) { if (!discount.type) { errors.push(`Discount ${index + 1}: Type is required`); } if (discount.value === undefined || discount.value === null) { errors.push(`Discount ${index + 1}: Value is required`); } else { if (discount.value < 0) { errors.push(`Discount ${index + 1}: Value cannot be negative`); } if (discount.type === 'percentage' && discount.value > 100) { errors.push(`Discount ${index + 1}: Percentage discount cannot exceed 100%`); } if (discount.type === 'percentage' && discount.value > 50) { warnings.push(`Discount ${index + 1}: High percentage discount (${discount.value}%) - please verify`); } } // Date validation if (discount.validFrom && discount.validUntil) { const startDate = new Date(discount.validFrom); const endDate = new Date(discount.validUntil); if (startDate >= endDate) { errors.push(`Discount ${index + 1}: End date must be after start date`); } if (endDate < new Date()) { warnings.push(`Discount ${index + 1}: Discount has already expired`); } } } /** * Validate seasonal pricing */ function validateSeasonalPricing(season, index, errors, warnings) { if (!season.name) { errors.push(`Seasonal pricing ${index + 1}: Name is required`); } if (season.multiplier === undefined || season.multiplier === null) { errors.push(`Seasonal pricing ${index + 1}: Multiplier is required`); } else { if (season.multiplier <= 0) { errors.push(`Seasonal pricing ${index + 1}: Multiplier must be positive`); } if (season.multiplier > 5) { warnings.push(`Seasonal pricing ${index + 1}: Very high multiplier (${season.multiplier}x) - please verify`); } } // Date format validation (MM-DD) const dateRegex = /^(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; if (season.startDate && !dateRegex.test(season.startDate)) { errors.push(`Seasonal pricing ${index + 1}: Start date must be in MM-DD format`); } if (season.endDate && !dateRegex.test(season.endDate)) { errors.push(`Seasonal pricing ${index + 1}: End date must be in MM-DD format`); } } /** * Validate inventory information - MVP minimal approach */ function validateInventory(inventory, productType, errors, warnings) { // Only validate if inventory data is provided - system will provide defaults if (!inventory) return; // Equipment rental inventory validation - only if provided if (productType === 'equipment_rental') { if (inventory.totalUnits !== undefined) { if (inventory.totalUnits < 1) { errors.push('Total units must be at least 1 for equipment'); } if (inventory.totalUnits > 100) { warnings.push('Large equipment inventory - ensure this is correct'); } } if (inventory.availableUnits !== undefined && inventory.totalUnits !== undefined) { if (inventory.availableUnits > inventory.totalUnits) { errors.push('Available units cannot exceed total units'); } if (inventory.availableUnits < 0) { errors.push('Available units cannot be negative'); } } // Serial numbers validation - only if provided if (inventory.serialNumbers && Array.isArray(inventory.serialNumbers)) { const uniqueSerials = new Set(inventory.serialNumbers); if (uniqueSerials.size !== inventory.serialNumbers.length) { errors.push('Serial numbers must be unique'); } inventory.serialNumbers.forEach((serial, index) => { if (!serial || typeof serial !== 'string' || serial.trim() === '') { errors.push(`Serial number ${index + 1} cannot be empty`); } }); } } // Minimart inventory validation - only if provided if (productType === 'mini_mart_sale') { if (inventory.stockQuantity !== undefined) { if (inventory.stockQuantity < 0) { errors.push('Stock quantity cannot be negative'); } } if (inventory.lowStockThreshold !== undefined) { if (inventory.lowStockThreshold < 0) { errors.push('Low stock threshold cannot be negative'); } if (inventory.stockQuantity !== undefined && inventory.lowStockThreshold > inventory.stockQuantity) { warnings.push('Low stock threshold is higher than current stock quantity'); } } } // Condition validation - only if provided const validConditions = ['excellent', 'good', 'fair', 'needs_repair']; if (inventory.condition && !validConditions.includes(inventory.condition)) { errors.push(`Invalid condition. Must be one of: ${validConditions.join(', ')}`); } if (inventory.condition === 'needs_repair' && productType === 'equipment_rental') { warnings.push('Equipment marked as needing repair - ensure it is not available for rental'); } } /** * Validate product specifications */ function validateSpecifications(specifications, category, errors, warnings) { // Brand validation if (specifications.brand) { if (specifications.brand.length < 2) { errors.push('Brand name must be at least 2 characters long'); } if (specifications.brand.length > 50) { errors.push('Brand name must be less than 50 characters'); } } // Model validation if (specifications.model) { if (specifications.model.length > 100) { errors.push('Model name must be less than 100 characters'); } } // Year validation if (specifications.year !== undefined) { const currentYear = new Date().getFullYear(); if (specifications.year < 1900 || specifications.year > currentYear + 1) { errors.push(`Year must be between 1900 and ${currentYear + 1}`); } if (specifications.year < currentYear - 20) { warnings.push('Very old equipment - consider noting age in description'); } } // Category-specific validations if (category === 'cameras') { validateCameraSpecs(specifications, errors, warnings); } else if (category === 'lenses') { validateLensSpecs(specifications, errors, warnings); } // Dimensions validation if (specifications.dimensions) { validateDimensions(specifications.dimensions, errors, warnings); } // Custom specs validation if (specifications.customSpecs && Array.isArray(specifications.customSpecs)) { specifications.customSpecs.forEach((spec, index) => { if (!spec.name || !spec.value) { errors.push(`Custom specification ${index + 1}: Name and value are required`); } }); } } /** * Validate camera-specific specifications */ function validateCameraSpecs(specs, errors, warnings) { if (specs.megapixels !== undefined) { if (specs.megapixels < 1 || specs.megapixels > 200) { errors.push('Megapixels must be between 1 and 200'); } if (specs.megapixels < 10) { warnings.push('Low megapixel count - ensure this is accurate for modern equipment'); } } if (specs.sensor) { const validSensorTypes = ['full-frame', 'aps-c', 'micro-four-thirds', 'medium-format', '35mm']; const sensorLower = specs.sensor.toLowerCase(); if (!validSensorTypes.some(type => sensorLower.includes(type))) { warnings.push('Unusual sensor type - please verify accuracy'); } } } /** * Validate lens-specific specifications */ function validateLensSpecs(specs, errors, warnings) { if (specs.focalLength) { // Basic focal length format validation const focalLengthPattern = /^\d+(-\d+)?(mm)?$/i; if (!focalLengthPattern.test(specs.focalLength.replace(/\s/g, ''))) { warnings.push('Focal length format may be incorrect (expected format: "50mm" or "24-70mm")'); } } if (specs.aperture) { // Basic aperture format validation const aperturePattern = /^f\/?\d+(\.\d+)?(-\d+(\.\d+)?)?$/i; if (!aperturePattern.test(specs.aperture.replace(/\s/g, ''))) { warnings.push('Aperture format may be incorrect (expected format: "f/1.8" or "f/2.8-5.6")'); } } } /** * Validate dimensions */ function validateDimensions(dimensions, errors, warnings) { ['length', 'width', 'height'].forEach(dimension => { if (dimensions[dimension] !== undefined) { if (dimensions[dimension] < 0) { errors.push(`${dimension} cannot be negative`); } if (dimensions[dimension] > 1000) { warnings.push(`Very large ${dimension} (${dimensions[dimension]}cm) - please verify`); } } }); if (!dimensions.unit) { warnings.push('Dimension unit not specified - defaulting to cm'); } else { const validUnits = ['cm', 'mm', 'in', 'm']; if (!validUnits.includes(dimensions.unit)) { errors.push(`Invalid dimension unit. Must be one of: ${validUnits.join(', ')}`); } } } /** * Validate availability rules */ function validateAvailabilityRules(rules, errors, warnings) { // Rental period validation if (rules.minRentalPeriod !== undefined) { if (rules.minRentalPeriod < 1) { errors.push('Minimum rental period must be at least 1 hour'); } } if (rules.maxRentalPeriod !== undefined) { if (rules.maxRentalPeriod < 1) { errors.push('Maximum rental period must be at least 1 hour'); } if (rules.minRentalPeriod && rules.maxRentalPeriod < rules.minRentalPeriod) { errors.push('Maximum rental period cannot be less than minimum rental period'); } if (rules.maxRentalPeriod > 8760) { // 365 days warnings.push('Very long maximum rental period (over 1 year) - please verify'); } } if (rules.advanceBookingRequired !== undefined) { if (rules.advanceBookingRequired < 0) { errors.push('Advance booking requirement cannot be negative'); } if (rules.advanceBookingRequired > 8760) { warnings.push('Very long advance booking requirement - please verify'); } } // Blackout dates validation if (rules.blackoutDates && Array.isArray(rules.blackoutDates)) { rules.blackoutDates.forEach((date, index) => { try { const blackoutDate = new Date(date); if (isNaN(blackoutDate.getTime())) { errors.push(`Blackout date ${index + 1}: Invalid date format`); } else if (blackoutDate < new Date()) { warnings.push(`Blackout date ${index + 1}: Date is in the past`); } } catch (error) { errors.push(`Blackout date ${index + 1}: Invalid date`); } }); } // Maintenance schedule validation if (rules.maintenanceSchedule && Array.isArray(rules.maintenanceSchedule)) { rules.maintenanceSchedule.forEach((schedule, index) => { validateMaintenanceSchedule(schedule, index, errors, warnings); }); } } /** * Validate maintenance schedule */ function validateMaintenanceSchedule(schedule, index, errors, warnings) { if (!schedule.startDate) { errors.push(`Maintenance schedule ${index + 1}: Start date is required`); } if (!schedule.endDate) { errors.push(`Maintenance schedule ${index + 1}: End date is required`); } if (schedule.startDate && schedule.endDate) { const startDate = new Date(schedule.startDate); const endDate = new Date(schedule.endDate); if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { errors.push(`Maintenance schedule ${index + 1}: Invalid date format`); } else { if (startDate >= endDate) { errors.push(`Maintenance schedule ${index + 1}: End date must be after start date`); } if (endDate < new Date()) { warnings.push(`Maintenance schedule ${index + 1}: Maintenance period is in the past`); } const duration = (endDate - startDate) / (1000 * 60 * 60 * 24); // days if (duration > 365) { warnings.push(`Maintenance schedule ${index + 1}: Very long maintenance period (${Math.round(duration)} days)`); } } } if (!schedule.reason) { warnings.push(`Maintenance schedule ${index + 1}: Reason not provided`); } if (schedule.recurring && !schedule.interval) { errors.push(`Maintenance schedule ${index + 1}: Interval required for recurring maintenance`); } if (schedule.interval) { const validIntervals = ['daily', 'weekly', 'monthly', 'yearly']; if (!validIntervals.includes(schedule.interval)) { errors.push(`Maintenance schedule ${index + 1}: Invalid interval. Must be one of: ${validIntervals.join(', ')}`); } } } /** * Validate business rules and logic */ function validateBusinessRules(input, errors, warnings) { // SKU validation if (input.sku) { const skuPattern = /^[A-Z]{2,3}-[A-Z]{2,3}-\d{6}-[A-Z0-9]{3}$/; if (!skuPattern.test(input.sku)) { warnings.push('SKU format does not match standard pattern (XX-XXX-XXXXXX-XXX)'); } } // Tags validation if (input.tags && Array.isArray(input.tags)) { if (input.tags.length > 20) { warnings.push('Many tags specified - consider reducing for better organization'); } input.tags.forEach((tag, index) => { if (!tag || typeof tag !== 'string' || tag.trim() === '') { errors.push(`Tag ${index + 1} is invalid`); } else if (tag.length > 50) { errors.push(`Tag ${index + 1} is too long (max 50 characters)`); } }); // Check for duplicate tags const uniqueTags = new Set(input.tags.map(tag => tag.toLowerCase())); if (uniqueTags.size !== input.tags.length) { warnings.push('Duplicate tags detected'); } } // Features validation if (input.features && Array.isArray(input.features)) { if (input.features.length > 50) { warnings.push('Many features listed - consider grouping similar features'); } input.features.forEach((feature, index) => { if (!feature || typeof feature !== 'string' || feature.trim() === '') { errors.push(`Feature ${index + 1} is invalid`); } else if (feature.length > 200) { errors.push(`Feature ${index + 1} is too long (max 200 characters)`); } }); } // What's included validation if (input.whatsIncluded && Array.isArray(input.whatsIncluded)) { input.whatsIncluded.forEach((item, index) => { if (!item || typeof item !== 'string' || item.trim() === '') { errors.push(`What's included item ${index + 1} is invalid`); } }); } // Requirements validation if (input.requirements && Array.isArray(input.requirements)) { input.requirements.forEach((requirement, index) => { if (!requirement || typeof requirement !== 'string' || requirement.trim() === '') { errors.push(`Requirement ${index + 1} is invalid`); } }); } // Usage instructions validation if (input.usageInstructions) { if (input.usageInstructions.length > 5000) { errors.push('Usage instructions are too long (max 5000 characters)'); } } } /** * Validate MongoDB ObjectId * @param {string} id - ID to validate * @returns {boolean} Whether the ID is valid */ export const validateObjectId = (id) => { return mongoose.Types.ObjectId.isValid(id); }; /** * Validate product availability for booking * @param {Object} product - Product object * @param {Date} startDate - Booking start date * @param {Date} endDate - Booking end date * @param {number} quantity - Requested quantity * @returns {Object} Availability validation result */ export const validateProductAvailability = (product, startDate, endDate, quantity = 1) => { const errors = []; const warnings = []; try { // Basic product checks if (!product.isActive) { errors.push('Product is not active'); } if (product.type !== 'equipment_rental') { errors.push('Product is not available for rental'); } // Inventory checks if (product.inventory.availableUnits < quantity) { errors.push(`Not enough units available (requested: ${quantity}, available: ${product.inventory.availableUnits})`); } // Date validation const now = new Date(); if (startDate <= now) { errors.push('Start date must be in the future'); } if (endDate <= startDate) { errors.push('End date must be after start date'); } // Availability rules checks if (product.availabilityRules) { const rules = product.availabilityRules; // Check minimum rental period if (rules.minRentalPeriod) { const rentalHours = (endDate - startDate) / (1000 * 60 * 60); if (rentalHours < rules.minRentalPeriod) { errors.push(`Rental period too short (minimum: ${rules.minRentalPeriod} hours)`); } } // Check maximum rental period if (rules.maxRentalPeriod) { const rentalHours = (endDate - startDate) / (1000 * 60 * 60); if (rentalHours > rules.maxRentalPeriod) { errors.push(`Rental period too long (maximum: ${rules.maxRentalPeriod} hours)`); } } // Check advance booking requirement if (rules.advanceBookingRequired) { const hoursUntilStart = (startDate - now) / (1000 * 60 * 60); if (hoursUntilStart < rules.advanceBookingRequired) { errors.push(`Booking must be made at least ${rules.advanceBookingRequired} hours in advance`); } } // Check blackout dates if (rules.blackoutDates && Array.isArray(rules.blackoutDates)) { const hasBlackoutConflict = rules.blackoutDates.some(blackoutDate => { const blackout = new Date(blackoutDate); return blackout >= startDate && blackout <= endDate; }); if (hasBlackoutConflict) { errors.push('Requested dates conflict with blackout periods'); } } // Check maintenance schedule if (rules.maintenanceSchedule && Array.isArray(rules.maintenanceSchedule)) { const hasMaintenanceConflict = rules.maintenanceSchedule.some(schedule => { const maintenanceStart = new Date(schedule.startDate); const maintenanceEnd = new Date(schedule.endDate); return (maintenanceStart <= endDate && maintenanceEnd >= startDate); }); if (hasMaintenanceConflict) { errors.push('Requested dates conflict with scheduled maintenance'); } } } // Condition check if (product.inventory.condition === 'needs_repair') { errors.push('Equipment needs repair and is not available for rental'); } if (product.inventory.condition === 'fair') { warnings.push('Equipment condition is fair - customer should be informed'); } return { isAvailable: errors.length === 0, errors, warnings }; } catch (error) { return { isAvailable: false, errors: [`Availability validation error: ${error.message}`], warnings: [] }; } }; /** * Validate image upload data * @param {Array} images - Array of image upload objects * @returns {Object} Validation result */ export const validateImageUploads = (images) => { const errors = []; const warnings = []; if (!Array.isArray(images)) { errors.push('Images must be provided as an array'); return { isValid: false, errors, warnings }; } if (images.length === 0) { warnings.push('No images provided'); return { isValid: true, errors, warnings }; } if (images.length > 3) { errors.push('Maximum 3 images allowed per product'); } images.forEach((image, index) => { if (!image.file) { errors.push(`Image ${index + 1}: File is required`); } if (image.alt && image.alt.length > 200) { warnings.push(`Image ${index + 1}: Alt text is very long`); } if (image.order !== undefined && (image.order < 0 || image.order > 10)) { errors.push(`Image ${index + 1}: Order must be between 0 and 10`); } }); return { isValid: errors.length === 0, errors, warnings }; }; export default { validateProductInput, validateObjectId, validateProductAvailability, validateImageUploads };