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;