mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -23,7 +23,7 @@ const {
|
||||
CoursePrerequisite, CourseAssessment,
|
||||
UnitQuiz, QuizQuestion, QuizOption,
|
||||
QuizAttempt, AssessmentSession,
|
||||
CourseInstructor,
|
||||
CourseInstructor, CourseAchievement,
|
||||
} = require("../../models/courses/courses.associations");
|
||||
|
||||
const mdl_Users = require("../../models/users/users.mdl");
|
||||
@@ -100,6 +100,8 @@ exports.createCourse = async (req, res) => {
|
||||
course_code, level, subscription,
|
||||
objectives = [],
|
||||
category_ids = [],
|
||||
achievement_keys = [],
|
||||
badge_color, badge_asset_id, badge_image_url,
|
||||
createdBy,
|
||||
} = req.body;
|
||||
|
||||
@@ -107,18 +109,28 @@ exports.createCourse = async (req, res) => {
|
||||
|
||||
const course = await Course.create({
|
||||
title,
|
||||
description: description ?? null,
|
||||
order_index: order_index ?? 0,
|
||||
course_code: course_code ?? null,
|
||||
level: level ?? null,
|
||||
subscription: subscription ?? "free",
|
||||
description: description ?? null,
|
||||
order_index: order_index ?? 0,
|
||||
course_code: course_code ?? null,
|
||||
level: level ?? null,
|
||||
subscription: subscription ?? "free",
|
||||
duration_seconds: 0,
|
||||
createdBy: createdBy ?? null,
|
||||
badge_color: badge_color ?? "purple",
|
||||
badge_asset_id: badge_asset_id ?? null,
|
||||
badge_image_url: badge_image_url ?? null,
|
||||
createdBy: createdBy ?? null,
|
||||
}, { transaction: t });
|
||||
|
||||
await syncObjectivesCreate(CourseObjective, "course_id", course.course_id, objectives, t);
|
||||
await syncJunction(CourseProductCat, course.course_id, category_ids, "category_id", t);
|
||||
|
||||
if (achievement_keys.length) {
|
||||
await CourseAchievement.bulkCreate(
|
||||
achievement_keys.slice(0, 3).map((key, i) => ({ course_id: course.course_id, achievement_key: key, order_index: i })),
|
||||
{ transaction: t },
|
||||
);
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
logActivity(req.user?.user_id, 'create_course', { entityType: 'course', entityId: course.course_id, details: { title: course.title } });
|
||||
return R.success(res, "Course created.", { data: course }, 201);
|
||||
@@ -140,6 +152,7 @@ exports.updateCourse = async (req, res) => {
|
||||
title, description, order_index,
|
||||
course_code, level, subscription,
|
||||
objectives, category_ids,
|
||||
badge_color, badge_asset_id, badge_image_url,
|
||||
updatedBy,
|
||||
} = req.body;
|
||||
|
||||
@@ -149,6 +162,9 @@ exports.updateCourse = async (req, res) => {
|
||||
if (course_code !== undefined) course.course_code = course_code;
|
||||
if (level !== undefined) course.level = level;
|
||||
if (subscription !== undefined) course.subscription = subscription;
|
||||
if (badge_color !== undefined) course.badge_color = badge_color;
|
||||
if (badge_asset_id !== undefined) course.badge_asset_id = badge_asset_id;
|
||||
if (badge_image_url !== undefined) course.badge_image_url = badge_image_url;
|
||||
course.updatedBy = updatedBy ?? null;
|
||||
await course.save({ transaction: t });
|
||||
|
||||
@@ -1760,3 +1776,52 @@ exports.getAssessmentSessions = async (req, res) => {
|
||||
return R.error(res, "Could not retrieve assessment sessions.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
// COURSE ACHIEVEMENTS
|
||||
// ══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
exports.getCourseAchievements = async (req, res) => {
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const rows = await CourseAchievement.findAll({
|
||||
where: { course_id: courseId },
|
||||
order: [["order_index", "ASC"]],
|
||||
});
|
||||
return R.success(res, "Course achievements retrieved.", { data: rows });
|
||||
} catch (err) {
|
||||
console.error("[COURSE][ACHIEVEMENTS][GET]", err);
|
||||
return R.error(res, "Could not retrieve course achievements.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.syncCourseAchievements = async (req, res) => {
|
||||
const t = await sequelize.transaction();
|
||||
try {
|
||||
const { courseId } = req.params;
|
||||
const { achievement_keys = [] } = req.body;
|
||||
|
||||
if (achievement_keys.length > 3)
|
||||
return R.error(res, "Maximum 3 achievements allowed per course.", 400);
|
||||
|
||||
await CourseAchievement.destroy({ where: { course_id: courseId }, transaction: t });
|
||||
|
||||
if (achievement_keys.length) {
|
||||
await CourseAchievement.bulkCreate(
|
||||
achievement_keys.map((key, i) => ({ course_id: courseId, achievement_key: key, order_index: i })),
|
||||
{ transaction: t },
|
||||
);
|
||||
}
|
||||
|
||||
await t.commit();
|
||||
const rows = await CourseAchievement.findAll({
|
||||
where: { course_id: courseId },
|
||||
order: [["order_index", "ASC"]],
|
||||
});
|
||||
return R.success(res, "Course achievements updated.", { data: rows });
|
||||
} catch (err) {
|
||||
await t.rollback();
|
||||
console.error("[COURSE][ACHIEVEMENTS][SYNC]", err);
|
||||
return R.error(res, "Could not update course achievements.", 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -113,16 +113,31 @@ exports.issueTokensBatch = async (req, res) => {
|
||||
storage_provider: "s3",
|
||||
deletedAt: null,
|
||||
},
|
||||
attributes: ["asset_id", "file_type", "storage_key", "mime_type"],
|
||||
attributes: ["asset_id", "file_type", "storage_key", "mime_type", "thumbnail_storage_key"],
|
||||
});
|
||||
|
||||
const ip = resolveIp(req);
|
||||
const tokens = {};
|
||||
const ip = resolveIp(req);
|
||||
const tokens = {};
|
||||
const thumbnails = {};
|
||||
|
||||
for (const asset of assets) {
|
||||
tokens[String(asset.asset_id)] = signToken(asset, req.user.user_id, ip);
|
||||
|
||||
// For image/video assets with a thumbnail — presign it so the browser can
|
||||
// load it directly from Garage without going through the stream proxy.
|
||||
if (asset.thumbnail_storage_key) {
|
||||
try {
|
||||
thumbnails[String(asset.asset_id)] = await s3.getSignedDownloadUrl(
|
||||
asset.thumbnail_storage_key,
|
||||
TOKEN_TTL_SEC,
|
||||
);
|
||||
} catch {
|
||||
// Non-fatal — stream token is the fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return R.success(res, "Tokens issued.", { tokens });
|
||||
return R.success(res, "Tokens issued.", { tokens, thumbnails });
|
||||
} catch (err) {
|
||||
console.error("[ADMIN][MEDIA][TOKENS BATCH]", err);
|
||||
return R.error(res, "Could not issue media tokens.", 500);
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: plan_prices.controller.js (admin)
|
||||
* Type of Program: Controller
|
||||
* Description: Admin CRUD for localized price overrides per tier plan.
|
||||
* Routes: GET/POST/PUT/DELETE /admin/tiers/:id/prices[/:currency]
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jun. 29, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
'use strict';
|
||||
|
||||
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
|
||||
const mdl_PlanPrices = require('../../models/tiers/plan_prices.mdl');
|
||||
const R = require('../../utils/response.util');
|
||||
const logActivity = require('../../utils/logActivity.util');
|
||||
const { isSupported, SUPPORTED_CURRENCIES, validateLocalizedPrice } = require('../../utils/currency.util');
|
||||
|
||||
// ─── GET /admin/tiers/:id/prices ─────────────────────────────────────────────
|
||||
|
||||
exports.getPrices = async (req, res) => {
|
||||
try {
|
||||
const plan = await mdl_TierPlans.findByPk(req.params.id);
|
||||
if (!plan) return R.error(res, 'Plan not found.', 404);
|
||||
|
||||
const prices = await mdl_PlanPrices.findAll({
|
||||
where: { plan_id: plan.plan_id },
|
||||
order: [['currency', 'ASC']],
|
||||
});
|
||||
|
||||
return R.success(res, 'Localized prices retrieved.', prices);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET PLAN PRICES]', err);
|
||||
return R.error(res, 'Could not retrieve localized prices.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── POST /admin/tiers/:id/prices ────────────────────────────────────────────
|
||||
|
||||
exports.addPrice = async (req, res) => {
|
||||
try {
|
||||
const plan = await mdl_TierPlans.findByPk(req.params.id);
|
||||
if (!plan) return R.error(res, 'Plan not found.', 404);
|
||||
|
||||
const { currency, price } = req.body;
|
||||
if (!currency || price === undefined) return R.error(res, 'currency and price are required.', 400);
|
||||
if (!isSupported(currency)) return R.error(res, `Unsupported currency: ${currency}.`, 400);
|
||||
if (currency === plan.currency) return R.error(res, `${currency} is already the plan's base currency.`, 400);
|
||||
if (Number(price) < 0) return R.error(res, 'Price must be 0 or greater.', 400);
|
||||
|
||||
const exists = await mdl_PlanPrices.findOne({ where: { plan_id: plan.plan_id, currency } });
|
||||
if (exists) return R.error(res, `A localized price for ${currency} already exists. Use PUT to update it.`, 409);
|
||||
|
||||
// ── Rate validation ────────────────────────────────────────────────────────
|
||||
const validation = await validateLocalizedPrice(plan.price, plan.currency, price, currency);
|
||||
if (validation.zone === 'block') return R.error(res, validation.message, 422);
|
||||
|
||||
const entry = await mdl_PlanPrices.create({
|
||||
plan_id: plan.plan_id,
|
||||
currency: currency.toUpperCase(),
|
||||
price: Number(price),
|
||||
});
|
||||
|
||||
logActivity(req.user?.user_id, 'add_plan_price', {
|
||||
entityType: 'plan_price',
|
||||
details: { plan_id: plan.plan_id, currency, price },
|
||||
});
|
||||
|
||||
if (validation.zone === 'warn')
|
||||
return res.status(201).json({ success: true, warning: true, message: validation.message, data: entry });
|
||||
|
||||
return R.success(res, 'Localized price added.', entry, 201);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][ADD PLAN PRICE]', err);
|
||||
return R.error(res, 'Could not add localized price.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── PUT /admin/tiers/:id/prices/:currency ───────────────────────────────────
|
||||
|
||||
exports.updatePrice = async (req, res) => {
|
||||
try {
|
||||
const { id, currency } = req.params;
|
||||
const { price } = req.body;
|
||||
|
||||
if (price === undefined) return R.error(res, 'price is required.', 400);
|
||||
if (Number(price) < 0) return R.error(res, 'Price must be 0 or greater.', 400);
|
||||
|
||||
const entry = await mdl_PlanPrices.findOne({ where: { plan_id: id, currency: currency.toUpperCase() } });
|
||||
if (!entry) return R.error(res, `No localized price found for ${currency} on this plan.`, 404);
|
||||
|
||||
// ── Rate validation ────────────────────────────────────────────────────────
|
||||
const plan = await mdl_TierPlans.findByPk(id);
|
||||
const validation = await validateLocalizedPrice(plan.price, plan.currency, price, currency);
|
||||
if (validation.zone === 'block') return R.error(res, validation.message, 422);
|
||||
|
||||
await entry.update({ price: Number(price) });
|
||||
|
||||
logActivity(req.user?.user_id, 'update_plan_price', {
|
||||
entityType: 'plan_price',
|
||||
details: { plan_id: id, currency, price },
|
||||
});
|
||||
|
||||
if (validation.zone === 'warn')
|
||||
return res.status(200).json({ success: true, warning: true, message: validation.message, data: entry });
|
||||
|
||||
return R.success(res, 'Localized price updated.', entry);
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][UPDATE PLAN PRICE]', err);
|
||||
return R.error(res, 'Could not update localized price.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── DELETE /admin/tiers/:id/prices/:currency ────────────────────────────────
|
||||
|
||||
exports.removePrice = async (req, res) => {
|
||||
try {
|
||||
const { id, currency } = req.params;
|
||||
|
||||
const entry = await mdl_PlanPrices.findOne({ where: { plan_id: id, currency: currency.toUpperCase() } });
|
||||
if (!entry) return R.error(res, `No localized price found for ${currency} on this plan.`, 404);
|
||||
|
||||
await entry.destroy();
|
||||
|
||||
logActivity(req.user?.user_id, 'remove_plan_price', {
|
||||
entityType: 'plan_price',
|
||||
details: { plan_id: id, currency },
|
||||
});
|
||||
|
||||
return R.success(res, 'Localized price removed.');
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][REMOVE PLAN PRICE]', err);
|
||||
return R.error(res, 'Could not remove localized price.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// ─── GET /admin/currencies ────────────────────────────────────────────────────
|
||||
|
||||
exports.getCurrencies = async (_req, res) => {
|
||||
return R.success(res, 'Supported currencies retrieved.', SUPPORTED_CURRENCIES);
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
'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_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');
|
||||
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');
|
||||
|
||||
@@ -104,6 +105,103 @@ exports.upsertPlanPolicy = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── 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) => {
|
||||
|
||||
@@ -83,11 +83,18 @@ exports.getPlan = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const DURATION_UNIT_TO_DAYS = { minute: 1 / 1440, hour: 1 / 24, day: 1, month: 30, year: 365 };
|
||||
|
||||
function computeDurationDays(value, unit) {
|
||||
const multiplier = DURATION_UNIT_TO_DAYS[unit] ?? 1;
|
||||
return parseFloat(value) * multiplier;
|
||||
}
|
||||
|
||||
exports.createPlan = async (req, res) => {
|
||||
try {
|
||||
const { tier_category_id, label, duration_days, price, currency } = req.body;
|
||||
if (!tier_category_id || !label || !duration_days || !price)
|
||||
return R.error(res, 'tier_category_id, label, duration_days, and price are required.', 400);
|
||||
const { tier_category_id, label, description, duration_value, duration_unit = 'day', price, currency } = 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);
|
||||
|
||||
const category = await mdl_TierCategories.findByPk(tier_category_id);
|
||||
if (!category || !category.is_active)
|
||||
@@ -96,10 +103,12 @@ exports.createPlan = async (req, res) => {
|
||||
if (category.is_default)
|
||||
return R.error(res, 'Plans cannot be created under the default (Free) tier. Free access is automatic.', 400);
|
||||
|
||||
const duration_days = computeDurationDays(duration_value, duration_unit);
|
||||
|
||||
const plan = await mdl_TierPlans.create({
|
||||
tier_category_id: category.tier_category_id,
|
||||
tier: category.slug,
|
||||
label, duration_days, price, currency,
|
||||
label, description, duration_days, duration_unit, price, currency,
|
||||
});
|
||||
const plain = plan.get({ plain: true });
|
||||
logActivity(req.user?.user_id, 'create_tier_plan', { entityType: 'tier_plan', details: { label: plain.label, tier: plain.tier } });
|
||||
@@ -115,12 +124,22 @@ exports.updatePlan = async (req, res) => {
|
||||
const plan = await mdl_TierPlans.findByPk(req.params.id);
|
||||
if (!plan) return R.error(res, 'Plan not found.', 404);
|
||||
|
||||
const allowed = ['label', 'duration_days', 'price', 'currency', 'is_active', 'tier_category_id'];
|
||||
const allowed = ['label', 'description', 'price', 'currency', 'is_active', 'tier_category_id'];
|
||||
const updates = {};
|
||||
for (const k of allowed) {
|
||||
if (req.body[k] !== undefined) updates[k] = req.body[k];
|
||||
}
|
||||
|
||||
// Recompute duration_days when value or unit changes
|
||||
const { duration_value, duration_unit } = req.body;
|
||||
if (duration_value !== undefined) {
|
||||
const unit = duration_unit ?? plan.duration_unit ?? 'day';
|
||||
updates.duration_days = computeDurationDays(duration_value, unit);
|
||||
updates.duration_unit = unit;
|
||||
} else if (duration_unit !== undefined) {
|
||||
updates.duration_unit = duration_unit;
|
||||
}
|
||||
|
||||
// If tier_category_id is being changed, sync the tier slug
|
||||
if (updates.tier_category_id) {
|
||||
const category = await mdl_TierCategories.findByPk(updates.tier_category_id);
|
||||
@@ -140,6 +159,22 @@ exports.updatePlan = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
exports.getPlanImpact = async (req, res) => {
|
||||
try {
|
||||
const plan = await mdl_TierPlans.findByPk(req.params.id);
|
||||
if (!plan) return R.error(res, 'Plan not found.', 404);
|
||||
|
||||
const active_subscriber_count = await mdl_UserTiers.count({
|
||||
where: { plan_id: req.params.id, status: 'active' },
|
||||
});
|
||||
|
||||
return R.success(res, 'Plan impact retrieved.', { active_subscriber_count });
|
||||
} catch (err) {
|
||||
console.error('[ADMIN][GET PLAN IMPACT]', err);
|
||||
return R.error(res, 'Could not retrieve plan impact.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.archivePlan = async (req, res) => {
|
||||
try {
|
||||
const plan = await mdl_TierPlans.findByPk(req.params.id);
|
||||
|
||||
Reference in New Issue
Block a user