import mongoose from "mongoose"; const transactionSchema = new mongoose.Schema( { // User information userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true, index: true, }, steamId: { type: String, required: true, index: true, }, // Transaction type type: { type: String, enum: [ "deposit", "withdrawal", "purchase", "sale", "trade", "bonus", "refund", ], required: true, index: true, }, // Transaction status status: { type: String, enum: ["pending", "completed", "failed", "cancelled", "processing"], required: true, default: "pending", index: true, }, // Amount amount: { type: Number, required: true, }, // Currency (for future multi-currency support) currency: { type: String, default: "USD", }, // Balance before and after (for audit trail) balanceBefore: { type: Number, required: true, }, balanceAfter: { type: Number, required: true, }, // Session tracking (for security) sessionId: { type: mongoose.Schema.Types.ObjectId, ref: "Session", required: false, // Optional for backwards compatibility index: true, }, sessionIdShort: { type: String, // Last 6 chars of session ID for display required: false, }, // Related entities itemId: { type: mongoose.Schema.Types.ObjectId, ref: "Item", required: false, // Only for purchase/sale transactions }, itemName: { type: String, required: false, }, itemImage: { type: String, required: false, }, // Payment method (for deposits/withdrawals) paymentMethod: { type: String, enum: ["stripe", "paypal", "crypto", "balance", "steam", "other", null], required: false, }, // External payment reference externalId: { type: String, required: false, index: true, }, // Description and notes description: { type: String, required: false, }, notes: { type: String, required: false, // Internal notes (not visible to user) }, // Fee information fee: { type: Number, default: 0, }, feePercentage: { type: Number, default: 0, }, // Timestamps completedAt: { type: Date, required: false, }, failedAt: { type: Date, required: false, }, cancelledAt: { type: Date, required: false, }, // Error information (if failed) errorMessage: { type: String, required: false, }, errorCode: { type: String, required: false, }, // Metadata (flexible field for additional data) metadata: { type: mongoose.Schema.Types.Mixed, required: false, }, }, { timestamps: true, // Adds createdAt and updatedAt collection: "transactions", } ); // Indexes for common queries transactionSchema.index({ userId: 1, createdAt: -1 }); transactionSchema.index({ steamId: 1, createdAt: -1 }); transactionSchema.index({ type: 1, status: 1 }); transactionSchema.index({ sessionId: 1, createdAt: -1 }); transactionSchema.index({ status: 1, createdAt: -1 }); // Virtual for formatted amount transactionSchema.virtual("formattedAmount").get(function () { return `$${this.amount.toFixed(2)}`; }); // Virtual for transaction direction (+ or -) transactionSchema.virtual("direction").get(function () { const positiveTypes = ["deposit", "sale", "bonus", "refund"]; return positiveTypes.includes(this.type) ? "+" : "-"; }); // Virtual for session color (deterministic based on sessionIdShort) transactionSchema.virtual("sessionColor").get(function () { if (!this.sessionIdShort) return "#64748b"; let hash = 0; for (let i = 0; i < this.sessionIdShort.length; i++) { hash = this.sessionIdShort.charCodeAt(i) + ((hash << 5) - hash); } const hue = Math.abs(hash) % 360; const saturation = 60 + (Math.abs(hash) % 20); const lightness = 45 + (Math.abs(hash) % 15); return `hsl(${hue}, ${saturation}%, ${lightness}%)`; }); // Instance methods /** * Mark transaction as completed */ transactionSchema.methods.complete = async function () { this.status = "completed"; this.completedAt = new Date(); return await this.save(); }; /** * Mark transaction as failed */ transactionSchema.methods.fail = async function (errorMessage, errorCode) { this.status = "failed"; this.failedAt = new Date(); this.errorMessage = errorMessage; this.errorCode = errorCode; return await this.save(); }; /** * Mark transaction as cancelled */ transactionSchema.methods.cancel = async function () { this.status = "cancelled"; this.cancelledAt = new Date(); return await this.save(); }; /** * Get session ID short (last 6 chars) */ transactionSchema.methods.getSessionIdShort = function () { if (!this.sessionId) return "SYSTEM"; return this.sessionId.toString().slice(-6).toUpperCase(); }; // Static methods /** * Create a new transaction with session tracking */ transactionSchema.statics.createTransaction = async function (data) { const transaction = new this({ userId: data.userId, steamId: data.steamId, type: data.type, status: data.status || "completed", amount: data.amount, currency: data.currency || "USD", balanceBefore: data.balanceBefore || 0, balanceAfter: data.balanceAfter || 0, sessionId: data.sessionId, sessionIdShort: data.sessionId ? data.sessionId.toString().slice(-6).toUpperCase() : "SYSTEM", itemId: data.itemId, itemName: data.itemName, paymentMethod: data.paymentMethod, description: data.description, notes: data.notes, fee: data.fee || 0, feePercentage: data.feePercentage || 0, metadata: data.metadata, }); return await transaction.save(); }; /** * Get user's transaction history */ transactionSchema.statics.getUserTransactions = async function ( userId, options = {} ) { const { limit = 50, skip = 0, type = null, status = null, startDate = null, endDate = null, } = options; const query = { userId }; if (type) query.type = type; if (status) query.status = status; if (startDate || endDate) { query.createdAt = {}; if (startDate) query.createdAt.$gte = new Date(startDate); if (endDate) query.createdAt.$lte = new Date(endDate); } return await this.find(query) .sort({ createdAt: -1 }) .limit(limit) .skip(skip) .populate("sessionId", "device browser os ip") .exec(); }; /** * Get transactions by session */ transactionSchema.statics.getSessionTransactions = async function (sessionId) { return await this.find({ sessionId }) .sort({ createdAt: -1 }) .populate("itemId", "name rarity game") .exec(); }; /** * Get user's transaction statistics */ transactionSchema.statics.getUserStats = async function (userId) { const stats = await this.aggregate([ { $match: { userId: new mongoose.Types.ObjectId(userId) } }, { $group: { _id: "$type", count: { $sum: 1 }, totalAmount: { $sum: "$amount" }, }, }, ]); const result = { totalDeposits: 0, totalWithdrawals: 0, totalPurchases: 0, totalSales: 0, depositCount: 0, withdrawalCount: 0, purchaseCount: 0, saleCount: 0, }; stats.forEach((stat) => { if (stat._id === "deposit") { result.totalDeposits = stat.totalAmount; result.depositCount = stat.count; } else if (stat._id === "withdrawal") { result.totalWithdrawals = stat.totalAmount; result.withdrawalCount = stat.count; } else if (stat._id === "purchase") { result.totalPurchases = stat.totalAmount; result.purchaseCount = stat.count; } else if (stat._id === "sale") { result.totalSales = stat.totalAmount; result.saleCount = stat.count; } }); return result; }; // Pre-save hook to set sessionIdShort transactionSchema.pre("save", function (next) { if (this.sessionId && !this.sessionIdShort) { this.sessionIdShort = this.sessionId.toString().slice(-6).toUpperCase(); } next(); }); // Ensure virtuals are included in JSON transactionSchema.set("toJSON", { virtuals: true }); transactionSchema.set("toObject", { virtuals: true }); const Transaction = mongoose.model("Transaction", transactionSchema); export default Transaction;