Files
starr-philproperties/cron/jobs/expire_user_tiers.cron.js
T
kennethobsequio a572c1e25f add: more commits
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-03 21:40:24 +08:00

101 lines
4.3 KiB
JavaScript

/***********************************************************************************************************************************************************************
* File Name : expire_user_tiers.cron.js
* Type : Cron Job
* Description : Marks active user_tiers rows as 'expired' when their expires_at
* has passed. Runs every minute to support short-duration plans
* (minute- and hour-level plans in addition to day/month/year).
*
* For each expired batch it:
* 1. Bulk-updates matching rows to status = 'expired'.
* 2. Sends an in-app UserNotification to each affected user.
*
* Safety:
* - Only touches rows with expires_at IS NOT NULL so
* manually-granted unlimited tiers (expires_at = NULL) are
* never touched.
* - Bulk update happens before notifications so a restart
* mid-run never re-expires already-expired rows.
*
* Schedule : Every minute ("* * * * *"). Registered by cron/client.cron.js.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 29, 2026
***********************************************************************************************************************************************************************/
'use strict';
const { Op } = require('sequelize');
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
const UserNotification = require('../../models/notifications/user_notification.mdl');
const CronNotificationSetting = require('../../models/notifications/cron_notification_setting.mdl');
const { getNotificationTemplate, renderNotificationContent } = require('../../services/notificationTemplate.service');
require('../../models/tiers/tier.associations');
async function run() {
// ── 1. Find all active tiers whose expires_at has passed ──────────────────
let expired;
try {
expired = await mdl_UserTiers.findAll({
where: {
status: 'active',
expires_at: { [Op.ne]: null, [Op.lte]: new Date() },
},
include: [{
model: mdl_TierPlans,
as: 'plan',
attributes: ['plan_id', 'label', 'tier'],
required: false,
}],
attributes: ['tier_id', 'user_id', 'tier'],
});
} catch (err) {
console.error('[CRON][EXPIRE TIERS] Failed to query user_tiers:', err);
return;
}
if (!expired.length) return;
const tierIds = expired.map((t) => t.tier_id);
// ── 2. Bulk-update to expired ──────────────────────────────────────────────
try {
await mdl_UserTiers.update(
{ status: 'expired' },
{ where: { tier_id: tierIds } }
);
} catch (err) {
console.error('[CRON][EXPIRE TIERS] Bulk update failed:', err);
return;
}
// ── 3. Send in-app notifications (one per affected user) ──────────────────
// Status flip above always happens — only this step is skippable via settings.
const settings = await CronNotificationSetting.findOne({ where: { job_name: 'expireUserTiers' } });
if (!settings || settings.enabled) {
try {
const template = await getNotificationTemplate('tier_expired');
const notifications = expired.map((t) => ({
user_id: t.user_id,
...renderNotificationContent(template, {
tier: t.tier,
label: t.plan?.label ?? null,
planId: t.plan?.plan_id ?? null,
}),
}));
await UserNotification.bulkCreate(notifications, { ignoreDuplicates: true });
} catch (err) {
console.error('[CRON][EXPIRE TIERS] Notification bulkCreate failed:', err);
}
}
console.log(`[CRON][EXPIRE TIERS] Expired ${expired.length} tier(s) for ${new Set(expired.map((t) => t.user_id)).size} user(s).`);
}
module.exports = {
name: 'expireUserTiers',
schedule: '* * * * *',
run,
};