pushy

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-06-28 11:29:07 +08:00
parent 463a8d3978
commit 89acdfc239
67 changed files with 2736 additions and 306 deletions
+101 -14
View File
@@ -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);