Spaces:
Running
Running
Download src/controllers/order.controller.js from AndreaNotes1/auth-app-api: direct link, hf CLI and curl.
- Browser
- Download file 8.42 kB
-
https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/order.controller.js
- Command line
-
hf download hf://spaces/AndreaNotes1/auth-app-api/src/controllers/order.controller.js
-
curl -L -o order.controller.js https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/order.controller.js
8.42 kB
| const mongoose = require("mongoose"); | |
| const Order = require("../models/Order"); | |
| const Course = require("../models/Course"); | |
| const AuditLog = require("../models/AuditLog"); | |
| const { generateOrderNumber } = require("../utils/orderNumber"); | |
| const { env } = require("../config/env"); | |
| const { sendOrderPlacedEmail, sendAdminNewOrderAlert } = require("../utils/email"); | |
| const payoneerCheckout = require("../utils/payoneerCheckout"); | |
| const logger = require("../utils/logger"); | |
| /** | |
| * Public checkout configuration — payment instructions shown on the | |
| * frontend before the customer submits their reference number. No | |
| * secrets here, just receiving-account details that are meant to be | |
| * shared with paying customers. | |
| */ | |
| function getPaymentOptions(req, res) { | |
| res.json({ | |
| bankWire: { | |
| currency: "USD", | |
| bankName: env.bank.name, | |
| accountName: env.bank.accountName, | |
| accountNumber: env.bank.accountNumber, | |
| routingNumber: env.bank.routingNumber, | |
| swiftBic: env.bank.swiftBic, | |
| iban: env.bank.iban || undefined, | |
| address: env.bank.address, | |
| }, | |
| payoneer: { | |
| accountEmail: env.payoneer.accountEmail, | |
| paymentUrl: env.payoneer.paymentUrl, | |
| }, | |
| }); | |
| } | |
| async function createOrder(req, res, next) { | |
| try { | |
| const { courseId, paymentMethod } = req.body; | |
| if (!mongoose.isValidObjectId(courseId)) { | |
| return res.status(400).json({ error: "Invalid course selected." }); | |
| } | |
| const course = await Course.findById(courseId); | |
| if (!course || !course.isPublished) { | |
| return res.status(404).json({ error: "Course not found or unavailable." }); | |
| } | |
| if (req.user.hasAccessToCourse(course._id)) { | |
| return res.status(409).json({ error: "You already have access to this course." }); | |
| } | |
| // Prevent duplicate open orders for the same course by the same user. | |
| const existingOpenOrder = await Order.findOne({ | |
| user: req.user._id, | |
| course: course._id, | |
| status: { $in: ["pending_payment", "awaiting_confirmation"] }, | |
| }); | |
| if (existingOpenOrder) { | |
| return res.status(200).json({ order: existingOpenOrder.toPublicJSON(), reused: true }); | |
| } | |
| const order = new Order({ | |
| orderNumber: generateOrderNumber(), | |
| user: req.user._id, | |
| course: course._id, | |
| courseTitleSnapshot: course.title, | |
| amount: course.price, | |
| currency: course.currency || "USD", | |
| paymentMethod, | |
| status: "awaiting_confirmation", | |
| }); | |
| await order.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "order.created", | |
| targetType: "Order", | |
| targetId: order._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { courseId: course._id.toString(), paymentMethod }, | |
| }); | |
| // Fire-and-forget emails; failures are logged but never block checkout. | |
| sendOrderPlacedEmail({ | |
| to: req.user.email, | |
| name: req.user.name, | |
| order, | |
| courseTitle: course.title, | |
| bankDetails: env.bank, | |
| payoneerDetails: env.payoneer, | |
| }) | |
| .then(() => { | |
| order.confirmationEmailSentAt = new Date(); | |
| return order.save(); | |
| }) | |
| .catch((err) => logger.error("Order confirmation email failed", { error: err.message })); | |
| sendAdminNewOrderAlert({ order, courseTitle: course.title, customerEmail: req.user.email }).catch((err) => | |
| logger.error("Admin new-order alert failed", { error: err.message }) | |
| ); | |
| res.status(201).json({ order: order.toPublicJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| /** | |
| * Creates an Order plus a hosted Payoneer Checkout session for it, and | |
| * returns the redirect URL along with a clear amount/tax/total | |
| * breakdown — the frontend shows that breakdown to the buyer before | |
| * sending them to Payoneer, per the transparency requirement. | |
| * | |
| * Unlike createOrder (used for the manual bank_wire/payoneer-reference | |
| * flow), this never puts the order in "awaiting_confirmation" for a | |
| * human to review — it stays "pending_payment" until Payoneer's | |
| * webhook confirms the payment (see webhook.controller.js), at which | |
| * point it's auto-confirmed and access is granted automatically. | |
| */ | |
| async function createPayoneerCheckoutSession(req, res, next) { | |
| try { | |
| const { courseId } = req.body; | |
| if (!mongoose.isValidObjectId(courseId)) { | |
| return res.status(400).json({ error: "Invalid course selected." }); | |
| } | |
| const course = await Course.findById(courseId); | |
| if (!course || !course.isPublished) { | |
| return res.status(404).json({ error: "Course not found or unavailable." }); | |
| } | |
| if (req.user.hasAccessToCourse(course._id)) { | |
| return res.status(409).json({ error: "You already have access to this course." }); | |
| } | |
| const existingOpenOrder = await Order.findOne({ | |
| user: req.user._id, | |
| course: course._id, | |
| paymentMethod: "payoneer_checkout", | |
| status: "pending_payment", | |
| }); | |
| if (existingOpenOrder && existingOpenOrder.providerCheckoutUrl) { | |
| return res.status(200).json({ order: existingOpenOrder.toPublicJSON(), reused: true }); | |
| } | |
| const order = new Order({ | |
| orderNumber: generateOrderNumber(), | |
| user: req.user._id, | |
| course: course._id, | |
| courseTitleSnapshot: course.title, | |
| amount: course.price, | |
| currency: course.currency || "USD", | |
| paymentMethod: "payoneer_checkout", | |
| status: "pending_payment", | |
| }); | |
| const session = await payoneerCheckout.createCheckoutSession({ | |
| order, | |
| course, | |
| customerEmail: req.user.email, | |
| customerName: req.user.name, | |
| successUrl: `${env.frontendUrl}/academy/checkout/${order._id}?result=success`, | |
| cancelUrl: `${env.frontendUrl}/academy/checkout/${order._id}?result=cancelled`, | |
| }); | |
| order.providerSessionId = session.sessionId; | |
| order.providerCheckoutUrl = session.checkoutUrl; | |
| if (session.tax) { | |
| order.taxAmount = session.tax.amount; | |
| order.taxCurrency = session.tax.currency; | |
| } | |
| await order.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "order.checkout_session_created", | |
| targetType: "Order", | |
| targetId: order._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { courseId: course._id.toString() }, | |
| }); | |
| res.status(201).json({ order: order.toPublicJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| /** Customer submits their bank wire reference / Payoneer transaction ID after paying. */ | |
| async function submitPaymentProof(req, res, next) { try { | |
| const { reference, note } = req.body; | |
| const order = await Order.findOne({ _id: req.params.id, user: req.user._id }); | |
| if (!order) return res.status(404).json({ error: "Order not found." }); | |
| if (!["pending_payment", "awaiting_confirmation"].includes(order.status)) { | |
| return res.status(409).json({ error: "This order can no longer be updated." }); | |
| } | |
| order.setPaymentReference(reference); | |
| if (note) order.setPayerNote(note); | |
| order.status = "awaiting_confirmation"; | |
| await order.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "order.payment_proof_submitted", | |
| targetType: "Order", | |
| targetId: order._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| }); | |
| res.json({ order: order.toPublicJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function listMyOrders(req, res, next) { | |
| try { | |
| const orders = await Order.find({ user: req.user._id }).sort({ createdAt: -1 }); | |
| res.json({ orders: orders.map((o) => o.toPublicJSON()) }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function getMyOrder(req, res, next) { | |
| try { | |
| const order = await Order.findOne({ _id: req.params.id, user: req.user._id }); | |
| if (!order) return res.status(404).json({ error: "Order not found." }); | |
| res.json({ order: order.toPublicJSON({ includeSensitive: true }) }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| module.exports = { | |
| getPaymentOptions, | |
| createOrder, | |
| createPayoneerCheckoutSession, | |
| submitPaymentProof, | |
| listMyOrders, | |
| getMyOrder, | |
| }; | |