const mongoose = require("mongoose"); const { encryptField, decryptField, hmacIndex } = require("../utils/crypto"); const PAYMENT_METHODS = ["bank_wire", "payoneer", "payoneer_checkout", "voucher"]; const ORDER_STATUSES = ["pending_payment", "awaiting_confirmation", "confirmed", "rejected", "cancelled"]; const orderSchema = new mongoose.Schema( { orderNumber: { type: String, required: true, unique: true, index: true }, user: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true }, course: { type: mongoose.Schema.Types.ObjectId, ref: "Course", required: true }, // Snapshot of course price/title at time of purchase so historical // orders remain accurate even if the course is later edited. courseTitleSnapshot: { type: String, required: true }, amount: { type: Number, required: true, min: 0 }, currency: { type: String, default: "USD" }, paymentMethod: { type: String, enum: PAYMENT_METHODS, required: true }, // Set only when paymentMethod === "voucher": the voucher whose // redemption produced this (free, pre-confirmed) order. voucher: { type: mongoose.Schema.Types.ObjectId, ref: "Voucher", default: null, index: true }, // --- Automated Payoneer Checkout fields (paymentMethod === // "payoneer_checkout" only). Populated when the checkout session is // created and used to (a) match an incoming webhook back to this // order, and (b) show the buyer a clear tax breakdown before they're // sent to Payoneer to actually pay. --- providerSessionId: { type: String, default: null, index: true }, providerCheckoutUrl: { type: String, default: null }, taxAmount: { type: Number, default: 0, min: 0 }, taxCurrency: { type: String, default: null }, // Sensitive: encrypted at rest with AES-256-GCM. Never returned by // default queries (select: false) and never logged in plaintext. // paymentReferenceIndex is a keyed HMAC used only to detect // duplicate submissions without ever decrypting. paymentReferenceEncrypted: { type: String, default: null, select: false }, paymentReferenceIndex: { type: String, default: null, index: true }, payerNoteEncrypted: { type: String, default: null, select: false }, status: { type: String, enum: ORDER_STATUSES, default: "pending_payment", index: true }, confirmedBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", default: null }, confirmedAt: { type: Date, default: null }, rejectionReason: { type: String, default: null }, confirmationEmailSentAt: { type: Date, default: null }, }, { timestamps: true } ); // --- Helpers to write/read the encrypted payment reference safely --- orderSchema.methods.setPaymentReference = function setPaymentReference(plainReference) { this.paymentReferenceEncrypted = encryptField(plainReference); this.paymentReferenceIndex = hmacIndex(plainReference); }; orderSchema.methods.getPaymentReference = function getPaymentReference() { return decryptField(this.paymentReferenceEncrypted); }; orderSchema.methods.setPayerNote = function setPayerNote(note) { this.payerNoteEncrypted = encryptField(note); }; orderSchema.methods.getPayerNote = function getPayerNote() { return decryptField(this.payerNoteEncrypted); }; orderSchema.methods.toPublicJSON = function toPublicJSON({ includeSensitive = false } = {}) { const json = { id: this._id, orderNumber: this.orderNumber, course: this.course, courseTitleSnapshot: this.courseTitleSnapshot, amount: this.amount, currency: this.currency, taxAmount: this.taxAmount, taxCurrency: this.taxCurrency, totalAmount: this.amount + (this.taxAmount || 0), paymentMethod: this.paymentMethod, voucher: this.voucher || undefined, providerCheckoutUrl: this.providerCheckoutUrl, status: this.status, confirmedAt: this.confirmedAt, rejectionReason: this.rejectionReason, createdAt: this.createdAt, }; if (includeSensitive) { json.paymentReference = this.getPaymentReference(); json.payerNote = this.getPayerNote(); } return json; }; module.exports = mongoose.model("Order", orderSchema); module.exports.PAYMENT_METHODS = PAYMENT_METHODS; module.exports.ORDER_STATUSES = ORDER_STATUSES;