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
@@ -3,8 +3,12 @@ const mdl_CoursePurchase = require('../../models/courses/course_purchases.mdl');
const mdl_Product = require('../../models/courses/products.mdl');
const paymentSvc = require('../../services/payment.service');
const R = require('../../utils/response.util');
const { resolvePurchasable, checkoutPath } = require('../../utils/purchasable.util');
// ─── CREATE ORDER ─────────────────────────────────────────────────────────────
// Despite the "course" naming (historical — this predates Units/Lessons being
// individually purchasable), this endpoint and table are generic: a Product's
// purchasable_type/purchasable_id drives everything below.
exports.createCourseOrder = async (req, res) => {
try {
@@ -19,15 +23,19 @@ exports.createCourseOrder = async (req, res) => {
});
if (existing) {
const stillActive = !existing.expires_at || new Date(existing.expires_at) > new Date();
if (stillActive) return R.error(res, 'You already have active access to this course.', 409);
if (stillActive) return R.error(res, `You already have active access to this ${product.purchasable_type}.`, 409);
}
const target = await resolvePurchasable(product.purchasable_type, product.purchasable_id);
if (!target) return R.error(res, 'Purchasable content not found.', 404);
const path = checkoutPath(product.purchasable_type, target);
const ppOrder = await paymentSvc.createOrder('paypal', {
amount: Number(product.price).toFixed(2),
currency: product.currency,
referenceId: `user_${req.user.user_id}_product_${product_id}`,
returnUrl: `${process.env.FRONTEND_URL}/course/${product.course_id}/checkout`,
cancelUrl: `${process.env.FRONTEND_URL}/course/${product.course_id}/checkout?cancelled=true`,
returnUrl: `${process.env.FRONTEND_URL}${path}`,
cancelUrl: `${process.env.FRONTEND_URL}${path}?cancelled=true`,
});
const approvalUrl = ppOrder.links?.find((l) => l.rel === 'approve')?.href ?? null;
@@ -100,10 +108,14 @@ exports.captureCourseOrder = async (req, res) => {
},
});
return R.success(res, 'Payment successful. Course access granted.', {
purchase_id: purchase.id,
expires_at: purchase.expires_at,
course_id: purchase.product.course_id,
const target = await resolvePurchasable(purchase.product.purchasable_type, purchase.product.purchasable_id);
return R.success(res, 'Payment successful. Access granted.', {
purchase_id: purchase.id,
expires_at: purchase.expires_at,
purchasable_type: purchase.product.purchasable_type,
purchasable_id: purchase.product.purchasable_id,
target_uuid: target?.uuid ?? null,
});
} catch (err) {
console.error('[CLIENT][COURSE PURCHASE][CAPTURE]', err);
@@ -144,7 +156,7 @@ exports.getMyPurchases = async (req, res) => {
try {
const purchases = await mdl_CoursePurchase.findAll({
where: { user_id: req.user.user_id },
include: [{ model: mdl_Product, as: 'product', attributes: ['id', 'name', 'course_id', 'access_days'] }],
include: [{ model: mdl_Product, as: 'product', attributes: ['id', 'name', 'purchasable_type', 'purchasable_id', 'access_days'] }],
attributes: { exclude: ['provider_payload'] },
order: [['createdAt', 'DESC']],
});