full
This commit is contained in:
@@ -104,6 +104,10 @@ const AcademyCategorySchema = new mongoose.Schema({
|
||||
ref: 'User',
|
||||
required: false,
|
||||
},
|
||||
relatedExpertise: {
|
||||
type: [String],
|
||||
default: [],
|
||||
},
|
||||
}, {
|
||||
toJSON: { virtuals: true },
|
||||
toObject: { virtuals: true }
|
||||
|
||||
@@ -37,8 +37,8 @@ const modelSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
enum: ['pending', 'accept', 'reject'],
|
||||
default: 'accept',
|
||||
enum: ['pending', 'temp_accept', 'accept', 'reject'],
|
||||
default: 'temp_accept',
|
||||
},
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
|
||||
32
models/AdminBroadcastModel.js
Normal file
32
models/AdminBroadcastModel.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
|
||||
// یک ردیف بهازای هر اعلان همگانی که از پنل ارسال میشه — جدا از میلیونها ردیف
|
||||
// NotificationModel که این ارسال بهشون تبدیل میشه، تا لیست پنل همیشه کوچیک و سریع بمونه
|
||||
const adminBroadcastSchema = new mongoose.Schema({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
recipientCount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
sentBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Admin',
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
adminBroadcastSchema.plugin(timestamp)
|
||||
adminBroadcastSchema.plugin(mongoosePaginate)
|
||||
|
||||
module.exports = mongoose.model('AdminBroadcast', adminBroadcastSchema)
|
||||
@@ -14,7 +14,10 @@ const advertisingCategorySchema = new mongoose.Schema({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
relatedExpertise: [{
|
||||
type: String
|
||||
}]
|
||||
})
|
||||
|
||||
advertisingCategorySchema.plugin(timestamp)
|
||||
|
||||
@@ -20,7 +20,7 @@ const advertisingCommentSchema = new mongoose.Schema({
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected'],
|
||||
enum: ['pending', 'temp_accept', 'accepted', 'rejected'],
|
||||
default: 'pending'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -26,6 +26,10 @@ const advertisingSchema = new mongoose.Schema({
|
||||
maxLength: 1024,
|
||||
default: null
|
||||
},
|
||||
country: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
province: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -153,7 +157,7 @@ const advertisingSchema = new mongoose.Schema({
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'expired'],
|
||||
enum: ['pre_payment', 'paid', 'temp_accept', 'accepted', 'rejected', 'expired'],
|
||||
default: 'pre_payment'
|
||||
},
|
||||
payment_status: {
|
||||
|
||||
77
models/BookingConfigModel.js
Normal file
77
models/BookingConfigModel.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const bookingServiceSchema = new mongoose.Schema({
|
||||
serviceId: { type: String, required: true },
|
||||
title: { type: String, required: true, trim: true },
|
||||
price: { type: Number, required: true },
|
||||
image: { type: String, default: null }
|
||||
}, { _id: false })
|
||||
|
||||
const bookingTimeRangeSchema = new mongoose.Schema({
|
||||
start_time: { type: String, required: true },
|
||||
end_time: { type: String, required: true }
|
||||
}, { _id: false })
|
||||
|
||||
const bookingWeeklyScheduleSchema = new mongoose.Schema({
|
||||
day: {
|
||||
type: String,
|
||||
enum: ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'],
|
||||
required: true
|
||||
},
|
||||
ranges: { type: [bookingTimeRangeSchema], default: [] }
|
||||
}, { _id: false })
|
||||
|
||||
const bookingConfigSchema = new mongoose.Schema({
|
||||
owner: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
services: {
|
||||
type: [bookingServiceSchema],
|
||||
default: []
|
||||
},
|
||||
weeklySchedule: {
|
||||
type: [bookingWeeklyScheduleSchema],
|
||||
default: []
|
||||
},
|
||||
holidayDates: {
|
||||
type: [Date],
|
||||
default: []
|
||||
},
|
||||
depositRequired: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// تعداد هفتههایی که برنامه هفتگی تکرار میشه (خالی = نامحدود)؛ بعد از پایانش کانفیگ بهصورت خودکار inactive میشه و مالکش نوتیف میگیره
|
||||
weeklyRepeatCount: {
|
||||
type: Number,
|
||||
default: null,
|
||||
min: 1
|
||||
},
|
||||
// زمانی که شمارش تکرار هفتگی از اون شروع شده (هر بار فعالسازی/تمدید ریست میشه)
|
||||
repeatStartedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'active', 'inactive'],
|
||||
default: 'draft',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
bookingConfigSchema.plugin(timestamp)
|
||||
bookingConfigSchema.plugin(mongoosePaginate)
|
||||
|
||||
const BookingConfigModel = mongoose.model('BookingConfig', bookingConfigSchema)
|
||||
module.exports = BookingConfigModel
|
||||
78
models/BookingModel.js
Normal file
78
models/BookingModel.js
Normal file
@@ -0,0 +1,78 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const bookingSchema = new mongoose.Schema({
|
||||
bookingConfig: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'BookingConfig',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
provider: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
buyer: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
serviceId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
serviceTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
servicePrice: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
date: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
startTime: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
endTime: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
depositRequired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
depositAmount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
depositPaid: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
paymentAuthority: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending_payment', 'confirmed', 'cancelled'],
|
||||
default: 'pending_payment',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
bookingSchema.plugin(timestamp)
|
||||
bookingSchema.plugin(mongoosePaginate)
|
||||
|
||||
const BookingModel = mongoose.model('Booking', bookingSchema)
|
||||
module.exports = BookingModel
|
||||
39
models/ChatRoomMessageModel.js
Normal file
39
models/ChatRoomMessageModel.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const chatRoomMessageSchema = new mongoose.Schema({
|
||||
roomId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ChatRoom',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
senderId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
content: { type: String, trim: true },
|
||||
file: { type: String },
|
||||
fileType: {
|
||||
type: String,
|
||||
enum: ['image', 'video', 'file', 'voice', 'location']
|
||||
},
|
||||
replyToId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ChatRoomMessage'
|
||||
},
|
||||
createdAt: { type: Date, default: Date.now, index: true },
|
||||
reactions: [
|
||||
{
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
emoji: { type: String, required: true, trim: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
module.exports = mongoose.model('ChatRoomMessage', chatRoomMessageSchema)
|
||||
40
models/ChatRoomModel.js
Normal file
40
models/ChatRoomModel.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const chatRoomSchema = new mongoose.Schema(
|
||||
{
|
||||
title: { type: String, required: true, trim: true },
|
||||
slug: { type: String, required: true, unique: true, trim: true },
|
||||
description: { type: String, trim: true },
|
||||
image: { type: String },
|
||||
members: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
|
||||
createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
|
||||
province: {
|
||||
id: { type: Number },
|
||||
name: { type: String }
|
||||
},
|
||||
city: {
|
||||
id: { type: Number },
|
||||
name: { type: String }
|
||||
},
|
||||
expertise: { type: String, trim: true },
|
||||
visibility: {
|
||||
type: String,
|
||||
enum: ['public', 'private'],
|
||||
default: 'public',
|
||||
index: true,
|
||||
},
|
||||
lastActivityAt: { type: Date, default: Date.now, index: true },
|
||||
idleTimeoutMs: { type: Number, default: 30 * 24 * 60 * 60 * 1000 },
|
||||
isSystem: { type: Boolean, default: false },
|
||||
e2ee_enabled: { type: Boolean, default: false },
|
||||
e2ee_wrapped_keys: [
|
||||
{
|
||||
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
|
||||
wrappedKey: { type: String, required: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{ timestamps: true }
|
||||
)
|
||||
|
||||
module.exports = mongoose.model('ChatRoom', chatRoomSchema)
|
||||
22
models/CommentLikeModel.js
Normal file
22
models/CommentLikeModel.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const commentLikeSchema = new mongoose.Schema({
|
||||
commentId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Comment',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
commentLikeSchema.index({ commentId: 1, userId: 1 }, { unique: true })
|
||||
commentLikeSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('CommentLike', commentLikeSchema)
|
||||
@@ -37,18 +37,34 @@ const commentSchema = new mongoose.Schema({
|
||||
ref: 'Post',
|
||||
required: false
|
||||
},
|
||||
shop_product: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopProductListing',
|
||||
required: false
|
||||
},
|
||||
parent: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Comment',
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
comment_for: {
|
||||
type: String,
|
||||
enum: ['user', 'project', 'post'],
|
||||
enum: ['user', 'project', 'post', 'shop_product'],
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected'],
|
||||
enum: ['pending', 'temp_accept', 'accepted', 'rejected'],
|
||||
default: 'pending'
|
||||
}
|
||||
})
|
||||
|
||||
// postController.js هر بار جزئیاتِ پست باز میشه CommentModel.countDocuments({post,status})
|
||||
// میزنه؛ getUserController.js هم لیستِ کامنتهای یه کاربر رو با user فیلتر میکنه
|
||||
commentSchema.index({ post: 1, status: 1 })
|
||||
commentSchema.index({ user: 1 })
|
||||
|
||||
commentSchema.plugin(timestamp)
|
||||
commentSchema.plugin(mongoosePaginate)
|
||||
const CommentModel = mongoose.model('Comment', commentSchema)
|
||||
|
||||
@@ -28,8 +28,8 @@ const modelSchema = new mongoose.Schema({
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected'],
|
||||
default: 'accepted'
|
||||
enum: ['pending', 'temp_accept', 'accepted', 'rejected'],
|
||||
default: 'pending'
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -18,7 +18,8 @@ const modelSchema = new mongoose.Schema({
|
||||
type: String
|
||||
},
|
||||
academyId:{
|
||||
type: String
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Academy'
|
||||
},
|
||||
caption: {
|
||||
type: String,
|
||||
@@ -32,8 +33,8 @@ const modelSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
enum: ['pending', 'accept', 'reject'],
|
||||
default: 'accept',
|
||||
enum: ['pending', 'temp_accept', 'accept', 'reject'],
|
||||
default: 'temp_accept',
|
||||
},
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
@@ -71,6 +72,16 @@ const modelSchema = new mongoose.Schema({
|
||||
teacher_name: {
|
||||
type: String
|
||||
},
|
||||
is_free: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
contactInfo: {
|
||||
mobile: { type: String, default: null },
|
||||
telegramLink: { type: String, default: null },
|
||||
whatsappNumber: { type: String, default: null },
|
||||
instagramLink: { type: String, default: null },
|
||||
},
|
||||
});
|
||||
|
||||
modelSchema.plugin(timestamp);
|
||||
|
||||
24
models/DeletedAccountTombstoneModel.js
Normal file
24
models/DeletedAccountTombstoneModel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const tombstoneSchema = new mongoose.Schema({
|
||||
mobile_hash: {
|
||||
type: String,
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
user_name_hash: {
|
||||
type: String,
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
expires_at: {
|
||||
type: Date,
|
||||
required: true,
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
tombstoneSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('DeletedAccountTombstone', tombstoneSchema)
|
||||
40
models/DeviceLinkSessionModel.js
Normal file
40
models/DeviceLinkSessionModel.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
/**
|
||||
* Short-lived handshake for linking a new browser/device to an account's existing
|
||||
* E2EE identity key (QR-based, WhatsApp/Signal style) — see
|
||||
* controllers/application/account/e2eeController.js. The server only ever relays
|
||||
* `wrappedIdentityKey`: it's the private key encrypted client-side against the new
|
||||
* device's ephemeral public key, so the server never sees plaintext key material.
|
||||
*/
|
||||
const deviceLinkSessionSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
ephemeralPublicKeyB64: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
wrappedIdentityKey: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'fulfilled'],
|
||||
default: 'pending'
|
||||
},
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
deviceLinkSessionSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 })
|
||||
deviceLinkSessionSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('DeviceLinkSession', deviceLinkSessionSchema)
|
||||
@@ -10,7 +10,7 @@ const exploreInteractionSchema = new mongoose.Schema({
|
||||
},
|
||||
targetType: {
|
||||
type: String,
|
||||
enum: ['post', 'profile', 'academy'],
|
||||
enum: ['post', 'profile', 'academy', 'shop_product', 'academy_course', 'billboard'],
|
||||
required: true
|
||||
},
|
||||
targetId: {
|
||||
@@ -24,7 +24,7 @@ const exploreInteractionSchema = new mongoose.Schema({
|
||||
},
|
||||
contentType: {
|
||||
type: String,
|
||||
enum: ['image', 'video', 'academy'],
|
||||
enum: ['image', 'video', 'academy', 'shop_product', 'billboard'],
|
||||
default: 'image'
|
||||
},
|
||||
action: {
|
||||
@@ -45,6 +45,24 @@ const exploreInteractionSchema = new mongoose.Schema({
|
||||
dwellMs: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
/**
|
||||
* Snapshot of the viewer's own profile province/city at view time (only when they've
|
||||
* set one and enabled show_location) — the only geography signal available without
|
||||
* IP-geolocation or live GPS capture, neither of which exists in this codebase. This
|
||||
* is "where the viewer is from", not "where they physically were when they viewed".
|
||||
*/
|
||||
viewerCountry: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
viewerProvince: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
viewerCity: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
64
models/GiftModel.js
Normal file
64
models/GiftModel.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const giftSchema = new mongoose.Schema({
|
||||
sender: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
receiver: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
message: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxLength: 500,
|
||||
default: null
|
||||
},
|
||||
reply: {
|
||||
type: String,
|
||||
trim: true,
|
||||
maxLength: 500,
|
||||
default: null
|
||||
},
|
||||
repliedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
transaction_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Payment',
|
||||
default: null
|
||||
},
|
||||
paymentMethod: {
|
||||
type: String,
|
||||
enum: ['online', 'wallet'],
|
||||
default: 'online'
|
||||
},
|
||||
viaChatMessage: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
opened: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
openedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
})
|
||||
|
||||
giftSchema.plugin(timestamp)
|
||||
giftSchema.plugin(mongoosePaginate)
|
||||
|
||||
const GiftModel = mongoose.model('Gift', giftSchema)
|
||||
module.exports = GiftModel
|
||||
26
models/InstagramImportedPostModel.js
Normal file
26
models/InstagramImportedPostModel.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const mongoose = require('mongoose');
|
||||
const timestamp = require('mongoose-timestamp');
|
||||
|
||||
const modelSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
instagram_media_id: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
},
|
||||
post: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
modelSchema.index({ user: 1, instagram_media_id: 1 }, { unique: true });
|
||||
modelSchema.plugin(timestamp);
|
||||
|
||||
const InstagramImportedPostModel = mongoose.model('InstagramImportedPost', modelSchema);
|
||||
module.exports = InstagramImportedPostModel;
|
||||
@@ -18,6 +18,12 @@ const likeSchema = new mongoose.Schema({
|
||||
}]
|
||||
})
|
||||
|
||||
// postController.js هر بار جزئیاتِ یه پست رو میگیره LikeModel.findOne({userId, postId})
|
||||
// میزنه — بدونِ ایندکس این یه scan کاملِ کالکشنه. عمداً unique نیست — دیتای فعلیِ
|
||||
// production حداقل یه گروهِ تکراریِ {postId,userId} داره؛ ساختِ ایندکسِ unique روی
|
||||
// این دیتا شکست میخوره. بعد از پاکسازیِ دستیِ تکراریها میشه unique هم اضافه کرد
|
||||
likeSchema.index({ postId: 1, userId: 1 })
|
||||
|
||||
// تعریف مدل LikeModel
|
||||
likeSchema.plugin(timestamp)
|
||||
const LikeModel = mongoose.model('Like', likeSchema)
|
||||
|
||||
@@ -44,6 +44,16 @@ const messageSchema = new Schema({
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
/** Countdown length in seconds — timer starts when receiver views the message */
|
||||
selfDestructSeconds: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
/** When the receiver opened the message and the self-destruct timer started */
|
||||
selfDestructStartedAt: {
|
||||
type: Date,
|
||||
required: false
|
||||
},
|
||||
/** Auto-delete after this time (self-destruct messages) */
|
||||
expiresAt: {
|
||||
type: Date,
|
||||
@@ -58,9 +68,27 @@ const messageSchema = new Schema({
|
||||
viewOnceOpenedAt: {
|
||||
type: Date,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
reactions: [
|
||||
{
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
emoji: { type: String, required: true, trim: true }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// GET /api/v1/chat فیلتر $or روی هر دو جهتِ {senderId,receiverId} میزنه و createdAt
|
||||
// رو سورت میکنه؛ messageController.js هم شمارشِ نخوندهها رو با {receiverId,senderId,
|
||||
// readStatus} و {receiverId,readStatus} میزنه — همهی اینها روی هر بارِ باز کردنِ
|
||||
// لیستِ چت/یه ترد اجرا میشن، بدونِ ایندکس یعنی scan کاملِ کالکشن
|
||||
messageSchema.index({ senderId: 1, receiverId: 1, createdAt: -1 })
|
||||
messageSchema.index({ receiverId: 1, senderId: 1, createdAt: -1 })
|
||||
messageSchema.index({ receiverId: 1, readStatus: 1 })
|
||||
|
||||
const MessageModel = mongoose.model('Message', messageSchema)
|
||||
|
||||
module.exports = MessageModel
|
||||
|
||||
@@ -27,6 +27,27 @@ const notificationSchema = new mongoose.Schema({
|
||||
read: {
|
||||
type: Boolean,
|
||||
default: false // وضعیت خوانده شده یا نشده
|
||||
},
|
||||
actor_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: false
|
||||
},
|
||||
// فقط برای اعلانهای همگانی پنل — جدا از read (که یعنی صفحه اعلانها باز شده)،
|
||||
// seen یعنی پوش واقعاً به دستگاه رسیده و clicked یعنی کاربر روش ضربه زده
|
||||
broadcast_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'AdminBroadcast',
|
||||
required: false,
|
||||
index: true
|
||||
},
|
||||
push_delivered_seen: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
push_clicked: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -22,6 +22,27 @@ const offerSchema = new mongoose.Schema({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Payment',
|
||||
default: null
|
||||
},
|
||||
// سهم ۳۰٪ گیرنده که فقط با پاسخگویی ظرف ۲۴ ساعت به اولین پیام دایرکت واریز میشود
|
||||
pendingCommissionAmount: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
firstMessageAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
replyDeadline: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
repliedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
commissionCredited: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
152
models/OrderModel.js
Normal file
152
models/OrderModel.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const variantSnapshotSchema = new mongoose.Schema({
|
||||
color: { type: String, default: null },
|
||||
size: { type: String, default: null },
|
||||
weight: { type: String, default: null },
|
||||
price: { type: Number, required: true }
|
||||
}, { _id: false })
|
||||
|
||||
const buyerAddressSnapshotSchema = new mongoose.Schema({
|
||||
first_name: { type: String, default: null },
|
||||
last_name: { type: String, default: null },
|
||||
mobile: { type: String, default: null },
|
||||
address: { type: String, default: null },
|
||||
province: { type: Object, default: null },
|
||||
city: { type: Object, default: null },
|
||||
lat: { type: String, default: null },
|
||||
lng: { type: String, default: null }
|
||||
}, { _id: false })
|
||||
|
||||
const orderSchema = new mongoose.Schema({
|
||||
buyer: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
listing: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopProductListing',
|
||||
required: true
|
||||
},
|
||||
variantId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
required: true
|
||||
},
|
||||
variantSnapshot: {
|
||||
type: variantSnapshotSchema,
|
||||
required: true
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1
|
||||
},
|
||||
/** Short, human-searchable reference code shown on invoices (derived from _id, generated post-create). */
|
||||
orderNumber: {
|
||||
type: String,
|
||||
index: true,
|
||||
default: null
|
||||
},
|
||||
unit_price: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
shipping_method: {
|
||||
type: String,
|
||||
enum: ['post', 'tipax', 'chapar', 'intercity_freight', 'courier', 'own_vehicle', null],
|
||||
default: null
|
||||
},
|
||||
shipping_cost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
commission_amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
seller_net_amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
total_amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [
|
||||
'pending_payment',
|
||||
'on_hold',
|
||||
'failed',
|
||||
'processing',
|
||||
'shipped',
|
||||
'ready_for_pickup',
|
||||
'completed',
|
||||
'cancelled',
|
||||
'refunded'
|
||||
],
|
||||
default: 'pending_payment',
|
||||
index: true
|
||||
},
|
||||
tracking_code: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
paymentAuthority: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
buyerAddressSnapshot: {
|
||||
type: buyerAddressSnapshotSchema,
|
||||
default: null
|
||||
},
|
||||
fulfilledAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
completedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
buyerConfirmedReceiptAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
receiptDeadlineAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
returnWindowExpiresAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
walletReleased: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
cancelReason: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
refundReason: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
orderSchema.plugin(timestamp)
|
||||
orderSchema.plugin(mongoosePaginate)
|
||||
|
||||
const OrderModel = mongoose.model('Order', orderSchema)
|
||||
module.exports = OrderModel
|
||||
31
models/OrderReportModel.js
Normal file
31
models/OrderReportModel.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const orderReportSchema = new mongoose.Schema({
|
||||
order: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Order',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
buyer: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['open', 'resolved'],
|
||||
default: 'open'
|
||||
}
|
||||
})
|
||||
|
||||
orderReportSchema.plugin(timestamp)
|
||||
|
||||
const OrderReportModel = mongoose.model('OrderReport', orderReportSchema)
|
||||
module.exports = OrderReportModel
|
||||
35
models/PartnerSearchAlertModel.js
Normal file
35
models/PartnerSearchAlertModel.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
/**
|
||||
* A saved "find a partner" search — kept active indefinitely (per product
|
||||
* decision) so the searcher keeps getting notified on every new matching
|
||||
* user, not just once. `notified_user_ids` dedupes so the same matched user
|
||||
* doesn't trigger a repeat notification for the same alert.
|
||||
*/
|
||||
const partnerSearchAlertSchema = new mongoose.Schema({
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
criteria: {
|
||||
type: mongoose.Schema.Types.Mixed,
|
||||
required: true
|
||||
},
|
||||
notified_user_ids: {
|
||||
type: [mongoose.Schema.Types.ObjectId],
|
||||
default: []
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
partnerSearchAlertSchema.plugin(timestamp)
|
||||
|
||||
const PartnerSearchAlertModel = mongoose.model('PartnerSearchAlert', partnerSearchAlertSchema)
|
||||
module.exports = PartnerSearchAlertModel
|
||||
@@ -28,6 +28,10 @@ const paymentSchema = new mongoose.Schema({
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Advertising'
|
||||
},
|
||||
order_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Order'
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
|
||||
24
models/PostBookmarkModel.js
Normal file
24
models/PostBookmarkModel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const postBookmarkSchema = new mongoose.Schema({
|
||||
postId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
postBookmarkSchema.index({ postId: 1, userId: 1 }, { unique: true })
|
||||
|
||||
postBookmarkSchema.plugin(timestamp)
|
||||
const PostBookmarkModel = mongoose.model('PostBookmark', postBookmarkSchema)
|
||||
|
||||
module.exports = PostBookmarkModel
|
||||
@@ -29,31 +29,106 @@ const modelSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 1000,
|
||||
maxLength: 2000,
|
||||
default: null,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
enum: ['pending', 'accept', 'reject'],
|
||||
enum: ['pending', 'temp_accept', 'accept', 'reject'],
|
||||
default: 'pending',
|
||||
},
|
||||
report_hidden: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
is_archived: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
archived_at: {
|
||||
type: Date,
|
||||
default: null,
|
||||
},
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
},
|
||||
tagged_users: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
}],
|
||||
linked_course_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Course',
|
||||
default: null,
|
||||
},
|
||||
linked_project_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Project',
|
||||
default: null,
|
||||
},
|
||||
linked_billboard_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Advertising',
|
||||
default: null,
|
||||
},
|
||||
linked_shop_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
default: null,
|
||||
},
|
||||
linked_shop_product_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopProductListing',
|
||||
default: null,
|
||||
},
|
||||
// برخلاف بقیهی linked_*_id ها که _id خودِ آیتم لینکشده رو نگه میدارن، این یکی userId
|
||||
// مالک کانفیگ رزروئه (نه _id خودِ BookingConfig) — چون صفحهی عمومی رزرو با userId
|
||||
// مسیردهی میشه (/booking/[userId]) نه شناسهی کانفیگ
|
||||
linked_booking_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
default: null,
|
||||
},
|
||||
likes: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
}],
|
||||
lat: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null,
|
||||
},
|
||||
lng: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null,
|
||||
},
|
||||
country: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
province: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
city: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
comments: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// getUserPosts/getUserPostsWeb (postController.js) هر بار پروفایل باز میشه دقیقاً
|
||||
// همین سه فیلد رو فیلتر و createdAt رو سورت میکنن — بدونِ ایندکس یه scan کامل +
|
||||
// sort توی حافظهست
|
||||
modelSchema.index({ user_id: 1, status: 1, is_archived: 1, createdAt: -1 });
|
||||
|
||||
modelSchema.plugin(timestamp);
|
||||
modelSchema.plugin(mongoosePaginate);
|
||||
const PostModel = mongoose.model('Post', modelSchema);
|
||||
|
||||
33
models/PostReportModel.js
Normal file
33
models/PostReportModel.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const postReportSchema = new mongoose.Schema({
|
||||
post_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
reporter_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
reason: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: '',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
postReportSchema.index({ post_id: 1, reporter_id: 1 })
|
||||
|
||||
postReportSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('PostReport', postReportSchema)
|
||||
28
models/PostSendModel.js
Normal file
28
models/PostSendModel.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const postSendSchema = new mongoose.Schema({
|
||||
postId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
senderId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
receiverId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
postSendSchema.plugin(timestamp)
|
||||
const PostSendModel = mongoose.model('PostSend', postSendSchema)
|
||||
|
||||
module.exports = PostSendModel
|
||||
22
models/PostShareModel.js
Normal file
22
models/PostShareModel.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const postShareSchema = new mongoose.Schema({
|
||||
postId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
postShareSchema.plugin(timestamp)
|
||||
const PostShareModel = mongoose.model('PostShare', postShareSchema)
|
||||
|
||||
module.exports = PostShareModel
|
||||
35
models/ProductCatalogModel.js
Normal file
35
models/ProductCatalogModel.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const productCatalogSchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
category: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopCategory',
|
||||
default: null
|
||||
},
|
||||
default_images: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
createdBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
productCatalogSchema.index({ name: 'text' })
|
||||
productCatalogSchema.plugin(timestamp)
|
||||
|
||||
const ProductCatalogModel = mongoose.model('ProductCatalog', productCatalogSchema)
|
||||
module.exports = ProductCatalogModel
|
||||
@@ -42,11 +42,25 @@ const projectSchema = new mongoose.Schema({
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
age_min: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
age_max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
conversation_projects: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
country: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
province: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -55,6 +69,30 @@ const projectSchema = new mongoose.Schema({
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
lat: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
lng: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
neighborhood: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 1024,
|
||||
default: null
|
||||
},
|
||||
offer_time: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -71,6 +109,73 @@ const projectSchema = new mongoose.Schema({
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
payment_method: {
|
||||
type: String,
|
||||
enum: ['monthly', 'daily', 'hourly', 'commission', 'negotiable'],
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
start_date: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 64,
|
||||
default: null
|
||||
},
|
||||
work_hours_start: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 32,
|
||||
default: null
|
||||
},
|
||||
work_hours_end: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 32,
|
||||
default: null
|
||||
},
|
||||
collaboration_type: {
|
||||
type: String,
|
||||
enum: ['full_time', 'part_time', 'project', 'internship'],
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
experience: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
maxLength: 64,
|
||||
default: null
|
||||
},
|
||||
roles: [{
|
||||
expertise: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true
|
||||
},
|
||||
sub_expertise: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
number_of_person: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
height_min: { type: Number, default: null },
|
||||
height_max: { type: Number, default: null },
|
||||
weight_min: { type: Number, default: null },
|
||||
weight_max: { type: Number, default: null },
|
||||
size_min: { type: Number, default: null },
|
||||
size_max: { type: Number, default: null },
|
||||
eye_color: { type: String, default: null },
|
||||
hair_color: { type: String, default: null },
|
||||
portfolio_required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
}],
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -95,7 +200,7 @@ const projectSchema = new mongoose.Schema({
|
||||
// فیلد برای وضعیت پروژه
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pre_payment', 'paid', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'],
|
||||
enum: ['pre_payment', 'paid', 'temp_accept', 'accepted', 'rejected', 'done', 'cancled', 'ongoing'],
|
||||
default: 'pre_payment'
|
||||
},
|
||||
public_status: {
|
||||
@@ -175,6 +280,11 @@ const projectSchema = new mongoose.Schema({
|
||||
isExpired: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// آیا مبلغ باقیمانده (پس از کسر کارمزد) هنگام تکمیل پروژه واریز شده است
|
||||
completionShareCredited: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -12,6 +12,17 @@ const requestSchema = new mongoose.Schema({
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
role_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
role_expertise: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
time: {
|
||||
type: Number,
|
||||
required: true
|
||||
@@ -20,6 +31,17 @@ const requestSchema = new mongoose.Schema({
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
portfolio: [{
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['image', 'video'],
|
||||
required: true
|
||||
}
|
||||
}],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'accepted', 'rejected'],
|
||||
|
||||
32
models/ReturnRequestModel.js
Normal file
32
models/ReturnRequestModel.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const returnRequestSchema = new mongoose.Schema({
|
||||
order: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Order',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
buyer: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
reason: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'approved', 'rejected', 'refunded'],
|
||||
default: 'pending',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
returnRequestSchema.plugin(timestamp)
|
||||
|
||||
const ReturnRequestModel = mongoose.model('ReturnRequest', returnRequestSchema)
|
||||
module.exports = ReturnRequestModel
|
||||
31
models/ShopCartItemModel.js
Normal file
31
models/ShopCartItemModel.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const shopCartItemSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
listing: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopProductListing',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
variantId: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
quantity: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
min: 1
|
||||
}
|
||||
})
|
||||
|
||||
shopCartItemSchema.index({ user: 1, listing: 1, variantId: 1 }, { unique: true })
|
||||
shopCartItemSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('ShopCartItem', shopCartItemSchema)
|
||||
31
models/ShopCategoryModel.js
Normal file
31
models/ShopCategoryModel.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const subSubCategorySchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
const subCategorySchema = new mongoose.Schema({
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
sub_sub_categories: [subSubCategorySchema]
|
||||
})
|
||||
|
||||
const shopCategorySchema = new mongoose.Schema({
|
||||
category: {
|
||||
type: String,
|
||||
unique: true
|
||||
},
|
||||
sub_categories: [subCategorySchema],
|
||||
relatedExpertise: [{
|
||||
type: String
|
||||
}]
|
||||
})
|
||||
|
||||
shopCategorySchema.plugin(timestamp)
|
||||
|
||||
const ShopCategoryModel = mongoose.model('ShopCategory', shopCategorySchema)
|
||||
module.exports = ShopCategoryModel
|
||||
62
models/ShopChatMessageModel.js
Normal file
62
models/ShopChatMessageModel.js
Normal file
@@ -0,0 +1,62 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
/**
|
||||
* Fully separate from the main site chat (MessageModel): no encryption, no
|
||||
* message deletion. A conversation is identified by the (shop, buyer) pair.
|
||||
*/
|
||||
const shopChatMessageSchema = new mongoose.Schema({
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
buyer: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
senderRole: {
|
||||
type: String,
|
||||
enum: ['shop', 'buyer'],
|
||||
required: true
|
||||
},
|
||||
senderId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
content: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
file: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
enum: ['image', 'video', null],
|
||||
default: null
|
||||
},
|
||||
readByShop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
readByBuyer: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
shopChatMessageSchema.index({ shop: 1, buyer: 1, createdAt: 1 })
|
||||
shopChatMessageSchema.plugin(timestamp)
|
||||
|
||||
const ShopChatMessageModel = mongoose.model('ShopChatMessage', shopChatMessageSchema)
|
||||
|
||||
module.exports = ShopChatMessageModel
|
||||
23
models/ShopFollowModel.js
Normal file
23
models/ShopFollowModel.js
Normal file
@@ -0,0 +1,23 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const shopFollowSchema = new mongoose.Schema({
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
shopFollowSchema.plugin(timestamp)
|
||||
shopFollowSchema.index({ shop: 1, user: 1 }, { unique: true })
|
||||
|
||||
const ShopFollowModel = mongoose.model('ShopFollow', shopFollowSchema)
|
||||
module.exports = ShopFollowModel
|
||||
142
models/ShopModel.js
Normal file
142
models/ShopModel.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const shippingMethodSchema = new mongoose.Schema({
|
||||
method: {
|
||||
type: String,
|
||||
enum: ['post', 'tipax', 'chapar', 'intercity_freight', 'courier', 'own_vehicle'],
|
||||
required: true
|
||||
},
|
||||
cost: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
delivery_days: {
|
||||
type: Number,
|
||||
default: 3
|
||||
}
|
||||
}, { _id: false })
|
||||
|
||||
const responseScheduleSchema = new mongoose.Schema({
|
||||
day: {
|
||||
type: String,
|
||||
enum: ['sat', 'sun', 'mon', 'tue', 'wed', 'thu', 'fri'],
|
||||
required: true
|
||||
},
|
||||
start_time: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
end_time: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}, { _id: false })
|
||||
|
||||
const shopSchema = new mongoose.Schema({
|
||||
owner: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: true
|
||||
},
|
||||
logo: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
shipping_methods: {
|
||||
type: [shippingMethodSchema],
|
||||
default: []
|
||||
},
|
||||
cod_available: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
same_day_available: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
free_shipping_threshold: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
estimated_delivery_text: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
has_physical_location: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
country: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
province: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
city: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
neighbourhood: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
address: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
lat: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
lng: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
contact: {
|
||||
mobile: { type: String, default: null },
|
||||
telegram: { type: String, default: null },
|
||||
whatsapp: { type: String, default: null },
|
||||
instagram: { type: String, default: null },
|
||||
landline: { type: String, default: null }
|
||||
},
|
||||
response_schedule: {
|
||||
type: [responseScheduleSchema],
|
||||
default: []
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['draft', 'pending_review', 'active', 'inactive', 'rejected'],
|
||||
default: 'draft',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
shopSchema.plugin(timestamp)
|
||||
shopSchema.plugin(mongoosePaginate)
|
||||
|
||||
const ShopModel = mongoose.model('Shop', shopSchema)
|
||||
module.exports = ShopModel
|
||||
22
models/ShopProductFavoriteModel.js
Normal file
22
models/ShopProductFavoriteModel.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const shopProductFavoriteSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
listing: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ShopProductListing',
|
||||
required: true,
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
shopProductFavoriteSchema.index({ user: 1, listing: 1 }, { unique: true })
|
||||
shopProductFavoriteSchema.plugin(timestamp)
|
||||
|
||||
module.exports = mongoose.model('ShopProductFavorite', shopProductFavoriteSchema)
|
||||
98
models/ShopProductListingModel.js
Normal file
98
models/ShopProductListingModel.js
Normal file
@@ -0,0 +1,98 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const variantSchema = new mongoose.Schema({
|
||||
color: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
weight: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
price: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
discount_price: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
stock: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
sku: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
const shopProductListingSchema = new mongoose.Schema({
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
catalogProduct: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'ProductCatalog',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
trim: true,
|
||||
required: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
category: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
sub_category: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
sub_sub_category: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
images: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
primaryImageIndex: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
variants: {
|
||||
type: [variantSchema],
|
||||
default: []
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending_shop_approval', 'active', 'inactive', 'rejected'],
|
||||
default: 'pending_shop_approval',
|
||||
index: true
|
||||
}
|
||||
})
|
||||
|
||||
shopProductListingSchema.plugin(timestamp)
|
||||
shopProductListingSchema.plugin(mongoosePaginate)
|
||||
|
||||
const ShopProductListingModel = mongoose.model('ShopProductListing', shopProductListingSchema)
|
||||
module.exports = ShopProductListingModel
|
||||
37
models/ShopRatingModel.js
Normal file
37
models/ShopRatingModel.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const shopRatingSchema = new mongoose.Schema({
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
order: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Order',
|
||||
required: true
|
||||
},
|
||||
userId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
rating: {
|
||||
type: Number,
|
||||
required: true,
|
||||
min: 1,
|
||||
max: 5
|
||||
},
|
||||
comment: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
shopRatingSchema.plugin(timestamp)
|
||||
|
||||
const ShopRatingModel = mongoose.model('ShopRating', shopRatingSchema)
|
||||
module.exports = ShopRatingModel
|
||||
29
models/SmartNotificationLogModel.js
Normal file
29
models/SmartNotificationLogModel.js
Normal file
@@ -0,0 +1,29 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
// جلوگیری از نوتیف تکراری برای یک کاربر دربارهی یک پست خاص — سبک و موقت (۳۰ روز TTL)،
|
||||
// شبیه نقشی که ExploreInteractionModel برای ردیابی سبک بازی میکنه
|
||||
const smartNotificationLogSchema = new mongoose.Schema({
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
post_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
createdAt: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
expires: 60 * 60 * 24 * 30 // ۳۰ روز
|
||||
}
|
||||
})
|
||||
|
||||
smartNotificationLogSchema.index({ user_id: 1, post_id: 1, type: 1 }, { unique: true })
|
||||
|
||||
module.exports = mongoose.model('SmartNotificationLog', smartNotificationLogSchema)
|
||||
@@ -1,19 +1,35 @@
|
||||
const mongoose = require('mongoose')
|
||||
|
||||
const countriesSchema = new mongoose.Schema({
|
||||
id: Number,
|
||||
iso2: String,
|
||||
name: String,
|
||||
name_fa: String,
|
||||
slug: String
|
||||
})
|
||||
|
||||
const provincesSchema = new mongoose.Schema({
|
||||
id: Number,
|
||||
name: String,
|
||||
slug: String
|
||||
// نام به زبان خودِ کشور (فعلاً فقط برای ایران پر شده) — برای نمایش مستقل از زبان رابط کاربری
|
||||
name_fa: String,
|
||||
slug: String,
|
||||
// Iran (source_id 103 in data/geo/countries.json) for every pre-existing province, so
|
||||
// callers that don't pass a country filter keep seeing exactly the old Iran-only list.
|
||||
country_id: { type: Number, default: null }
|
||||
})
|
||||
|
||||
const citiesSchema = new mongoose.Schema({
|
||||
id: Number,
|
||||
name: String,
|
||||
name_fa: String,
|
||||
slug: String,
|
||||
province_id: Number
|
||||
province_id: Number,
|
||||
country_id: { type: Number, default: null }
|
||||
})
|
||||
|
||||
const CountryModel = mongoose.model('Country', countriesSchema)
|
||||
const ProvinceModel = mongoose.model('Province', provincesSchema)
|
||||
const CityModel = mongoose.model('City', citiesSchema)
|
||||
|
||||
module.exports = { ProvinceModel, CityModel }
|
||||
module.exports = { CountryModel, ProvinceModel, CityModel }
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
// متنهای روی استوری با مختصات نسبی (۰ تا ۱) تا در هر ابعادی درست رندر شوند
|
||||
// متنها و لینکهای روی استوری با مختصات نسبی (۰ تا ۱) تا در هر ابعادی درست رندر شوند —
|
||||
// type مشخص میکنه اورلی متنیه یا لینکِ یکی از موارد قابللینک (پکیج/پروژه/بیلبورد/فروشگاه/
|
||||
// کالا/رزرو، همونهایی که توی ثبت پست هم قابللینکان)
|
||||
const overlaySchema = new mongoose.Schema({
|
||||
type: { type: String, enum: ['text', 'link'], default: 'text' },
|
||||
text: { type: String, default: '' },
|
||||
x: { type: Number, default: 0.5 },
|
||||
y: { type: Number, default: 0.5 },
|
||||
@@ -10,7 +13,11 @@ const overlaySchema = new mongoose.Schema({
|
||||
color: { type: String, default: '#ffffff' },
|
||||
background: { type: String, default: '' },
|
||||
rotation: { type: Number, default: 0 },
|
||||
align: { type: String, default: 'center' }
|
||||
align: { type: String, default: 'center' },
|
||||
// فقط برای type:'link'
|
||||
link_kind: { type: String, default: null },
|
||||
link_id: { type: String, default: null },
|
||||
link_title: { type: String, default: null }
|
||||
}, { _id: false })
|
||||
|
||||
const storySchema = new mongoose.Schema({
|
||||
@@ -33,6 +40,11 @@ const storySchema = new mongoose.Schema({
|
||||
type: [overlaySchema],
|
||||
default: []
|
||||
},
|
||||
linked_post_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post',
|
||||
default: null
|
||||
},
|
||||
expires_at: {
|
||||
type: Date,
|
||||
required: true,
|
||||
@@ -47,7 +59,23 @@ const storySchema = new mongoose.Schema({
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}]
|
||||
}],
|
||||
likes: [{
|
||||
user_id: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User'
|
||||
},
|
||||
liked_at: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
}
|
||||
}],
|
||||
admin_status: {
|
||||
type: String,
|
||||
enum: ['active', 'inactive'],
|
||||
default: 'active',
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
storySchema.index({ expires_at: 1 }, { expireAfterSeconds: 0 })
|
||||
|
||||
24
models/UserFollowModel.js
Normal file
24
models/UserFollowModel.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const userFollowSchema = new mongoose.Schema({
|
||||
followerId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
followingId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true,
|
||||
},
|
||||
})
|
||||
|
||||
userFollowSchema.index({ followerId: 1, followingId: 1 }, { unique: true })
|
||||
|
||||
userFollowSchema.plugin(timestamp)
|
||||
const UserFollowModel = mongoose.model('UserFollow', userFollowSchema)
|
||||
|
||||
module.exports = UserFollowModel
|
||||
@@ -6,7 +6,8 @@ const userSchema = new mongoose.Schema({
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
unique: true
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
otp: {
|
||||
type: String,
|
||||
@@ -21,6 +22,93 @@ const userSchema = new mongoose.Schema({
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
email: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
lowercase: true,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
google_id: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
auth_provider: {
|
||||
type: String,
|
||||
required: false,
|
||||
enum: ['mobile', 'google', 'instagram'],
|
||||
default: 'mobile'
|
||||
},
|
||||
instagram_id: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
unique: true,
|
||||
sparse: true
|
||||
},
|
||||
instagram_username: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
default: null
|
||||
},
|
||||
instagram_account_type: {
|
||||
type: String,
|
||||
required: false,
|
||||
enum: ['BUSINESS', 'MEDIA_CREATOR', null],
|
||||
default: null
|
||||
},
|
||||
instagram_access_token_enc: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
instagram_token_expires_at: {
|
||||
type: Date,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
instagram_connected_at: {
|
||||
type: Date,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
instagram_last_synced_at: {
|
||||
type: Date,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
instagram_sync_prefs: {
|
||||
bio: { type: Boolean, default: false },
|
||||
posts: { type: Boolean, default: false },
|
||||
reels: { type: Boolean, default: false },
|
||||
profile_picture: { type: Boolean, default: false },
|
||||
auto_update: { type: Boolean, default: false }
|
||||
},
|
||||
email_verified: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
totp_secret: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
totp_pending_secret: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
totp_enabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
user_name: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -69,6 +157,21 @@ const userSchema = new mongoose.Schema({
|
||||
maxLength: 255,
|
||||
default: 'none'
|
||||
},
|
||||
license_verified: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
gold_badge: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
verify_badge: {
|
||||
type: String,
|
||||
enum: ['none', 'gray', 'blue', 'green', 'gold'],
|
||||
default: 'none'
|
||||
},
|
||||
profile_image: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -97,6 +200,14 @@ const userSchema = new mongoose.Schema({
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
display_sub_expertise: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
gender: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -145,6 +256,73 @@ const userSchema = new mongoose.Schema({
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
marital_status: {
|
||||
type: String,
|
||||
enum: ['single', 'divorced'],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
residence_type: {
|
||||
type: String,
|
||||
enum: ['owner', 'tenant'],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
residence_area: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
vehicle_type: {
|
||||
type: String,
|
||||
enum: ['none', 'car', 'motorcycle'],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
vehicle_details: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
personal_style: {
|
||||
type: String,
|
||||
enum: ['formal', 'sporty'],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
job: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 255,
|
||||
default: null
|
||||
},
|
||||
income_range: {
|
||||
type: String,
|
||||
enum: ['under_5m', '5m_10m', '10m_20m', 'over_20m'],
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
moral_traits: {
|
||||
type: String,
|
||||
required: false,
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 1024,
|
||||
default: null
|
||||
},
|
||||
personal_details_public: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
cooperation_type: {
|
||||
type: String,
|
||||
required: false,
|
||||
@@ -203,6 +381,10 @@ const userSchema = new mongoose.Schema({
|
||||
maxLength: 512,
|
||||
default: null
|
||||
},
|
||||
country: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
province: {
|
||||
type: Object,
|
||||
default: null
|
||||
@@ -263,7 +445,10 @@ const userSchema = new mongoose.Schema({
|
||||
trim: true,
|
||||
minLength: 1,
|
||||
maxLength: 512,
|
||||
default: null
|
||||
// پیشفرض قبلاً null بود — یعنی تا اولین کامنت امتیازدار روی کاربر (recalculateUserRating
|
||||
// در utils/commentRating.js)، هیچ سطحی نداشت. الان از همون رشتهای که getUserLevel با
|
||||
// صفر ستاره برمیگردونه استفاده میکنیم، تا هر کاربر از ابتدا سطح واقعی داشته باشه
|
||||
default: 'تازه وارد'
|
||||
},
|
||||
user_score: {
|
||||
type: String,
|
||||
@@ -322,6 +507,15 @@ const userSchema = new mongoose.Schema({
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
block_permanent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
lifetime_free_creations: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
advertising_profiles: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'AdvertisingProfile'
|
||||
@@ -362,7 +556,78 @@ const userSchema = new mongoose.Schema({
|
||||
last_free_offer_date: {
|
||||
type: Date,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
account_status: {
|
||||
type: String,
|
||||
enum: ['active', 'deactivated'],
|
||||
default: 'active',
|
||||
index: true
|
||||
},
|
||||
deactivated_at: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
reactivation_locked_until: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
scheduled_purge_at: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
allow_save_posts: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
ghost_mode: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
post_location_enabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
is_private: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// تنظیمات کیف پول — امکان دریافت هدیه (پروفایل + چت) و اینکه آیا درخواست همکاری
|
||||
// (offer) به این کاربر رایگانه یا طبق قیمت معمول پرداختی میشه
|
||||
gift_enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
offer_payment_enabled: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
preferred_language: {
|
||||
type: String,
|
||||
enum: require('../utils/appLanguages').APP_LANGUAGE_CODES,
|
||||
default: 'fa'
|
||||
},
|
||||
e2ee_public_key: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null
|
||||
},
|
||||
pinned_post_ids: {
|
||||
type: [{
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Post'
|
||||
}],
|
||||
default: []
|
||||
},
|
||||
// توکنهای FCM دستگاههای کاربر — آرایه چون یک کاربر میتونه چند دستگاه اندروید داشته باشه
|
||||
fcm_tokens: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
// توکنهای FCM Web Push (مرورگر) — جدا از fcm_tokens چون توکنهای وب و اندروید فرمت/طول عمر متفاوتی دارن
|
||||
fcm_web_tokens: {
|
||||
type: [String],
|
||||
default: []
|
||||
},
|
||||
// contactInfo: {
|
||||
// phone: String,
|
||||
// mobile: String,
|
||||
@@ -387,5 +652,25 @@ const userSchema = new mongoose.Schema({
|
||||
|
||||
userSchema.plugin(timestamp)
|
||||
userSchema.plugin(mongoosePaginate)
|
||||
|
||||
// وقتی فیلدهای مرتبط با «یافتن همکار» تغییر میکنن (یا ثبتنام کامل میشه)، جستجوهای
|
||||
// ذخیرهشدهی فعال رو چک میکنه — همهی کنترلرهای ویرایش پروفایل در نهایت save() صدا
|
||||
// میزنن، پس اینجا تکنقطهایترین جاییه که بدون تکرار در هر کنترلر جدا کار میکنه.
|
||||
//
|
||||
// isModified() باید در pre('save') خونده بشه، نه post('save') — مونگوس وضعیت
|
||||
// «تغییر کرده» رو بهعنوان بخشی از خود save() پاک میکنه، پس تا زمانی که به
|
||||
// post('save') برسیم isModified() برای همهی فیلدها false برمیگرده. مقدار رو
|
||||
// از طریق $locals بین pre و post منتقل میکنیم.
|
||||
userSchema.pre('save', function (next) {
|
||||
this.$locals.partnerAlertsRelevant = require('../utils/partnerMatch').shouldCheck(this)
|
||||
next()
|
||||
})
|
||||
|
||||
userSchema.post('save', function (doc) {
|
||||
if (doc.$locals.partnerAlertsRelevant) {
|
||||
require('../utils/partnerMatch').checkPartnerAlerts(doc).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
const UserModel = mongoose.model('User', userSchema)
|
||||
module.exports = UserModel
|
||||
|
||||
30
models/UserSuggestionModel.js
Normal file
30
models/UserSuggestionModel.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const mongoose = require('mongoose')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
|
||||
const userSuggestionSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
trim: true,
|
||||
minLength: 3,
|
||||
maxLength: 2000
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['new', 'in_progress', 'done', 'rejected'],
|
||||
default: 'new'
|
||||
}
|
||||
})
|
||||
|
||||
userSuggestionSchema.plugin(timestamp)
|
||||
userSuggestionSchema.plugin(mongoosePaginate)
|
||||
|
||||
const UserSuggestionModel = mongoose.model('UserSuggestion', userSuggestionSchema)
|
||||
|
||||
module.exports = UserSuggestionModel
|
||||
54
models/WalletTransactionModel.js
Normal file
54
models/WalletTransactionModel.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const walletTransactionSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
bucket: {
|
||||
type: String,
|
||||
enum: ['shop', 'gift', 'charged', 'academy', 'project', 'booking'],
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
/** Only set for bucket:'shop' entries — lets a seller with multiple shops drill into one. */
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
default: null,
|
||||
index: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['credit', 'debit'],
|
||||
required: true
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
referenceType: {
|
||||
type: String,
|
||||
enum: ['order', 'withdrawal', 'payment', null],
|
||||
default: null
|
||||
},
|
||||
referenceId: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
default: null
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
trim: true,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
walletTransactionSchema.plugin(timestamp)
|
||||
walletTransactionSchema.plugin(mongoosePaginate)
|
||||
|
||||
const WalletTransactionModel = mongoose.model('WalletTransaction', walletTransactionSchema)
|
||||
module.exports = WalletTransactionModel
|
||||
50
models/WalletWithdrawalRequestModel.js
Normal file
50
models/WalletWithdrawalRequestModel.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const mongoose = require('mongoose')
|
||||
const mongoosePaginate = require('mongoose-paginate-v2')
|
||||
const timestamp = require('mongoose-timestamp')
|
||||
|
||||
const walletWithdrawalRequestSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
index: true
|
||||
},
|
||||
bucket: {
|
||||
type: String,
|
||||
enum: ['shop', 'gift', 'academy', 'project', 'booking'],
|
||||
required: true
|
||||
},
|
||||
shop: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: 'Shop',
|
||||
default: null
|
||||
},
|
||||
amount: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
shabaSnapshot: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['در حال انجام', 'منتظر تایید', 'انجام شده'],
|
||||
default: 'منتظر تایید',
|
||||
index: true
|
||||
},
|
||||
requestedAt: {
|
||||
type: Date,
|
||||
default: Date.now
|
||||
},
|
||||
paidAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
walletWithdrawalRequestSchema.plugin(timestamp)
|
||||
walletWithdrawalRequestSchema.plugin(mongoosePaginate)
|
||||
|
||||
const WalletWithdrawalRequestModel = mongoose.model('WalletWithdrawalRequest', walletWithdrawalRequestSchema)
|
||||
module.exports = WalletWithdrawalRequestModel
|
||||
Reference in New Issue
Block a user