Files
TurboTrades/models/Transaction.js
2026-01-10 04:57:43 +00:00

371 lines
8.4 KiB
JavaScript

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;