mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
tier plans improving
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -44,7 +44,7 @@ exports.getCategory = async (req, res) => {
|
||||
|
||||
exports.createCategory = async (req, res) => {
|
||||
try {
|
||||
const { slug, name, description, rank, color, badge_asset_id, badge_icon, badge_label } = req.body;
|
||||
const { slug, name, description, rank, color, badge_asset_id, badge_icon, badge_label, is_special } = req.body;
|
||||
if (!slug || !name) return R.error(res, 'slug and name are required.', 400);
|
||||
|
||||
const parsedRank = Number(rank ?? 1);
|
||||
@@ -63,6 +63,7 @@ exports.createCategory = async (req, res) => {
|
||||
badge_label: badge_label ?? null,
|
||||
is_default: false,
|
||||
is_active: true,
|
||||
is_special: !!is_special,
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, 'create_tier_category', { entityType: 'tier_category', details: { slug, name } });
|
||||
@@ -82,7 +83,7 @@ exports.updateCategory = async (req, res) => {
|
||||
const cat = await mdl_TierCategories.findByPk(req.params.id);
|
||||
if (!cat) return R.error(res, 'Tier category not found.', 404);
|
||||
|
||||
const { name, description, rank, color, badge_asset_id, badge_icon, badge_label, is_active } = req.body;
|
||||
const { name, description, rank, color, badge_asset_id, badge_icon, badge_label, is_active, is_special } = req.body;
|
||||
|
||||
if (!cat.is_default && rank !== undefined) {
|
||||
const parsedRank = Number(rank);
|
||||
@@ -98,7 +99,8 @@ exports.updateCategory = async (req, res) => {
|
||||
badge_icon: badge_icon !== undefined ? (badge_icon || null) : cat.badge_icon,
|
||||
badge_label: badge_label !== undefined ? (badge_label || null) : cat.badge_label,
|
||||
// Default category (free) cannot be deactivated
|
||||
is_active: (!cat.is_default && is_active !== undefined) ? is_active : cat.is_active,
|
||||
is_active: (!cat.is_default && is_active !== undefined) ? is_active : cat.is_active,
|
||||
is_special: is_special !== undefined ? !!is_special : cat.is_special,
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, 'update_tier_category', { entityType: 'tier_category', details: { id: cat.tier_category_id, slug: cat.slug } });
|
||||
|
||||
@@ -15,8 +15,11 @@ const VALID_RULE_TYPES = new Set([
|
||||
'course_subscription_access',
|
||||
'required_active_tier',
|
||||
'group_restriction',
|
||||
'item_allowlist',
|
||||
]);
|
||||
|
||||
const VALID_ITEM_TYPES = new Set(['course', 'unit', 'lesson']);
|
||||
|
||||
async function validateRules(rules) {
|
||||
if (!Array.isArray(rules)) return 'access_rules must be an array.';
|
||||
|
||||
@@ -41,6 +44,15 @@ async function validateRules(rules) {
|
||||
if (!Array.isArray(rule.group_ids))
|
||||
return 'group_restriction.group_ids must be an array.';
|
||||
}
|
||||
|
||||
if (rule.type === 'item_allowlist') {
|
||||
if (!VALID_ITEM_TYPES.has(rule.item_type))
|
||||
return `item_allowlist.item_type must be one of: ${[...VALID_ITEM_TYPES].join(', ')}.`;
|
||||
if (!Array.isArray(rule.item_ids) || !rule.item_ids.length)
|
||||
return 'item_allowlist.item_ids must be a non-empty array.';
|
||||
if (rule.item_ids.length > 3)
|
||||
return 'item_allowlist.item_ids must contain at most 3 items.';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ const R = require('../../utils/response.util');
|
||||
const { paginate } = require('../../utils/paginate.util');
|
||||
const { getFieldValues } = require('../../utils/fieldValues.util');
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const UserNotification = require('../../models/notifications/user_notification.mdl');
|
||||
const { NOTIFICATION_REGISTRY } = require('../../data/notifications.data');
|
||||
const { resolveTierPlanUserIds } = require('../../utils/audienceResolver.util');
|
||||
|
||||
const {
|
||||
excludeAttributes: plansExclude,
|
||||
@@ -75,6 +78,7 @@ exports.getPlans = async (req, res) => {
|
||||
jsonbSchemas: plansSchemas,
|
||||
computedAttributes: plansComputed,
|
||||
context: archived ? 'archived' : 'list',
|
||||
auditOptions: { mdl_Users, parentAlias: 'TierPlan' },
|
||||
findOptions: archived ? {
|
||||
paranoid: false,
|
||||
where: { deletedAt: { [Op.ne]: null } },
|
||||
@@ -108,7 +112,7 @@ function computeDurationDays(value, unit) {
|
||||
|
||||
exports.createPlan = async (req, res) => {
|
||||
try {
|
||||
const { tier_category_id, label, description, features, duration_value, duration_unit = 'day', price, currency } = req.body;
|
||||
const { tier_category_id, label, description, features, duration_value, duration_unit = 'day', price, currency, createdBy } = req.body;
|
||||
if (!tier_category_id || !label || !duration_value || !price)
|
||||
return R.error(res, 'tier_category_id, label, duration_value, and price are required.', 400);
|
||||
|
||||
@@ -125,6 +129,7 @@ exports.createPlan = async (req, res) => {
|
||||
tier_category_id: category.tier_category_id,
|
||||
tier: category.slug,
|
||||
label, description, features, duration_days, duration_unit, price, currency,
|
||||
createdBy: createdBy ?? req.user?.user_id ?? null,
|
||||
});
|
||||
const plain = plan.get({ plain: true });
|
||||
logActivity(req.user?.user_id, 'create_tier_plan', { entityType: 'tier_plan', details: { label: plain.label, tier: plain.tier } });
|
||||
@@ -145,6 +150,7 @@ exports.updatePlan = async (req, res) => {
|
||||
for (const k of allowed) {
|
||||
if (req.body[k] !== undefined) updates[k] = req.body[k];
|
||||
}
|
||||
updates.updatedBy = req.body.updatedBy ?? req.user?.user_id ?? null;
|
||||
|
||||
// Recompute duration_days when value or unit changes
|
||||
const { duration_value, duration_unit } = req.body;
|
||||
@@ -197,8 +203,23 @@ exports.archivePlan = async (req, res) => {
|
||||
if (!plan) return R.error(res, 'Plan not found.', 404);
|
||||
if (plan.deletedAt) return R.error(res, 'Plan is already archived.', 400);
|
||||
|
||||
await plan.update({ is_active: false });
|
||||
await plan.update({ is_active: false, deletedBy: req.user?.user_id ?? null });
|
||||
await plan.destroy();
|
||||
|
||||
try {
|
||||
const userIds = await resolveTierPlanUserIds(plan.plan_id);
|
||||
if (userIds.length) {
|
||||
const now = new Date();
|
||||
const notify = NOTIFICATION_REGISTRY.tier_plan_archived.build({ label: plan.label, planId: plan.plan_id });
|
||||
await UserNotification.bulkCreate(
|
||||
userIds.map((user_id) => ({ user_id, ...notify, seen: false, createdAt: now, updatedAt: now })),
|
||||
{ validate: false }
|
||||
);
|
||||
}
|
||||
} catch (notifyErr) {
|
||||
console.error('[ADMIN][ARCHIVE PLAN][NOTIFY]', notifyErr);
|
||||
}
|
||||
|
||||
logActivity(req.user?.user_id, 'archive_tier_plan', { entityType: 'tier_plan', details: { label: plan.label } });
|
||||
return R.success(res, 'Plan archived successfully.');
|
||||
} catch (err) {
|
||||
@@ -222,9 +243,31 @@ exports.bulkArchivePlans = async (req, res) => {
|
||||
|
||||
const activeIds = activePlans.map((p) => p.plan_id);
|
||||
|
||||
await mdl_TierPlans.update({ is_active: false }, { where: { plan_id: activeIds } });
|
||||
await mdl_TierPlans.update({ is_active: false, deletedBy: req.user?.user_id ?? null }, { where: { plan_id: activeIds } });
|
||||
await mdl_TierPlans.destroy({ where: { plan_id: activeIds } });
|
||||
|
||||
try {
|
||||
const holders = await mdl_UserTiers.findAll({
|
||||
attributes: ['user_id', 'plan_id'],
|
||||
where: { plan_id: activeIds, status: 'active' },
|
||||
raw: true,
|
||||
});
|
||||
if (holders.length) {
|
||||
const now = new Date();
|
||||
const labelByPlanId = new Map(activePlans.map((p) => [String(p.plan_id), p.label]));
|
||||
const notifications = holders.map(({ user_id, plan_id }) => ({
|
||||
user_id,
|
||||
...NOTIFICATION_REGISTRY.tier_plan_archived.build({ label: labelByPlanId.get(String(plan_id)), planId: plan_id }),
|
||||
seen: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
}));
|
||||
await UserNotification.bulkCreate(notifications, { validate: false });
|
||||
}
|
||||
} catch (notifyErr) {
|
||||
console.error('[ADMIN][BULK ARCHIVE PLANS][NOTIFY]', notifyErr);
|
||||
}
|
||||
|
||||
logActivity(req.user?.user_id, 'bulk_archive_tier_plans', { entityType: 'tier_plan', details: { ids: activeIds, count: activeIds.length } });
|
||||
return R.success(res, `${activeIds.length} plan(s) archived successfully.`, {
|
||||
archived_ids: activeIds,
|
||||
|
||||
Reference in New Issue
Block a user