Spaces:
Running
Running
Download src/controllers/voucher.controller.js from AndreaNotes1/auth-app-api: direct link, hf CLI and curl.
- Browser
- Download file 9.12 kB
-
https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/voucher.controller.js
- Command line
-
hf download hf://spaces/AndreaNotes1/auth-app-api/src/controllers/voucher.controller.js
-
curl -L -o voucher.controller.js https://huggingface.co/spaces/AndreaNotes1/auth-app-api/resolve/main/src/controllers/voucher.controller.js
9.12 kB
| const mongoose = require("mongoose"); | |
| const Voucher = require("../models/Voucher"); | |
| const Order = require("../models/Order"); | |
| const User = require("../models/User"); | |
| const Course = require("../models/Course"); | |
| const AuditLog = require("../models/AuditLog"); | |
| const { generateOrderNumber } = require("../utils/orderNumber"); | |
| const { generateVoucherCode } = require("../utils/voucherCode"); | |
| const { sendVoucherRedeemedEmail } = require("../utils/email"); | |
| const logger = require("../utils/logger"); | |
| function normalizeEmails(emails) { | |
| if (!Array.isArray(emails)) return []; | |
| return [...new Set(emails.map((e) => String(e).toLowerCase().trim()).filter(Boolean))]; | |
| } | |
| // --------------------------------------------------------------------- | |
| // Admin: create / list / inspect / deactivate vouchers | |
| // --------------------------------------------------------------------- | |
| /** | |
| * Creates a voucher for a specific course. If no code is supplied, one is | |
| * generated server-side (retried on the astronomically unlikely chance of | |
| * a collision) so admins can just click "Generate" without inventing a | |
| * code themselves. | |
| */ | |
| async function createVoucher(req, res, next) { | |
| try { | |
| const { courseId, code, maxRedemptions, restrictedToEmails, expiresAt, note } = req.body; | |
| if (!mongoose.isValidObjectId(courseId)) { | |
| return res.status(400).json({ error: "Invalid course selected." }); | |
| } | |
| const course = await Course.findById(courseId); | |
| if (!course) return res.status(404).json({ error: "Course not found." }); | |
| let finalCode = code ? String(code).toUpperCase().trim() : null; | |
| if (finalCode) { | |
| const existing = await Voucher.findOne({ code: finalCode }); | |
| if (existing) return res.status(409).json({ error: "That voucher code is already in use." }); | |
| } else { | |
| for (let attempt = 0; attempt < 5 && !finalCode; attempt++) { | |
| const candidate = generateVoucherCode(); | |
| // eslint-disable-next-line no-await-in-loop | |
| const existing = await Voucher.findOne({ code: candidate }); | |
| if (!existing) finalCode = candidate; | |
| } | |
| if (!finalCode) { | |
| return res.status(500).json({ error: "Could not generate a unique voucher code. Please try again." }); | |
| } | |
| } | |
| const voucher = await Voucher.create({ | |
| code: finalCode, | |
| course: course._id, | |
| createdBy: req.user._id, | |
| maxRedemptions: maxRedemptions || 1, | |
| restrictedToEmails: normalizeEmails(restrictedToEmails), | |
| expiresAt: expiresAt || null, | |
| note: note || "", | |
| }); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "admin.voucher_created", | |
| targetType: "Voucher", | |
| targetId: voucher._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { | |
| courseId: course._id.toString(), | |
| maxRedemptions: voucher.maxRedemptions, | |
| restrictedToEmails: voucher.restrictedToEmails, | |
| }, | |
| }); | |
| res.status(201).json({ voucher: voucher.toAdminJSON() }); | |
| } catch (err) { | |
| if (err && err.code === 11000) { | |
| return res.status(409).json({ error: "That voucher code is already in use." }); | |
| } | |
| next(err); | |
| } | |
| } | |
| async function listVouchers(req, res, next) { | |
| try { | |
| const { courseId, active } = req.query; | |
| const filter = {}; | |
| if (courseId && mongoose.isValidObjectId(courseId)) filter.course = courseId; | |
| if (active === "true") filter.isActive = true; | |
| if (active === "false") filter.isActive = false; | |
| const vouchers = await Voucher.find(filter) | |
| .sort({ createdAt: -1 }) | |
| .populate("course", "title") | |
| .populate("redemptions.user", "name email"); | |
| res.json({ | |
| vouchers: vouchers.map((v) => ({ | |
| ...v.toAdminJSON(), | |
| course: v.course ? { id: v.course._id, title: v.course.title } : null, | |
| redemptions: v.redemptions.map((r) => ({ | |
| user: r.user ? { id: r.user._id, name: r.user.name, email: r.user.email } : null, | |
| order: r.order, | |
| redeemedAt: r.redeemedAt, | |
| })), | |
| })), | |
| }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| async function getVoucher(req, res, next) { | |
| try { | |
| const voucher = await Voucher.findById(req.params.id) | |
| .populate("course", "title") | |
| .populate("redemptions.user", "name email"); | |
| if (!voucher) return res.status(404).json({ error: "Voucher not found." }); | |
| res.json({ | |
| voucher: { | |
| ...voucher.toAdminJSON(), | |
| course: voucher.course ? { id: voucher.course._id, title: voucher.course.title } : null, | |
| redemptions: voucher.redemptions.map((r) => ({ | |
| user: r.user ? { id: r.user._id, name: r.user.name, email: r.user.email } : null, | |
| order: r.order, | |
| redeemedAt: r.redeemedAt, | |
| })), | |
| }, | |
| }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| } | |
| function makeSetVoucherActiveHandler(isActive) { | |
| return async function handler(req, res, next) { | |
| try { | |
| const voucher = await Voucher.findById(req.params.id); | |
| if (!voucher) return res.status(404).json({ error: "Voucher not found." }); | |
| voucher.isActive = isActive; | |
| await voucher.save(); | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: isActive ? "admin.voucher_reactivated" : "admin.voucher_deactivated", | |
| targetType: "Voucher", | |
| targetId: voucher._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| }); | |
| res.json({ voucher: voucher.toAdminJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } | |
| }; | |
| } | |
| const deactivateVoucher = makeSetVoucherActiveHandler(false); | |
| const reactivateVoucher = makeSetVoucherActiveHandler(true); | |
| // --------------------------------------------------------------------- | |
| // User: redeem a voucher code for free course access | |
| // --------------------------------------------------------------------- | |
| /** | |
| * Redeeming a voucher mirrors utils/orderConfirmation.js's | |
| * confirmOrderAndGrantAccess: it produces a real (already-confirmed, | |
| * zero-amount) Order so the course shows up consistently in "my | |
| * orders"/admin reporting, and grants access the same way a paid order | |
| * would. Wrapped in a transaction so the order, the user's enrolment, | |
| * and the voucher's redemption count can never drift out of sync. | |
| */ | |
| async function redeemVoucher(req, res, next) { | |
| const { code } = req.body; | |
| const normalizedCode = String(code || "") | |
| .toUpperCase() | |
| .trim(); | |
| const session = await mongoose.startSession(); | |
| try { | |
| let resultOrder; | |
| let resultCourse; | |
| let blockReason; | |
| await session.withTransaction(async () => { | |
| const voucher = await Voucher.findOne({ code: normalizedCode }).session(session); | |
| if (!voucher) { | |
| blockReason = "That voucher code isn't valid."; | |
| return; | |
| } | |
| const course = await Course.findById(voucher.course).session(session); | |
| if (!course || !course.isPublished) { | |
| blockReason = "That voucher's course is no longer available."; | |
| return; | |
| } | |
| if (req.user.hasAccessToCourse(course._id)) { | |
| blockReason = "You already have access to this course."; | |
| return; | |
| } | |
| const reason = voucher.redemptionBlockReason(req.user); | |
| if (reason) { | |
| blockReason = reason; | |
| return; | |
| } | |
| const order = new Order({ | |
| orderNumber: generateOrderNumber(), | |
| user: req.user._id, | |
| course: course._id, | |
| courseTitleSnapshot: course.title, | |
| amount: 0, | |
| currency: course.currency || "USD", | |
| paymentMethod: "voucher", | |
| voucher: voucher._id, | |
| status: "confirmed", | |
| confirmedAt: new Date(), | |
| }); | |
| await order.save({ session }); | |
| const user = await User.findById(req.user._id).session(session); | |
| user.enrolledCourses.push({ course: course._id, order: order._id, accessGrantedAt: new Date() }); | |
| await user.save({ session }); | |
| voucher.redemptions.push({ user: user._id, order: order._id, redeemedAt: new Date() }); | |
| await voucher.save({ session }); | |
| resultOrder = order; | |
| resultCourse = course; | |
| }); | |
| if (blockReason) { | |
| return res.status(409).json({ error: blockReason }); | |
| } | |
| await AuditLog.create({ | |
| actor: req.user._id, | |
| actorEmail: req.user.email, | |
| action: "voucher.redeemed", | |
| targetType: "Order", | |
| targetId: resultOrder._id, | |
| ip: req.ip, | |
| userAgent: req.headers["user-agent"], | |
| metadata: { courseId: resultCourse._id.toString(), voucherCode: normalizedCode }, | |
| }); | |
| sendVoucherRedeemedEmail({ | |
| to: req.user.email, | |
| name: req.user.name, | |
| order: resultOrder, | |
| courseTitle: resultCourse.title, | |
| voucherCode: normalizedCode, | |
| }).catch((err) => logger.error("Voucher-redeemed email failed", { error: err.message })); | |
| res.status(201).json({ order: resultOrder.toPublicJSON() }); | |
| } catch (err) { | |
| next(err); | |
| } finally { | |
| session.endSession(); | |
| } | |
| } | |
| module.exports = { | |
| createVoucher, | |
| listVouchers, | |
| getVoucher, | |
| deactivateVoucher, | |
| reactivateVoucher, | |
| redeemVoucher, | |
| }; | |