This commit is contained in:
payacom
2026-08-31 20:45:41 +03:30
parent fa9f23c61d
commit 3f416a7a60
367 changed files with 621895 additions and 6443 deletions

View File

@@ -104,6 +104,10 @@ const AcademyCategorySchema = new mongoose.Schema({
ref: 'User',
required: false,
},
relatedExpertise: {
type: [String],
default: [],
},
}, {
toJSON: { virtuals: true },
toObject: { virtuals: true }

View File

@@ -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,

View 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)

View File

@@ -14,7 +14,10 @@ const advertisingCategorySchema = new mongoose.Schema({
type: Boolean,
required: false,
default: true
}
},
relatedExpertise: [{
type: String
}]
})
advertisingCategorySchema.plugin(timestamp)

View File

@@ -20,7 +20,7 @@ const advertisingCommentSchema = new mongoose.Schema({
},
status: {
type: String,
enum: ['pending', 'accepted', 'rejected'],
enum: ['pending', 'temp_accept', 'accepted', 'rejected'],
default: 'pending'
}
})

View File

@@ -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: {

View 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
View 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

View 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
View 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)

View 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)

View File

@@ -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)

View File

@@ -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'
}
});

View File

@@ -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);

View 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)

View 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)

View File

@@ -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
View 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

View 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;

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}
})

View File

@@ -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
View 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

View 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

View 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

View File

@@ -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

View 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

View File

@@ -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
View 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
View 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
View 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

View 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

View File

@@ -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
}
})

View File

@@ -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'],

View 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

View 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)

View 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

View 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
View 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
View 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

View 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)

View 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
View 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

View 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)

View File

@@ -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 }

View File

@@ -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
View 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

View File

@@ -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

View 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

View 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

View 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