mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
const mdl_TierCategories = require('../../models/tiers/tier_categories.mdl');
|
|
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
|
|
const mdl_PlanPolicies = require('../../models/tiers/plan_policies.mdl');
|
|
const mdl_PaymentPolicies = require('../../models/tiers/payment_policies.mdl');
|
|
const mdl_SystemBadges = require('../../models/system_badges/system_badges.mdl');
|
|
const Asset = require('../../models/assets/assets.mdl');
|
|
const R = require('../../utils/response.util');
|
|
const logActivity = require('../../utils/logActivity.util');
|
|
|
|
require('../../models/tiers/tier.associations');
|
|
|
|
const VALID_RULE_TYPES = new Set([
|
|
'course_subscription_access',
|
|
'required_active_tier',
|
|
'group_restriction',
|
|
]);
|
|
|
|
async function validateRules(rules) {
|
|
if (!Array.isArray(rules)) return 'access_rules must be an array.';
|
|
|
|
// Load valid tier slugs dynamically from DB
|
|
const categories = await mdl_TierCategories.findAll({ attributes: ['slug'] });
|
|
const validSlugs = new Set(categories.map((c) => c.slug));
|
|
|
|
for (const rule of rules) {
|
|
if (!VALID_RULE_TYPES.has(rule.type)) return `Unknown rule type: ${rule.type}`;
|
|
|
|
if (rule.type === 'course_subscription_access') {
|
|
if (!Array.isArray(rule.levels) || !rule.levels.length)
|
|
return 'course_subscription_access.levels must be a non-empty array of tier slugs.';
|
|
}
|
|
|
|
if (rule.type === 'required_active_tier') {
|
|
if (!rule.tier || !validSlugs.has(rule.tier))
|
|
return `required_active_tier.tier must be a valid tier category slug.`;
|
|
}
|
|
|
|
if (rule.type === 'group_restriction') {
|
|
if (!Array.isArray(rule.group_ids))
|
|
return 'group_restriction.group_ids must be an array.';
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const ASSET_ATTRS = ['asset_id', 'file_url', 'display_name', 'mime_type'];
|
|
|
|
// ─── PLAN POLICIES ────────────────────────────────────────────────────────────
|
|
|
|
exports.getPlanPolicy = async (req, res) => {
|
|
try {
|
|
const plan = await mdl_TierPlans.findByPk(req.params.planId);
|
|
if (!plan) return R.error(res, 'Plan not found.', 404);
|
|
|
|
const policy = await mdl_PlanPolicies.findOne({ where: { plan_id: req.params.planId } });
|
|
return R.success(res, 'Policy retrieved.', policy ?? null);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET PLAN POLICY]', err);
|
|
return R.error(res, 'Could not retrieve policy.', 500);
|
|
}
|
|
};
|
|
|
|
exports.upsertPlanPolicy = async (req, res) => {
|
|
try {
|
|
const { planId } = req.params;
|
|
|
|
const plan = await mdl_TierPlans.findByPk(planId);
|
|
if (!plan) return R.error(res, 'Plan not found.', 404);
|
|
|
|
const { access_rules } = req.body;
|
|
|
|
let parsedRules = [];
|
|
if (access_rules !== undefined) {
|
|
try {
|
|
parsedRules = typeof access_rules === 'string' ? JSON.parse(access_rules) : access_rules;
|
|
} catch {
|
|
return R.error(res, 'access_rules is not valid JSON.', 400);
|
|
}
|
|
const err = await validateRules(parsedRules);
|
|
if (err) return R.error(res, err, 400);
|
|
}
|
|
|
|
let existing = await mdl_PlanPolicies.findOne({ where: { plan_id: planId } });
|
|
|
|
const payload = {
|
|
plan_id: planId,
|
|
access_rules: access_rules !== undefined ? parsedRules : (existing?.access_rules ?? []),
|
|
};
|
|
|
|
if (!existing) {
|
|
existing = await mdl_PlanPolicies.create(payload);
|
|
logActivity(req.user?.user_id, 'create_plan_policy', { entityType: 'plan_policy', details: { plan_id: planId } });
|
|
} else {
|
|
await existing.update(payload);
|
|
logActivity(req.user?.user_id, 'update_plan_policy', { entityType: 'plan_policy', details: { plan_id: planId } });
|
|
}
|
|
|
|
const result = await mdl_PlanPolicies.findOne({ where: { plan_id: planId } });
|
|
return R.success(res, 'Policy saved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][UPSERT PLAN POLICY]', err);
|
|
return R.error(res, 'Could not save policy.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── PAYMENT POLICIES ─────────────────────────────────────────────────────────
|
|
|
|
const VALID_PROMO_TYPES = new Set(['flat', 'percent']);
|
|
const VALID_WINDOW_UNITS = new Set(['minutes', 'hours', 'days']);
|
|
|
|
function validatePromoRules(rules) {
|
|
if (!Array.isArray(rules)) return 'promo_rules must be an array.';
|
|
for (const r of rules) {
|
|
if (!r.code || typeof r.code !== 'string') return 'Each promo rule must have a code string.';
|
|
if (!VALID_PROMO_TYPES.has(r.type)) return `Invalid promo type "${r.type}". Must be 'flat' or 'percent'.`;
|
|
if (!r.value || Number(r.value) <= 0) return 'Promo rule value must be a positive number.';
|
|
if (r.max_uses != null && (!Number.isInteger(r.max_uses) || r.max_uses < 1))
|
|
return 'max_uses must be a positive integer.';
|
|
if (r.expires_at != null && isNaN(new Date(r.expires_at).getTime()))
|
|
return 'expires_at must be a valid ISO date string.';
|
|
if (r.min_amount != null && Number(r.min_amount) < 0)
|
|
return 'min_amount must be a non-negative number.';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function validateRefundPolicy(rp) {
|
|
if (typeof rp !== 'object' || rp === null || Array.isArray(rp))
|
|
return 'refund_policy must be an object.';
|
|
if (rp.allowed != null && typeof rp.allowed !== 'boolean')
|
|
return 'refund_policy.allowed must be a boolean.';
|
|
if (rp.window_unit != null && !VALID_WINDOW_UNITS.has(rp.window_unit))
|
|
return `refund_policy.window_unit must be 'minutes', 'hours', or 'days'.`;
|
|
if (rp.window_value != null && (typeof rp.window_value !== 'number' || rp.window_value <= 0))
|
|
return 'refund_policy.window_value must be a positive number.';
|
|
return null;
|
|
}
|
|
|
|
exports.getPaymentPolicy = async (req, res) => {
|
|
try {
|
|
const plan = await mdl_TierPlans.findByPk(req.params.planId);
|
|
if (!plan) return R.error(res, 'Plan not found.', 404);
|
|
|
|
const policy = await mdl_PaymentPolicies.findOne({ where: { plan_id: req.params.planId } });
|
|
return R.success(res, 'Payment policy retrieved.', policy ?? null);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET PAYMENT POLICY]', err);
|
|
return R.error(res, 'Could not retrieve payment policy.', 500);
|
|
}
|
|
};
|
|
|
|
exports.upsertPaymentPolicy = async (req, res) => {
|
|
try {
|
|
const { planId } = req.params;
|
|
|
|
const plan = await mdl_TierPlans.findByPk(planId);
|
|
if (!plan) return R.error(res, 'Plan not found.', 404);
|
|
|
|
const { promo_rules, refund_policy, allowed_providers } = req.body;
|
|
|
|
if (promo_rules !== undefined) {
|
|
const err = validatePromoRules(promo_rules);
|
|
if (err) return R.error(res, err, 400);
|
|
}
|
|
|
|
if (refund_policy !== undefined) {
|
|
const err = validateRefundPolicy(refund_policy);
|
|
if (err) return R.error(res, err, 400);
|
|
}
|
|
|
|
if (allowed_providers !== undefined) {
|
|
if (!Array.isArray(allowed_providers) || !allowed_providers.every((p) => typeof p === 'string'))
|
|
return R.error(res, 'allowed_providers must be an array of provider name strings.', 400);
|
|
}
|
|
|
|
let existing = await mdl_PaymentPolicies.findOne({ where: { plan_id: planId } });
|
|
|
|
const DEFAULTS = { allowed: true, window_value: 5, window_unit: 'minutes', reason_required: false };
|
|
|
|
const payload = {
|
|
plan_id: planId,
|
|
promo_rules: promo_rules !== undefined ? promo_rules : (existing?.promo_rules ?? []),
|
|
refund_policy: refund_policy !== undefined ? refund_policy : (existing?.refund_policy ?? DEFAULTS),
|
|
allowed_providers: allowed_providers !== undefined ? allowed_providers : (existing?.allowed_providers ?? ['paypal']),
|
|
};
|
|
|
|
if (!existing) {
|
|
existing = await mdl_PaymentPolicies.create(payload);
|
|
logActivity(req.user?.user_id, 'create_payment_policy', { entityType: 'payment_policy', details: { plan_id: planId } });
|
|
} else {
|
|
await existing.update(payload);
|
|
logActivity(req.user?.user_id, 'update_payment_policy', { entityType: 'payment_policy', details: { plan_id: planId } });
|
|
}
|
|
|
|
const result = await mdl_PaymentPolicies.findOne({ where: { plan_id: planId } });
|
|
return R.success(res, 'Payment policy saved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][UPSERT PAYMENT POLICY]', err);
|
|
return R.error(res, 'Could not save payment policy.', 500);
|
|
}
|
|
};
|
|
|
|
// ─── SYSTEM BADGES ────────────────────────────────────────────────────────────
|
|
|
|
exports.getSystemBadges = async (req, res) => {
|
|
try {
|
|
const badges = await mdl_SystemBadges.findAll({
|
|
include: [{ model: Asset, as: 'asset', attributes: ASSET_ATTRS, required: false }],
|
|
order: [['key', 'ASC']],
|
|
});
|
|
return R.success(res, 'System badges retrieved.', badges);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET SYSTEM BADGES]', err);
|
|
return R.error(res, 'Could not retrieve system badges.', 500);
|
|
}
|
|
};
|
|
|
|
exports.getSystemBadge = async (req, res) => {
|
|
try {
|
|
const badge = await mdl_SystemBadges.findOne({
|
|
where: { key: req.params.key },
|
|
include: [{ model: Asset, as: 'asset', attributes: ASSET_ATTRS, required: false }],
|
|
});
|
|
if (!badge) return R.error(res, 'System badge not found.', 404);
|
|
return R.success(res, 'System badge retrieved.', badge);
|
|
} catch (err) {
|
|
console.error('[ADMIN][GET SYSTEM BADGE]', err);
|
|
return R.error(res, 'Could not retrieve system badge.', 500);
|
|
}
|
|
};
|
|
|
|
exports.upsertSystemBadge = async (req, res) => {
|
|
try {
|
|
const { key } = req.params;
|
|
const { asset_id, label, description, information, active_from, active_until } = req.body;
|
|
|
|
let existing = await mdl_SystemBadges.findOne({ where: { key } });
|
|
|
|
const payload = {
|
|
key,
|
|
asset_id: asset_id !== undefined ? (asset_id || null) : existing?.asset_id ?? null,
|
|
label: label ?? existing?.label ?? key,
|
|
description: description ?? existing?.description ?? null,
|
|
information: information ?? existing?.information ?? null,
|
|
active_from: active_from !== undefined ? (active_from || null) : existing?.active_from ?? null,
|
|
active_until: active_until !== undefined ? (active_until || null) : existing?.active_until ?? null,
|
|
};
|
|
|
|
if (!existing) {
|
|
existing = await mdl_SystemBadges.create(payload);
|
|
logActivity(req.user?.user_id, 'create_system_badge', { entityType: 'system_badge', details: { key } });
|
|
} else {
|
|
await existing.update(payload);
|
|
logActivity(req.user?.user_id, 'update_system_badge', { entityType: 'system_badge', details: { key } });
|
|
}
|
|
|
|
const result = await mdl_SystemBadges.findOne({
|
|
where: { key },
|
|
include: [{ model: Asset, as: 'asset', attributes: ASSET_ATTRS, required: false }],
|
|
});
|
|
return R.success(res, 'System badge saved.', result);
|
|
} catch (err) {
|
|
console.error('[ADMIN][UPSERT SYSTEM BADGE]', err);
|
|
return R.error(res, 'Could not save system badge.', 500);
|
|
}
|
|
};
|