@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
JavaScript
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
}
}