mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -10,10 +10,12 @@
|
||||
* Date Created: Jun. 6, 2026
|
||||
* Modified: Jun. 9, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
|
||||
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
|
||||
const mdl_Payments = require('../../models/tiers/payments.mdl');
|
||||
const mdl_PlanCourses = require('../../models/tiers/plan_courses.mdl');
|
||||
const mdl_TierCategories = require('../../models/tiers/tier_categories.mdl');
|
||||
const mdl_TierPlans = require('../../models/tiers/tier_plans.mdl');
|
||||
const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
|
||||
const mdl_Payments = require('../../models/tiers/payments.mdl');
|
||||
const mdl_SystemBadges = require('../../models/system_badges/system_badges.mdl');
|
||||
const Asset = require('../../models/assets/assets.mdl');
|
||||
const { onTierActivated } = require('../../services/achievements.service');
|
||||
const { Course } = require('../../models/courses/courses.mdl');
|
||||
const paypal = require('../../services/paypal.service');
|
||||
@@ -49,9 +51,42 @@ exports.getMyTier = async (req, res) => {
|
||||
try {
|
||||
const tier = await mdl_UserTiers.findOne({
|
||||
where: { user_id: req.user.user_id, status: 'active' },
|
||||
include: [{
|
||||
model: mdl_TierPlans,
|
||||
as: 'plan',
|
||||
required: false,
|
||||
include: [{
|
||||
model: mdl_TierCategories,
|
||||
as: 'category',
|
||||
required: false,
|
||||
include: [{ model: Asset, as: 'badgeAsset', attributes: ['asset_id', 'file_url', 'display_name'], required: false }],
|
||||
}],
|
||||
}],
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return R.success(res, 'Active tier retrieved.', tier ?? { tier: 'free', status: 'active' });
|
||||
|
||||
if (!tier) {
|
||||
// Free users with no user_tier row: look up free category badge
|
||||
const freeCategory = await mdl_TierCategories.findOne({
|
||||
where: { slug: 'free' },
|
||||
include: [{ model: Asset, as: 'badgeAsset', attributes: ['asset_id', 'file_url', 'display_name'], required: false }],
|
||||
});
|
||||
return R.success(res, 'Active tier retrieved.', { tier: 'free', status: 'active', category: freeCategory ?? null });
|
||||
}
|
||||
|
||||
// Supplement with the tier category badge even when the user's tier slug doesn't come via a plan
|
||||
// (e.g., manually granted tiers that only store a slug, not a plan_id)
|
||||
if (!tier.plan?.category) {
|
||||
const category = await mdl_TierCategories.findOne({
|
||||
where: { slug: tier.tier },
|
||||
include: [{ model: Asset, as: 'badgeAsset', attributes: ['asset_id', 'file_url', 'display_name'], required: false }],
|
||||
});
|
||||
const plain = tier.toJSON();
|
||||
plain.category = category?.toJSON() ?? null;
|
||||
return R.success(res, 'Active tier retrieved.', plain);
|
||||
}
|
||||
|
||||
return R.success(res, 'Active tier retrieved.', tier);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][GET MY TIER]', err);
|
||||
return R.error(res, 'Could not retrieve tier.', 500);
|
||||
@@ -282,25 +317,62 @@ exports.getMyPayments = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
// ─── SYSTEM BADGES (read-only for client profile) ────────────────────────────
|
||||
|
||||
exports.getCategories = async (req, res) => {
|
||||
try {
|
||||
const categories = await mdl_TierCategories.findAll({
|
||||
where: { is_active: true },
|
||||
attributes: ['tier_category_id', 'slug', 'name', 'rank', 'color', 'badge_label', 'is_default'],
|
||||
include: [{ model: Asset, as: 'badgeAsset', attributes: ['file_url', 'display_name'], required: false }],
|
||||
order: [['rank', 'ASC']],
|
||||
});
|
||||
return R.success(res, 'Tier categories retrieved.', categories);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][GET TIER CATEGORIES]', err);
|
||||
return R.error(res, 'Could not retrieve tier categories.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
exports.getSystemBadges = async (req, res) => {
|
||||
try {
|
||||
const badges = await mdl_SystemBadges.findAll({
|
||||
attributes: ['key', 'label', 'description', 'information', 'active_from', 'active_until'],
|
||||
include: [{ model: Asset, as: 'asset', attributes: ['file_url', 'display_name'], required: false }],
|
||||
order: [['key', 'ASC']],
|
||||
});
|
||||
return R.success(res, 'System badges retrieved.', badges);
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][GET SYSTEM BADGES]', err);
|
||||
return R.error(res, 'Could not retrieve system badges.', 500);
|
||||
}
|
||||
};
|
||||
|
||||
// — add refundOrder export ────────────────
|
||||
|
||||
const REFUND_WINDOW_MS = 5 * 60 * 1000; // 5 minutes from paid_at
|
||||
|
||||
exports.refundOrder = async (req, res) => {
|
||||
try {
|
||||
const user_id = req.user.user_id;
|
||||
|
||||
|
||||
// Get the active tier
|
||||
const activeTier = await mdl_UserTiers.findOne({
|
||||
where: { user_id, status: 'active' },
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
if (!activeTier) return R.error(res, 'No active tier to refund.', 404);
|
||||
|
||||
|
||||
// Get the completed payment for this tier
|
||||
const payment = await mdl_Payments.findOne({
|
||||
where: { user_id, tier_id: activeTier.tier_id, status: 'completed' },
|
||||
order: [['paid_at', 'DESC']],
|
||||
});
|
||||
if (!payment) return R.error(res, 'No completed payment found for this tier.', 404);
|
||||
|
||||
// Enforce 5-minute refund window
|
||||
if (!payment.paid_at || Date.now() - new Date(payment.paid_at).getTime() > REFUND_WINDOW_MS)
|
||||
return R.error(res, 'Refund window has expired. Refunds are only available within 5 minutes of payment.', 403);
|
||||
|
||||
// Get capture_id from provider_payload
|
||||
const captureId = payment.provider_payload?.capture_id;
|
||||
@@ -325,13 +397,28 @@ exports.refundOrder = async (req, res) => {
|
||||
},
|
||||
});
|
||||
|
||||
// Cancel tier at end of period — keep access until expires_at
|
||||
await activeTier.update({ status: 'revoked' });
|
||||
|
||||
return R.success(res, 'Refund processed successfully. Your access will remain until the end of the billing period.', {
|
||||
refund_id: refundData.id,
|
||||
status: refundData.status,
|
||||
expires_at: activeTier.expires_at,
|
||||
// Immediately terminate access — cut expires_at to now and revoke
|
||||
const now = new Date();
|
||||
await activeTier.update({
|
||||
status: 'revoked',
|
||||
expires_at: now,
|
||||
revoked_at: now,
|
||||
});
|
||||
|
||||
// Drop user back to free immediately
|
||||
await mdl_UserTiers.create({
|
||||
user_id,
|
||||
tier: 'free',
|
||||
status: 'active',
|
||||
starts_at: now,
|
||||
expires_at: null,
|
||||
granted_by: null,
|
||||
notes: 'Auto-downgrade after refund.',
|
||||
});
|
||||
|
||||
return R.success(res, 'Refund processed successfully. Your access has been revoked.', {
|
||||
refund_id: refundData.id,
|
||||
status: refundData.status,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[CLIENT][REFUND]', err);
|
||||
|
||||
Reference in New Issue
Block a user