187 lines
7.9 KiB
JavaScript
187 lines
7.9 KiB
JavaScript
const PartnerSearchAlertModel = require('../models/PartnerSearchAlertModel')
|
|
const NotificationModel = require('../models/NotificationModel')
|
|
const { sendPushToTokens } = require('./pushNotification')
|
|
|
|
let ioInstance = null
|
|
|
|
/** یکبار از index.js صدا زده میشه — مثل الگوی notifyFollowers.js */
|
|
function setIo (io) {
|
|
ioInstance = io
|
|
}
|
|
|
|
function ageFromBirthday (birthday) {
|
|
if (!birthday) return null
|
|
const diffMs = Date.now() - new Date(birthday).getTime()
|
|
return Math.floor(diffMs / (365.25 * 24 * 60 * 60 * 1000))
|
|
}
|
|
|
|
function inRange (value, min, max) {
|
|
if (value === null || value === undefined || Number.isNaN(value)) return false
|
|
if (min !== undefined && min !== null && value < min) return false
|
|
if (max !== undefined && max !== null && value > max) return false
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* Compares one user document against one saved alert's criteria. Runs as a
|
|
* plain JS predicate (not a Mongo query) since it always checks a single
|
|
* already-loaded document, not a collection.
|
|
*/
|
|
function matchesCriteria (user, criteria = {}) {
|
|
if (criteria.gender && user.gender !== criteria.gender) return false
|
|
|
|
if (criteria.heightMin !== undefined || criteria.heightMax !== undefined) {
|
|
if (!inRange(Number(user.height), criteria.heightMin, criteria.heightMax)) return false
|
|
}
|
|
if (criteria.weightMin !== undefined || criteria.weightMax !== undefined) {
|
|
if (!inRange(Number(user.weight), criteria.weightMin, criteria.weightMax)) return false
|
|
}
|
|
if (criteria.sizeMin !== undefined || criteria.sizeMax !== undefined) {
|
|
if (!inRange(Number(user.size), criteria.sizeMin, criteria.sizeMax)) return false
|
|
}
|
|
if (criteria.ageMin !== undefined || criteria.ageMax !== undefined) {
|
|
if (!inRange(ageFromBirthday(user.birthday), criteria.ageMin, criteria.ageMax)) return false
|
|
}
|
|
|
|
if (criteria.hair_color && user.hair_color !== criteria.hair_color) return false
|
|
if (criteria.eye_color && user.eye_color !== criteria.eye_color) return false
|
|
if (criteria.marital_status && user.marital_status !== criteria.marital_status) return false
|
|
if (criteria.residence_type && user.residence_type !== criteria.residence_type) return false
|
|
if (criteria.vehicle_type && user.vehicle_type !== criteria.vehicle_type) return false
|
|
if (criteria.personal_style && user.personal_style !== criteria.personal_style) return false
|
|
if (criteria.income_range && user.income_range !== criteria.income_range) return false
|
|
|
|
if (criteria.job) {
|
|
if (!user.job || !user.job.toLowerCase().includes(criteria.job.toLowerCase())) return false
|
|
}
|
|
|
|
// Multi-select dating-profile fields: match if the candidate's stored
|
|
// array overlaps ANY of the alert's selected values — mirrors the $in
|
|
// semantics of criteriaToMongoFilter in partnerSearchController.js.
|
|
if (criteria.relationship_goal?.length) {
|
|
const userValues = user.relationship_goal || []
|
|
if (!criteria.relationship_goal.some((v) => userValues.includes(v))) return false
|
|
}
|
|
if (criteria.personality_traits?.length) {
|
|
const userValues = user.personality_traits || []
|
|
if (!criteria.personality_traits.some((v) => userValues.includes(v))) return false
|
|
}
|
|
if (criteria.lifestyle?.length) {
|
|
const userValues = user.lifestyle || []
|
|
if (!criteria.lifestyle.some((v) => userValues.includes(v))) return false
|
|
}
|
|
if (criteria.interests?.length) {
|
|
const userValues = user.interests || []
|
|
if (!criteria.interests.some((v) => userValues.includes(v))) return false
|
|
}
|
|
if (criteria.values?.length) {
|
|
const userValues = user.values || []
|
|
if (!criteria.values.some((v) => userValues.includes(v))) return false
|
|
}
|
|
|
|
// Single-select dating-profile fields: exact match.
|
|
if (criteria.seeking_gender && user.seeking_gender !== criteria.seeking_gender) return false
|
|
if (criteria.relationship_view && user.relationship_view !== criteria.relationship_view) return false
|
|
if (criteria.geo_preference && user.geo_preference !== criteria.geo_preference) return false
|
|
if (criteria.smoking_status && user.smoking_status !== criteria.smoking_status) return false
|
|
|
|
// children_status is multi-select (a user may pick more than one status) —
|
|
// same overlap semantics as the multi-select fields above.
|
|
if (criteria.children_status?.length) {
|
|
const userValues = user.children_status || []
|
|
if (!criteria.children_status.some((v) => userValues.includes(v))) return false
|
|
}
|
|
|
|
if (criteria.country_id && user.country?.id !== criteria.country_id) return false
|
|
if (criteria.province_id && user.province?.id !== criteria.province_id) return false
|
|
if (criteria.city_id && user.city?.id !== criteria.city_id) return false
|
|
|
|
return true
|
|
}
|
|
|
|
/** Only re-check when a field a saved search could plausibly filter on has
|
|
* actually changed — avoids running on unrelated saves like `last_online`. */
|
|
const MATCH_RELEVANT_FIELDS = [
|
|
'gender', 'height', 'weight', 'size', 'birthday', 'hair_color', 'eye_color',
|
|
'marital_status', 'residence_type', 'residence_area', 'vehicle_type',
|
|
'vehicle_details', 'personal_style', 'job', 'income_range',
|
|
'country', 'province', 'city', 'is_Register',
|
|
'relationship_goal', 'seeking_gender', 'desired_age_range', 'personality_traits',
|
|
'lifestyle', 'interests', 'values', 'relationship_view', 'geo_preference',
|
|
'children_status', 'smoking_status'
|
|
]
|
|
|
|
function shouldCheck (user) {
|
|
return MATCH_RELEVANT_FIELDS.some((field) => user.isModified(field))
|
|
}
|
|
|
|
/** Finds every active alert this user now matches (that hasn't already been
|
|
* notified about this exact user) and notifies each alert owner once.
|
|
* Caller is responsible for the shouldCheck() gate — isModified() is only
|
|
* meaningful in pre('save'), so it can't be re-checked here in a post('save')
|
|
* context (Mongoose clears the modified-paths tracking before post hooks run). */
|
|
async function checkPartnerAlerts (user) {
|
|
try {
|
|
const alerts = await PartnerSearchAlertModel.find({
|
|
active: true,
|
|
user_id: { $ne: user._id },
|
|
notified_user_ids: { $ne: user._id }
|
|
})
|
|
if (alerts.length === 0) return
|
|
|
|
const matchedAlerts = alerts.filter((alert) => matchesCriteria(user, alert.criteria))
|
|
if (matchedAlerts.length === 0) return
|
|
|
|
const title = 'همکار پیشنهادی جدید'
|
|
const description = 'کاربری با مشخصات مورد نظر شما به مجموعه مدستاگرام پیوست'
|
|
|
|
const notifications = matchedAlerts.map((alert) => ({
|
|
user_id: alert.user_id,
|
|
type: 'partner_match',
|
|
title,
|
|
description,
|
|
project_post_id: user._id,
|
|
actor_id: user._id
|
|
}))
|
|
// create() (not insertMany) — insertMany skips document middleware, so the
|
|
// mongoose-timestamp plugin never runs and createdAt/updatedAt are left
|
|
// unset, which sorts these notifications to the very last page of the list
|
|
await NotificationModel.create(notifications)
|
|
|
|
await Promise.all(
|
|
matchedAlerts.map((alert) =>
|
|
PartnerSearchAlertModel.updateOne(
|
|
{ _id: alert._id },
|
|
{ $addToSet: { notified_user_ids: user._id } }
|
|
)
|
|
)
|
|
)
|
|
|
|
if (ioInstance) {
|
|
matchedAlerts.forEach((alert) => {
|
|
try {
|
|
ioInstance.to(`user:${String(alert.user_id)}`).emit('notification', { type: 'partner_match', title, description })
|
|
} catch (_) {}
|
|
})
|
|
}
|
|
|
|
const UserModel = require('../models/UserModel')
|
|
const alertOwners = await UserModel.find({ _id: { $in: matchedAlerts.map((a) => a.user_id) } })
|
|
.select('fcm_tokens fcm_web_tokens')
|
|
.lean()
|
|
const tokens = alertOwners.flatMap((u) => [...(u.fcm_tokens || []), ...(u.fcm_web_tokens || [])])
|
|
if (tokens.length > 0) {
|
|
await sendPushToTokens(tokens, {
|
|
title,
|
|
description,
|
|
type: 'partner_match',
|
|
data: { project_post_id: String(user._id) }
|
|
}).catch(() => {})
|
|
}
|
|
} catch (error) {
|
|
console.error('[checkPartnerAlerts] خطا:', error.message)
|
|
}
|
|
}
|
|
|
|
module.exports = { matchesCriteria, checkPartnerAlerts, setIo, ageFromBirthday, shouldCheck }
|