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 };