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
+57 -7
View File
@@ -30,9 +30,12 @@
***********************************************************************************************************************************************************************/
"use strict";
const { Op } = require("sequelize");
const R = require("../../utils/response.util");
const logActivity = require("../../utils/logActivity.util");
const sequelize = require("../../config/db.config");
const mdl_Product = require("../../models/courses/products.mdl");
const mdl_CoursePurchase = require("../../models/courses/course_purchases.mdl");
const {
Unit, Lesson,
@@ -61,6 +64,35 @@ function sanitizeQuestions(questions = []) {
});
}
// Batch-fetch active product listings + this user's completed/unexpired
// purchases for a set of standalone targets (unit or lesson), same shape as
// the courses.controller.js equivalent — used by getUnits/getLessons below so
// the browse-list Buy button has price data without an N+1 query per row.
async function attachProducts(user_id, purchasable_type, ids) {
if (!ids.length) return { productById: new Map(), purchasedIds: new Set() };
const products = await mdl_Product.findAll({
where: { purchasable_type, purchasable_id: { [Op.in]: ids }, is_active: true },
attributes: ["id", "name", "price", "currency", "access_days", "is_active", "purchasable_id"],
});
const productById = new Map(products.map((p) => [String(p.purchasable_id), p]));
const productIds = products.map((p) => p.id);
const purchases = productIds.length ? await mdl_CoursePurchase.findAll({
where: {
user_id, product_id: { [Op.in]: productIds }, status: "completed",
[Op.or]: [{ expires_at: null }, { expires_at: { [Op.gt]: new Date() } }],
},
attributes: ["product_id"],
}) : [];
const purchasedProductIds = new Set(purchases.map((p) => String(p.product_id)));
const purchasedIds = new Set(
products.filter((p) => purchasedProductIds.has(String(p.id))).map((p) => String(p.purchasable_id))
);
return { productById, purchasedIds };
}
// ─── UNIT LIBRARY (learner view) ──────────────────────────────────────────────
// Client-side Units/Lessons browsing shows ALL content, bound to a course or
@@ -104,6 +136,8 @@ exports.getUnits = async (req, res) => {
coursesByUnit.set(row.unit_id, list);
}
const { productById, purchasedIds } = await attachProducts(req.user.user_id, "unit", unitIds);
// is_locked mirrors canAccessUnit: a unit with its own subscription or at
// least one attached course needs an access check; a fully open standalone
// unit (no subscription, no course links) is never locked.
@@ -112,7 +146,13 @@ exports.getUnits = async (req, res) => {
const is_locked = (row.subscription || Number(row.course_count) > 0)
? !(await coursesCtrl.canAccessUnit(req.user.user_id, row.unit_id))
: false;
result.push({ ...row, courses: coursesByUnit.get(row.unit_id) ?? [], is_locked });
result.push({
...row,
courses: coursesByUnit.get(row.unit_id) ?? [],
is_locked,
product: productById.get(String(row.unit_id)) ?? null,
has_purchased: purchasedIds.has(String(row.unit_id)),
});
}
return R.success(res, "Units retrieved.", result);
@@ -131,7 +171,7 @@ exports.getLessons = async (req, res) => {
try {
const rows = await sequelize.query(`
SELECT
l.lesson_id, l.uuid, l.title, l.description, l.duration_seconds,
l.lesson_id, l.uuid, l.title, l.subscription, l.description, l.duration_seconds,
(SELECT CAST(COUNT(*) AS INTEGER) FROM unit_lessons ul
JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL
WHERE ul.lesson_id = l.lesson_id) AS unit_count
@@ -158,15 +198,23 @@ exports.getLessons = async (req, res) => {
coursesByLesson.set(row.lesson_id, list);
}
// is_locked mirrors canAccessLesson: standalone/unattached lessons are
// open, attached lessons need at least one accessible course through
// any attached unit.
const { productById, purchasedIds } = await attachProducts(req.user.user_id, "lesson", lessonIds);
// is_locked mirrors canAccessLesson: a lesson with its own subscription or
// at least one attached unit needs an access check; a fully open
// standalone lesson (no subscription, no unit links) is never locked.
const result = [];
for (const row of rows) {
const is_locked = Number(row.unit_count) > 0
const is_locked = (row.subscription || Number(row.unit_count) > 0)
? !(await coursesCtrl.canAccessLesson(req.user.user_id, row.lesson_id))
: false;
result.push({ ...row, courses: coursesByLesson.get(row.lesson_id) ?? [], is_locked });
result.push({
...row,
courses: coursesByLesson.get(row.lesson_id) ?? [],
is_locked,
product: productById.get(String(row.lesson_id)) ?? null,
has_purchased: purchasedIds.has(String(row.lesson_id)),
});
}
return R.success(res, "Lessons retrieved.", result);
@@ -481,3 +529,5 @@ exports.markStandaloneLessonComplete = async (req, res) => {
exports.getUnitByUuid = coursesCtrl.getUnitByUuid;
exports.getLessonsByUnitUuid = coursesCtrl.getLessonsByUnitUuid;
exports.getLessonByUuid = coursesCtrl.getLessonByUuid;
exports.getUnitCheckoutInfo = coursesCtrl.getUnitCheckoutInfo;
exports.getLessonCheckoutInfo = coursesCtrl.getLessonCheckoutInfo;