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', 'widowed', 'not_in_relationship', 'prefer_not_to_say'], required: false, default: null }, relationship_goal: { type: [{ type: String, enum: [ 'friendship', 'serious_partner', 'marriage_intro', 'emotional_relationship', 'travel_event_companion', 'social_networking', 'not_sure' ] }], required: false, default: [] }, seeking_gender: { type: String, enum: ['female', 'male', 'both', 'not_important'], required: false, default: null }, desired_age_range: { type: String, enum: ['18_24', '25_30', '31_35', '36_40', '41_50', 'over_50', 'not_important'], required: false, default: null }, personality_traits: { type: [{ type: String, enum: [ 'kind', 'loyal', 'funny', 'calm', 'energetic', 'responsible', 'ambitious', 'emotional', 'logical', 'patient', 'honest', 'romantic', 'independent', 'family_oriented', 'adventurous', 'introvert', 'extrovert' ] }], required: false, default: [] }, lifestyle: { type: [{ type: String, enum: [ 'active_healthy', 'homebody_calm', 'social_outgoing', 'busy_career', 'traveler', 'night_owl', 'early_riser', 'minimalist', 'luxury_lover' ] }], required: false, default: [] }, interests: { type: [{ type: String, enum: [ 'movies_series', 'music', 'books_reading', 'sports', 'cooking', 'photography', 'travel', 'nature', 'gaming', 'art_painting', 'fashion', 'pets', 'dancing', 'language_learning' ] }], required: false, default: [] }, values: { type: [{ type: String, enum: [ 'family', 'career_growth', 'religion_spirituality', 'personal_freedom', 'financial_stability', 'health_fitness', 'personal_growth', 'excitement_new_experiences' ] }], required: false, default: [] }, relationship_view: { type: String, enum: [ 'traditional', 'modern', 'long_term', 'friendship_first', 'marriage_soon', 'friendship_only_for_now' ], required: false, default: null }, geo_preference: { type: String, enum: [ 'same_city_only', 'nearby_cities', 'whole_province', 'whole_iran', 'even_abroad', 'online_is_fine' ], required: false, default: null }, children_status: { type: [{ type: String, enum: ['no_children', 'has_children', 'open_to_children', 'not_now', 'prefer_not_to_say'] }], required: false, default: [] }, smoking_status: { type: String, enum: ['none', 'sometimes', 'yes', 'doesnt_matter', 'prefer_not_to_say'], 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: ['casual', 'classic', 'minimal', 'formal', 'street', 'romantic'], required: false, default: null }, job: { type: String, required: false, trim: true, minLength: 1, maxLength: 255, default: null }, income_range: { type: String, enum: [ 'under_40m', '40m_50m', '50m_60m', '60m_70m', '70m_80m', '80m_90m', '90m_100m', '100m_150m', '150m_200m', 'over_200m' ], required: false, 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 }, // وقتی «ریست الگوریتم» زده می‌شه ست می‌شه — buildUserExploreSignals لایک/کامنت/آفر // قبل از این تاریخ رو نادیده می‌گیره تا کاربر واقعاً مثل تازه‌وارد رفتار ببینه، نه // فقط کش رتبه‌بندی پاک بشه درحالی‌که سیگنال‌های شخصی‌سازی قدیمی هنوز اثر دارن algorithm_reset_at: { type: Date, default: null }, rate: { type: String, trim: true, maxLength: 512, default: null }, // کد یکتای معرفی‌کننده — با اولین درخواستِ GET /users/referral-code به‌صورت lazy // ساخته می‌شه (referralController.js)، نه از قبل برای همه‌ی کاربرها. unique+sparse // چون این کد توی لینکِ عمومیِ اشتراک‌گذاری میره و باید واقعاً غیرتکراری باشه referral_code: { type: String, trim: true, minLength: 1, maxLength: 512, default: null, unique: true, sparse: true }, // کاربری که این کاربر رو با لینکِ معرفی‌اش وارد اپ کرده — فقط توی اولین مرحله‌ی // ثبت‌نام (POST /register با mobile) ست می‌شه، دیگه هیچ‌جا عوض نمی‌شه referred_by: { type: mongoose.Schema.Types.ObjectId, ref: 'User', default: null }, // جلوگیری از پرداخت دوباره‌ی پاداشِ معرفی — با تکمیلِ ثبت‌نام (userTypeController) // یک‌بار true می‌شه؛ اگه این مرحله به هر دلیلی دوباره صدا زده بشه، پرداختِ تکراری نمی‌شه referral_rewarded: { type: Boolean, default: false }, 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 }, // تعداد بیلبورد رایگانی که این کاربر تا الان ثبت کرده — سقف کلی (نه روزانه) طبق // utils/billboardFreeLimit.js، مستقل از daily_free_request که بین پروژه/بیلبورد/ // درخواست مشترکه free_billboard_count: { type: Number, default: 0 }, 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 }, // بک‌آپ کلید خصوصیِ E2EE رمزنگاری‌شده با رمز/PIN انتخابیِ کاربر (سرور هیچ‌وقت PIN یا // کلید خام رو نمی‌بینه، فقط این بلاکِ رمزنگاری‌شده رو ذخیره می‌کنه) — برای بازیابیِ // هویتِ رمزنگاری وقتی هیچ دستگاه دیگه‌ای لینک‌شده در دسترس نیست e2ee_key_backup: { type: { salt: String, iv: String, ciphertext: String, iterations: Number }, required: false, default: null }, // یک رشته‌ی تصادفیِ ۳۲بایتیِ per-account که با درخواستِ احرازهویت‌شده به همین // اکانت، از هر دستگاهی (وب/اندروید) قابل‌دریافته — به‌جای PIN دستی، به‌عنوانِ // رمزِ رمزنگاریِ e2ee_key_backup استفاده می‌شه تا بازیابیِ هویتِ E2EE روی دستگاهِ // جدید کاملاً خودکار و بدون دخالتِ کاربر انجام بشه. توجه: این یعنی تضمینِ // end-to-end دیگه واقعی نیست — هرکسی که به این فیلد (یا سرور) دسترسی پیدا کنه // می‌تونه بک‌آپ رو باز کنه؛ این تصمیم آگاهانه به نفعِ هیچ‌وقت‌گم‌نشدنِ پیام‌ها گرفته شده e2ee_auto_backup_secret: { 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