const mongoose = require("mongoose"); const Order = require("../models/Order"); const User = require("../models/User"); const Course = require("../models/Course"); const AuditLog = require("../models/AuditLog"); const { sendPaymentConfirmedEmail } = require("./email"); const logger = require("./logger"); /** * The single source of truth for "an order has been paid — unlock the * course and tell the customer." Used by both: * - admin.controller.js's confirmOrder (a human admin manually * reviewing a bank-wire/manual payment reference), and * - webhook.controller.js's Payoneer webhook handler (an automated * Payoneer Checkout payment confirmation) * * so that access-granting and the confirmation email can never drift * out of sync between the manual and automated payment paths. * * `confirmedBy` is a User id for a manual admin confirmation, or null * for an automated one (the order's paymentMethod already records how * it was paid). * * Idempotent: safe to call more than once for the same order (a * webhook can legitimately be retried/redelivered by the provider) — * an already-confirmed order is left untouched and simply returned. */ async function confirmOrderAndGrantAccess({ orderId, confirmedBy = null, ip = null, userAgent = null }) { const session = await mongoose.startSession(); try { let resultOrder; let resultUser; let course; let alreadyConfirmed = false; await session.withTransaction(async () => { const order = await Order.findById(orderId).session(session); if (!order) throw Object.assign(new Error("Order not found."), { status: 404 }); if (order.status === "confirmed") { alreadyConfirmed = true; resultOrder = order; return; } if (!["awaiting_confirmation", "pending_payment"].includes(order.status)) { throw Object.assign(new Error("Order cannot be confirmed from its current status."), { status: 409 }); } order.status = "confirmed"; order.confirmedBy = confirmedBy; order.confirmedAt = new Date(); await order.save({ session }); const user = await User.findById(order.user).session(session); if (!user) throw Object.assign(new Error("Associated user not found."), { status: 404 }); const alreadyEnrolled = user.enrolledCourses.some((ec) => ec.course.toString() === order.course.toString()); if (!alreadyEnrolled) { user.enrolledCourses.push({ course: order.course, order: order._id, accessGrantedAt: new Date() }); await user.save({ session }); } course = await Course.findById(order.course).session(session); resultOrder = order; resultUser = user; }); if (alreadyConfirmed) { return { order: resultOrder, alreadyConfirmed: true }; } await AuditLog.create({ actor: confirmedBy, actorEmail: confirmedBy ? undefined : "system:payoneer-webhook", action: confirmedBy ? "admin.order_confirmed" : "system.order_auto_confirmed", targetType: "Order", targetId: resultOrder._id, ip, userAgent, metadata: { userId: resultUser._id.toString(), paymentMethod: resultOrder.paymentMethod }, }); sendPaymentConfirmedEmail({ to: resultUser.email, name: resultUser.name, order: resultOrder, courseTitle: course ? course.title : resultOrder.courseTitleSnapshot, }).catch((err) => logger.error("Payment-confirmed email failed", { error: err.message })); return { order: resultOrder, alreadyConfirmed: false }; } finally { session.endSession(); } } module.exports = { confirmOrderAndGrantAccess };