This commit is contained in:
NotII
2025-03-10 17:39:37 +00:00
parent c08df8919d
commit 20d5559832
69 changed files with 7676 additions and 78 deletions

View File

@@ -0,0 +1,14 @@
import mongoose from "mongoose";
const BlockedUserSchema = new mongoose.Schema({
telegramUserId: { type: Number, required: true, unique: true },
reason: { type: String },
blockedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: "Staff",
required: true
},
blockedAt: { type: Date, default: Date.now }
});
export default mongoose.model("BlockedUser", BlockedUserSchema);

View File

@@ -0,0 +1,10 @@
import mongoose from 'mongoose';
const BuyerSchema = new mongoose.Schema({
telegramId: { type: Number, required: true, unique: true },
username: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
banned: { type: Boolean, default: false }
});
export default mongoose.model('Buyer', BuyerSchema);

View File

@@ -0,0 +1,68 @@
import mongoose from "mongoose";
const MessageSchema = new mongoose.Schema({
sender: {
type: String,
enum: ["buyer", "vendor"],
required: true
},
buyerId: {
type: String,
required: true
},
vendorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vendor",
required: true
},
content: {
type: String,
required: true
},
attachments: [{
type: String,
required: false
}],
read: {
type: Boolean,
default: false
},
createdAt: {
type: Date,
default: Date.now
}
});
const ChatSchema = new mongoose.Schema({
buyerId: {
type: String,
required: true
},
vendorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vendor",
required: true
},
storeId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Store",
required: true
},
messages: [MessageSchema],
orderId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Order",
required: false
},
lastUpdated: {
type: Date,
default: Date.now
}
});
// Create indexes for faster queries
ChatSchema.index({ buyerId: 1, vendorId: 1 });
ChatSchema.index({ vendorId: 1, lastUpdated: -1 });
ChatSchema.index({ buyerId: 1, lastUpdated: -1 });
export default mongoose.model("Chat", ChatSchema);

View File

@@ -0,0 +1,43 @@
import mongoose from "mongoose";
const EscrowSchema = new mongoose.Schema({
orderId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Order",
required: true,
},
buyerId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Buyer",
required: true,
},
vendorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vendor",
required: true,
},
amount: {
type: Number,
required: true,
min: 0.01,
},
currency: {
type: String,
enum: ["ltc", "btc", "xmr"],
required: true,
},
status: {
type: String,
enum: ["held", "released", "disputed"],
default: "held",
},
releaseDate: {
type: Date,
required: true,
default: function () {
return new Date(Date.now() + 8 * 24 * 60 * 60 * 1000); // Auto set to 8 days from now
},
},
});
export default mongoose.model("Escrow", EscrowSchema);

View File

@@ -0,0 +1,40 @@
import mongoose from "mongoose";
const InvitationSchema = new mongoose.Schema({
code: {
type: String,
required: true,
unique: true,
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Staffs',
required: true,
},
isUsed: {
type: Boolean,
default: false,
},
usedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Vendor',
default: null,
},
expiresAt: {
type: Date,
required: true,
},
createdAt: {
type: Date,
default: Date.now,
},
usedAt: {
type: Date,
default: null,
}
});
InvitationSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
InvitationSchema.index({ code: 1 });
export default mongoose.model("Invitation", InvitationSchema);

View File

@@ -0,0 +1,100 @@
import mongoose from "mongoose";
import AutoIncrement from "mongoose-sequence";
const connection = mongoose.connection;
const OrderSchema = new mongoose.Schema({
orderId: { type: Number, unique: true },
vendorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vendor",
required: true,
},
storeId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Store",
required: true,
},
pgpAddress: { type: String, required: true },
orderDate: { type: Date, default: Date.now, },
txid: { type: Array, default: [] },
products: [
{
productId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Product",
required: true,
},
quantity: { type: Number, required: true, min: 0.1 },
pricePerUnit: { type: Number, required: true, min: 0.01 },
totalItemPrice: { type: Number, required: true, min: 0.01 },
},
],
shippingMethod: { type: Object },
totalPrice: { type: Number, required: true, min: 0.01 },
// Promotion fields
promotion: {
type: mongoose.Schema.Types.ObjectId,
ref: "Promotion",
default: null
},
promotionCode: {
type: String,
default: null
},
discountAmount: {
type: Number,
default: 0,
min: 0
},
subtotalBeforeDiscount: {
type: Number,
default: 0,
min: 0
},
status: {
type: String,
enum: [
"unpaid",
"cancelled",
"confirming",
"paid",
"shipped",
"disputed",
"completed",
"acknowledged"
],
default: "unpaid",
},
paymentAddress: { type: String, required: true },
wallet: {
type: mongoose.Schema.Types.ObjectId,
ref: "Wallet",
},
cryptoTotal: { type: Number, required: true, default: 0 },
//txid: { type: String, default: null },
telegramChatId: { type: String, default: null },
telegramBuyerId: { type: String, default: null },
telegramUsername: { type: String, default: null },
trackingNumber: { type: String, default: null },
escrowExpiresAt: {
type: Date,
required: true,
default: () => new Date(Date.now() + 8 * 24 * 60 * 60 * 1000),
},
});
OrderSchema.plugin(AutoIncrement(connection), { inc_field: "orderId" });
export default mongoose.model("Order", OrderSchema);

