Spaces:
Running
Running
File size: 3,663 Bytes
4c6b1e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 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 };
|