Spaces:
Running
Running
File size: 4,368 Bytes
6e13570 4c6b1e8 6e13570 4c6b1e8 6e13570 4c6b1e8 6e13570 4c6b1e8 6e13570 | 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 99 100 101 | 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;
|