View File

@@ -0,0 +1,69 @@
import mongoose from "mongoose";
const ProductSchema = new mongoose.Schema({
storeId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Store",
required: true,
},
category: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "Store.categories"
},
name: { type: String, required: true },
description: { type: String },
unitType: {
type: String,
enum: ["pcs", "gr", "kg"],
required: true,
},
// Add inventory tracking fields
stockTracking: { type: Boolean, default: true },
currentStock: {
type: Number,
default: 0,
validate: {
validator: function(value) {
return !this.stockTracking || value >= 0;
},
message: "Stock cannot be negative"
}
},
lowStockThreshold: { type: Number, default: 10 },
stockStatus: {
type: String,
enum: ["in_stock", "low_stock", "out_of_stock"],
default: "out_of_stock"
},
pricing: [
{
minQuantity: {
type: Number,
required: true,
validate: {
validator: function(value) {
if (this.parent().unitType === "gr") {
return value >= 0.1;
}
return value >= 1;
},
message: "Invalid minQuantity for unitType"
}
},
pricePerUnit: {
type: Number,
required: true,
min: 0.01
},
},
],
image: { type: String },
});
export default mongoose.model("Product", ProductSchema);

View File

@@ -0,0 +1,126 @@
import mongoose from "mongoose";
const PromotionSchema = new mongoose.Schema(
{
storeId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Store",
required: [true, "Store ID is required"]
},
code: {
type: String,
required: [true, "Promotion code is required"],
trim: true,
uppercase: true,
minlength: [3, "Promotion code must be at least 3 characters"],
maxlength: [20, "Promotion code cannot exceed 20 characters"]
},
discountType: {
type: String,
required: [true, "Discount type is required"],
enum: {
values: ["percentage", "fixed"],
message: "Discount type must be either percentage or fixed"
}
},
discountValue: {
type: Number,
required: [true, "Discount value is required"],
validate: {
validator: function(value) {
if (this.discountType === "percentage") {
return value > 0 && value <= 100;
}
return value > 0;
},
message: props =>
props.value <= 0
? "Discount value must be greater than 0"
: "Percentage discount cannot exceed 100%"
}
},
minOrderAmount: {
type: Number,
default: 0,
min: [0, "Minimum order amount cannot be negative"]
},
maxUsage: {
type: Number,
default: null
},
usageCount: {
type: Number,
default: 0
},
isActive: {
type: Boolean,
default: true
},
startDate: {
type: Date,
default: Date.now
},
endDate: {
type: Date,
default: null
},
description: {
type: String,
trim: true,
maxlength: [200, "Description cannot exceed 200 characters"]
}
},
{
timestamps: true
}
);
// Compound index to ensure unique promo codes per store
PromotionSchema.index({ storeId: 1, code: 1 }, { unique: true });
// Check if a promotion is valid and can be applied
PromotionSchema.methods.isValid = function() {
const now = new Date();
// Check if promotion is active
if (!this.isActive) return false;
// Check if promotion has expired
if (this.endDate && now > this.endDate) return false;
// Check if promotion has reached max usage (if set)
if (this.maxUsage !== null && this.usageCount >= this.maxUsage) return false;
return true;
};
// Calculate discount amount for a given order total
PromotionSchema.methods.calculateDiscount = function(orderTotal) {
if (!this.isValid()) {
return 0;
}
if (orderTotal < this.minOrderAmount) {
return 0;
}
let discountAmount = 0;
if (this.discountType === "percentage") {
discountAmount = (orderTotal * this.discountValue) / 100;
} else {
// Fixed amount discount
discountAmount = this.discountValue;
// Ensure discount doesn't exceed order total
if (discountAmount > orderTotal) {
discountAmount = orderTotal;
}
}
return parseFloat(discountAmount.toFixed(2));
};
const Promotion = mongoose.model("Promotion", PromotionSchema);
export default Promotion;

View File

@@ -0,0 +1,54 @@
import mongoose from 'mongoose';
const PromotionUseSchema = new mongoose.Schema(
{
promotionId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Promotion',
required: [true, 'Promotion ID is required']
},
orderId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Order',
required: [true, 'Order ID is required']
},
userId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: [true, 'User ID is required']
},
storeId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Store',
required: [true, 'Store ID is required']
},
code: {
type: String,
required: [true, 'Promotion code is required']
},
discountType: {
type: String,
enum: ['percentage', 'fixed'],
required: [true, 'Discount type is required']
},
discountValue: {
type: Number,
required: [true, 'Discount value is required']
},
discountAmount: {
type: Number,
required: [true, 'Discount amount is required']
},
orderTotal: {
type: Number,
required: [true, 'Order total is required']
}
},
{
timestamps: true
}
);
const PromotionUse = mongoose.model('PromotionUse', PromotionUseSchema);
export default PromotionUse;

View File

