const mongoose = require('mongoose') const mongoosePaginate = require('mongoose-paginate-v2') const timestamp = require('mongoose-timestamp') const userSchema = new mongoose.Schema({ mobile: { type: String, required: false, trim: true, unique: true, sparse: true }, otp: { type: String, required: false, trim: true, minLength: 6, maxLength: 6, default: null }, otpSentAt: { type: Date, 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, trim: true, minLength: 1, maxLength: 255, default: null }, password: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, first_name: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, last_name: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, user_type: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, is_verified: { type: String, required: false, trim: true, minLength: 1, 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, trim: true, default: null }, national_card_image: { type: String, required: false, trim: true, default: null }, expertise: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, sub_expertise: { type: [String], // تغییر نوع به آرایه از استرینگ required: false, trim: true, minLength: 1, maxLength: 255, default: null }, display_sub_expertise: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, gender: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, height: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, weight: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, size: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, eye_color: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, hair_color: { type: String, required: false, trim: true, minLength: 1, 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, trim: true, minLength: 1, maxLength: 255, default: null }, cooperation_abroad: { type: Boolean, required: false, default: null }, conversation_projects: { type: Boolean, required: false, default: null }, bio: { type: String, required: false, minLength: 1, maxLength: 1024, default: null }, national_code: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, birthday: { type: Date, required: false, default: null }, is_Register: { type: String, default: false }, shaba: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, address: { type: String, trim: true, minLength: 1, maxLength: 512, default: null }, country: { type: Object, default: null }, province: { type: Object, default: null }, city: { type: Object, default: null }, lat: { type: String, trim: true, minLength: 1, maxLength: 512, default: null }, lng: { type: String, trim: true, minLength: 1, maxLength: 512, default: null }, show_location: { type: Boolean, required: false, default: null }, rate: { type: String, trim: true, maxLength: 512, default: null }, referral_code: { type: String, trim: true, minLength: 1, maxLength: 512, default: null }, shop_id: { type: mongoose.Schema.Types.ObjectId, ref: 'Shop', default: null }, last_online: { type: Date, // ذخیره تاریخ و زمان آخرین بازدید default: Date.now // تنظیم تاریخ به تاریخ فعلی }, business_license: { type: String, required: false, trim: true, default: null }, user_level: { type: String, trim: true, minLength: 1, maxLength: 512, // پیش‌فرض قبلاً null بود — یعنی تا اولین کامنت امتیازدار روی کاربر (recalculateUserRating // در utils/commentRating.js)، هیچ سطحی نداشت. الان از همون رشته‌ای که getUserLevel با // صفر ستاره برمی‌گردونه استفاده می‌کنیم، تا هر کاربر از ابتدا سطح واقعی داشته باشه default: 'تازه وارد' }, user_score: { type: String, trim: true, minLength: 1, maxLength: 512, default: null }, last_post: { type: Object, default: null }, // user_ratings: [{ // project_id: { // type: mongoose.Schema.Types.ObjectId, // ref: 'Project', // required: true // }, // creator_id: { // type: mongoose.Schema.Types.ObjectId, // ref: 'User', // required: true // }, // rating: { // type: Number, // required: true // }, // comment: { // type: String, // trim: true // } // }], daily_free_request: { type: Number, default: 1 }, last_free_request_date: { type: Date, default: null }, blocked_by: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], blocked_users: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }], block_status: { type: Boolean, required: false, default: null }, blocked_until: { type: Date, 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' }], services: [{ title: { type: String, required: true, trim: true }, originalPrice: { type: String, required: true }, discountPrice: { type: String, required: false }, discountPercentage: { type: Number, required: false }, status: { type: String, default: "UnderReview", required: false }, image: { type: String, required: false, trim: true } }], monthly_free_offer: { type: Number, default: 1 }, 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, // telegramLink: String, // whatsappNumber: String, // instagramLink: String, // saveInfoForNextAds: { type: Boolean, default: false } // }, // adTotalRatings: { // type: Number, // default: 0 // }, // adRatingsCount: { // type: Number, // default: 0 // }, // adAverageRating: { // type: Number, // default: 0 // } }) 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