assets and tier plans revamp

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-08-01 17:44:25 +08:00
parent 39c3c4566b
commit cae958b5d5
41 changed files with 1271 additions and 165 deletions
+50 -47
View File
@@ -184,12 +184,13 @@ exports.createOrder = async (req, res) => {
const plan = await mdl_TierPlans.findOne({ where: { plan_id, is_active: true } });
if (!plan) return R.error(res, 'Plan not found or inactive.', 404);
// Repurchasing a plan under a tier already held active is allowed — it
// extends the existing grant's expires_at (see captureOrder) rather than
// being blocked. Surfaced here only for checkout-page messaging.
const existingActive = await mdl_UserTiers.findOne({
where: { user_id: req.user.user_id, tier: plan.tier, status: 'active' },
attributes: ['expires_at'],
});
if (existingActive) {
return R.error(res, `You already have an active ${plan.tier} subscription until ${existingActive.expires_at}. You can repurchase once it expires.`, 409);
}
const effectivePrice = Number(plan.price);
const effectiveCurrency = plan.currency;
@@ -242,13 +243,15 @@ exports.createOrder = async (req, res) => {
});
return R.success(res, 'Order created.', {
payment_id: payment.payment_id,
order_id: ppOrder.id,
approval_url: approvalUrl,
amount: total,
currency: effectiveCurrency,
promo_code: promoResult.code,
discount: discount.toFixed(2),
payment_id: payment.payment_id,
order_id: ppOrder.id,
approval_url: approvalUrl,
amount: total,
currency: effectiveCurrency,
promo_code: promoResult.code,
discount: discount.toFixed(2),
extends_existing: !!existingActive,
current_expires_at: existingActive?.expires_at ?? null,
}, 201);
} catch (err) {
console.error('[CLIENT][CREATE ORDER]', err);
@@ -297,47 +300,44 @@ exports.captureOrder = async (req, res) => {
const capture = captureData.purchase_units?.[0]?.payments?.captures?.[0];
// Scoped, defensive re-check: createOrder already blocked this, but time may
// have passed (or two checkout tabs raced) between order creation and capture.
// Money has already moved via PayPal at this point, so auto-refund rather than
// leaving the user charged with nothing to show for it.
// Repurchasing a plan under a tier already held active extends the
// existing grant's expires_at by the new plan's duration, rather than
// being blocked/refunded — the original plan_id is kept (whichever plan
// first granted this tier keeps governing its bundle/access_rules; a
// sibling-plan repurchase only adds time). This also keeps the
// one-active-row-per-(user,tier) DB invariant intact, since no second
// row is ever created.
const existingActive = await mdl_UserTiers.findOne({
where: { user_id: req.user.user_id, tier: payment.plan.tier, status: 'active' },
});
let resultTier;
let successMessage;
if (existingActive) {
try {
await paymentSvc.refundCapture(payment.provider, capture?.id, payment.amount, payment.currency);
await payment.update({
status: 'refunded',
provider_payload: { ...payment.provider_payload, capture_id: capture?.id, capture: captureData, error: 'duplicate_active_tier_auto_refunded' },
});
return R.error(res, `You already have an active ${payment.plan.tier} subscription. Your payment has been automatically refunded.`, 409);
} catch (refundErr) {
console.error('[CLIENT][CAPTURE ORDER] auto-refund failed for duplicate active tier:', refundErr?.response?.data ?? refundErr);
await payment.update({
status: 'failed',
provider_payload: { ...payment.provider_payload, capture_id: capture?.id, capture: captureData, error: 'duplicate_active_tier_refund_failed' },
});
return R.error(res, `You already have an active ${payment.plan.tier} subscription. Refund could not be processed automatically — please contact support.`, 409);
}
const newExpiresAt = new Date(existingActive.expires_at.getTime() + payment.plan.duration_days * 86400000);
await existingActive.update({ expires_at: newExpiresAt });
resultTier = existingActive;
successMessage = `Payment successful. Your ${payment.plan.tier} access has been extended to ${newExpiresAt.toLocaleDateString()}.`;
} else {
const startsAt = new Date();
const expiresAt = new Date(startsAt.getTime() + payment.plan.duration_days * 86400000);
resultTier = await mdl_UserTiers.create({
user_id: req.user.user_id,
tier: payment.plan.tier,
plan_id: payment.plan_id,
status: 'active',
starts_at: startsAt,
expires_at: expiresAt,
granted_by: null,
});
successMessage = 'Payment successful. Tier activated.';
}
const startsAt = new Date();
const expiresAt = new Date(startsAt.getTime() + payment.plan.duration_days * 86400000);
const newTier = await mdl_UserTiers.create({
user_id: req.user.user_id,
tier: payment.plan.tier,
plan_id: payment.plan_id,
status: 'active',
starts_at: startsAt,
expires_at: expiresAt,
granted_by: null,
});
await payment.update({
status: 'completed',
tier_id: newTier.tier_id,
tier_id: resultTier.tier_id,
paid_at: new Date(),
provider_payload: {
...payment.provider_payload,
@@ -347,11 +347,14 @@ exports.captureOrder = async (req, res) => {
},
});
await onTierActivated(req.user.user_id, newTier.tier);
// Idempotent (grantAchievement checks for an existing row first) — safe
// to call again on an extension, won't grant a duplicate achievement.
await onTierActivated(req.user.user_id, resultTier.tier);
return R.success(res, 'Payment successful. Tier activated.', {
tier: newTier.tier,
expires_at: newTier.expires_at,
return R.success(res, successMessage, {
tier: resultTier.tier,
expires_at: resultTier.expires_at,
extended: !!existingActive,
});
} catch (err) {
console.error('[CLIENT][CAPTURE ORDER]', err);