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.

139 lines (120 loc) 5.13 kB
import { mongoose, models } from "../mongoDB/index.js"; import { StockService } from "./StockService.js"; import cron from "node-cron"; /** * RentalSessionManager (MVP) * ---------------------------------------------- * Handles automatic status transitions for: * - Equipment rentals (Order.items where productType === 'equipment_rental') * - Studio & makeover sessions (Booking collection) * * This first iteration focuses ONLY on completing / overdue logic and stock restoration. * Notification / email triggers are left as TODO placeholders to be wired into the * upcoming action-based notification system. */ // Transition constants const ORDER_ACTIVE_STATUSES = ["in_progress", "ready_for_pickup"]; const BOOKING_ACTIVE_STATUSES = ["in_progress", "confirmed"]; // Grace period defaults (in minutes) const DEFAULT_RENTAL_GRACE_MINUTES = 120; /** * Process expirations and perform necessary transitions. * @param {Date} [now] Optional date for deterministic testing */ export async function processExpirations(now = new Date()) { // Check if MongoDB supports transactions (replica set or sharded cluster) const supportsTransactions = mongoose.connection.readyState === 1 && (mongoose.connection.db?.serverConfig?.isReplicaSet || mongoose.connection.db?.serverConfig?.isMongos); if (supportsTransactions) { // Use transactions for production safety const session = await mongoose.startSession(); try { await session.withTransaction(async () => { await _processRentalOrders(now, session); await _processServiceBookings(now, session); }); } catch (err) { console.error("RentalSessionManager transaction error", err); } finally { await session.endSession(); } } else { // Fallback for development (standalone MongoDB) try { await _processRentalOrders(now, null); await _processServiceBookings(now, null); } catch (err) { console.error("RentalSessionManager error", err); } } } /** * Schedule cron job that periodically runs processExpirations. * Runs every 15 minutes. */ export function scheduleJobs() { cron.schedule("*/15 * * * *", async () => { await processExpirations(); }, { timezone: "Africa/Lagos" }); } /* -------------------------------------------------------------------------- */ /* Internals */ /* -------------------------------------------------------------------------- */ async function _processRentalOrders(now, session) { // Orders whose items include equipment_rental and are past endDate const query = models.Order.find({ status: { $in: ORDER_ACTIVE_STATUSES }, "items.productType": "equipment_rental", "items.rentalPeriod.endDate": { $lt: now }, }); const orders = session ? await query.session(session) : await query; for (const order of orders) { // Determine grace window per item; if any item overdue beyond grace -> overdue let orderOverdue = false; for (const item of order.items.filter(i => i.productType === "equipment_rental")) { const end = item.rentalPeriod?.endDate; if (!end) continue; const grace = item.rentalPeriod?.graceMinutes ?? DEFAULT_RENTAL_GRACE_MINUTES; const overdueTime = new Date(end.getTime() + grace * 60 * 1000); if (now > overdueTime) { orderOverdue = true; break; } } // Update order status & restore stock const newStatus = orderOverdue ? "overdue" : "completed"; order.status = newStatus; // Release reservation stock await StockService.releaseReservation(order._id, order.items); // Metadata & audit log order.metadata.autoTransitioned = true; order.metadata.autoTransitionedAt = now; await order.save(session ? { session } : {}); // TODO notify:order_auto_completed or order_overdue } } async function _processServiceBookings(now, session) { // Bookings const query = models.Booking.find({ status: { $in: BOOKING_ACTIVE_STATUSES }, $expr: { $lt: [ { $add: ["$preferredDate", { $multiply: ["$duration", 60 * 60 * 1000] }], }, now, ], }, }); const bookings = session ? await query.session(session) : await query; for (const bk of bookings) { bk.status = "completed"; // For MVP, set completed directly bk.completedAt = now; bk.metadata = bk.metadata || {}; bk.metadata.autoTransitioned = true; bk.metadata.autoTransitionedAt = now; await bk.save(session ? { session } : {}); // TODO notify:booking_auto_completed } }