@@ -0,0 +1,11 @@
import mongoose from "mongoose";
const StaffSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
passwordHash: { type: String, required: true },
role: { type: String, enum: ["admin", "moderator"], default: "moderator" },
currentToken: { type: String, default: null },
createdAt: { type: Date, default: Date.now },
});
export default mongoose.model("Staff", StaffSchema);

View File

@@ -0,0 +1,132 @@
import mongoose from "mongoose";
import { type } from "os";
const CategorySchema = new mongoose.Schema({
_id: {
type: mongoose.Schema.Types.ObjectId,
auto: true
},
name: {
type: String,
required: true
},
parentId: {
type: mongoose.Schema.Types.ObjectId,
default: null
}
}, { _id: true });
const StoreSchema = new mongoose.Schema({
vendorId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Vendor",
required: true
},
storeName: {
type: String,
required: true
},
welcomeMessage: {
type: String,
default: "Welcome to my store!"
},
telegramToken: {
type: String,
default: ""
},
pgpKey: {
type: String,
default: ""
},
createdAt: {
type: Date,
default: Date.now
},
shipsFrom: {
type: String,
default: "UK",
},
shipsTo: {
type: String,
default: "UK",
},
categories: [CategorySchema],
shippingOptions: [
{
name: {
type: String,
required: true
},
price: {
type: Number,
required: true,
min: 0
}
}
],
wallets:{
type: Object,
default: {
"litecoin": "",
"bitcoin": "",
"monero":""
}
},
feeRate:{
type: Number,
default: 2
}
});
// Add a method to get category hierarchy
StoreSchema.methods.getCategoryHierarchy = function() {
const categories = this.categories.toObject();
// Helper function to build tree structure
const buildTree = (parentId = null) => {
return categories
.filter(cat =>
(!parentId && !cat.parentId) ||
(cat.parentId?.toString() === parentId?.toString())
)
.map(cat => ({
...cat,
children: buildTree(cat._id)
}));
};
return buildTree();
};
// Add validation to prevent circular references
CategorySchema.pre('save', function(next) {
if (!this.parentId) {
return next();
}
const checkCircular = (categoryId, parentId) => {
if (!parentId) return false;
if (categoryId.toString() === parentId.toString()) return true;
const parent = this.parent().categories.id(parentId);
if (!parent) return false;
return checkCircular(categoryId, parent.parentId);
};
if (checkCircular(this._id, this.parentId)) {
next(new Error('Circular reference detected in category hierarchy'));
} else {
next();
}
});
// Add index for better query performance
StoreSchema.index({ 'categories.name': 1, 'categories.parentId': 1 });
const Store = mongoose.model("Store", StoreSchema);
export default Store;

View File

@@ -0,0 +1,22 @@
import mongoose from "mongoose";
const { Schema, model, Types } = mongoose;
/**
* Defines the schema for Telegram users.
* - `telegramUserId`: Unique Telegram user ID.
* - `stores`: Array of objects storing store references and chat IDs.
* - `createdAt`: Timestamp for when the user was added.
*/
const TelegramUserSchema = new Schema({
telegramUserId: { type: Number, required: true, unique: true },
stores: [
{
store: { type: Types.ObjectId, ref: "Store", required: true },
chatId: { type: Number, required: true }
}
],
createdAt: { type: Date, default: Date.now }
});
export default model("TelegramUser", TelegramUserSchema);

View File

@@ -0,0 +1,13 @@
import mongoose from "mongoose";
const VendorSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
passwordHash: { type: String, required: true },
currentToken: { type: String, default: null },
storeId: { type: mongoose.Schema.Types.ObjectId, ref: "Store", default: null },
pgpKey: { type: String, default: ""},
lastOnline: { type: Date, default: Date.now },
createdAt: { type: Date, default: Date.now },
});
export default mongoose.model("Vendor", VendorSchema);

View File

@@ -0,0 +1,56 @@
import mongoose from "mongoose";
import crypto from "crypto";
// Set default values if environment variables are not available
const encryptionKeyHex = process.env.ENCRYPTION_KEY || '48c66ee5a54e596e2029ea832a512401099533ece34cb0fbbb8c4023ca68ba8e';
const encryptionIvHex = process.env.ENCRYPTION_IV || '539e26d426cd4bac9844a8e446d63ab1';
const algorithm = "aes-256-cbc";
const encryptionKey = Buffer.from(encryptionKeyHex, "hex");
const iv = Buffer.from(encryptionIvHex, "hex");
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
return encrypted;
}
function decrypt(text) {
const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv);
let decrypted = decipher.update(text, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
const WalletSchema = new mongoose.Schema({
walletName: {
type: String,
},
orderId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Order",
required: true,
unique: true,
},
address: {
type: String,
required: true,
},
encryptedPrivateKey: {
type: String,
required: true,
},
});
WalletSchema.pre("save", function (next) {
if (!this.isModified("encryptedPrivateKey")) return next();
this.encryptedPrivateKey = encrypt(this.encryptedPrivateKey);
next();
});
WalletSchema.methods.getDecryptedPrivateKey = function () {
return decrypt(this.encryptedPrivateKey);
};
export default mongoose.model("Wallet", WalletSchema);