From 0013c22523c4e1ae08689cdfcd90a330f9948785 Mon Sep 17 00:00:00 2001 From: Kenneth Obsequio Date: Sat, 15 Aug 2026 11:09:09 +0800 Subject: [PATCH] forgot useMemo usage for AssetPickerSheet Signed-off-by: Kenneth Obsequio --- controllers/client/courses.controller.js | 55 ++++++++++++++++++- .../completion_requirements.registry.js | 9 +++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/controllers/client/courses.controller.js b/controllers/client/courses.controller.js index ddb5be4..7c44645 100644 --- a/controllers/client/courses.controller.js +++ b/controllers/client/courses.controller.js @@ -313,7 +313,46 @@ exports.getCourse = async (req, res) => { // Access check — plan association → subscription field → individual purchase if (!await canAccessCourse(req.user.user_id, courseId)) { - return R.error(res, "You do not have access to this course.", 403); + // Still send the course's own basic info — including its unit/lesson + // structure (titles + durations, no lesson content/blocks/quiz answers) + // so the client can render the "Course content" outline as a preview, + // with an upsell prompt on every actionable button. + const blockedCourse = await Course.findOne({ + where: { course_id: courseId, ...notDeleted }, + attributes: COURSE_LIST_ATTRS, + include: [ + { + model: Unit, as: "units", + where: notDeleted, required: false, + attributes: ["unit_id", "uuid", "title", "description", "duration_seconds"], + through: { attributes: ["order_index"] }, + include: [ + { + model: Lesson, as: "lessons", + where: notDeleted, required: false, + attributes: ["lesson_id", "uuid", "title", "description", "duration_seconds"], + through: { attributes: ["order_index"] }, + }, + { + model: UnitQuiz, as: "quiz", + required: false, + attributes: ["quiz_id", "uuid", "title"], + }, + ], + }, + ], + }); + const product = blockedCourse && await mdl_Product.findOne({ + where: { purchasable_type: 'course', purchasable_id: courseId, is_active: true }, + attributes: ['id', 'name', 'price', 'currency', 'access_days'], + }); + const blockedPlain = blockedCourse ? blockedCourse.toJSON() : null; + if (blockedPlain) blockedPlain.units = flattenUnits(blockedPlain.units); + return res.status(403).json({ + status: "error", + message: "You do not have access to this course.", + item: blockedPlain ? { ...blockedPlain, product: product ?? null } : null, + }); } const course = await Course.findOne({ @@ -1311,7 +1350,19 @@ exports.getLessonsByUnitUuid = async (req, res) => { status: "error", message: "You do not have access to this unit.", course: first ? { title: first.title, subscription: first.subscription } : null, - item: { uuid: unit.uuid, subscription: unit.subscription }, + item: { + uuid: unit.uuid, subscription: unit.subscription, + title: unit.title, description: unit.description, + duration_seconds: unit.duration_seconds, + // Structure only (titles/durations) — no page.blocks/objectives content, + // no quiz questions — so the client can render the lessons/quiz outline + // as a preview without leaking any actual gated content. + lessons: (unit.lessons ?? []).map((l) => ({ + lesson_id: l.lesson_id, uuid: l.uuid, + title: l.title, duration_seconds: l.duration_seconds, + })), + quiz: unit.quiz ? { quiz_id: unit.quiz.quiz_id, uuid: unit.quiz.uuid, title: unit.quiz.title } : null, + }, }); } diff --git a/utils/courses/completion_requirements.registry.js b/utils/courses/completion_requirements.registry.js index b8cf32e..1db0649 100644 --- a/utils/courses/completion_requirements.registry.js +++ b/utils/courses/completion_requirements.registry.js @@ -195,6 +195,15 @@ const DEFAULT_HANDLERS = { */ async function evaluateEntity({ entityType, entityId, userId, courseId = null, transaction = null }) { const t = transaction; + + // Units always complete by passing their own quiz — overrides whatever + // completion_requirement type (if any) is configured on the unit. A unit + // with no quiz attached never completes (isUnitQuizPassed returns false). + if (entityType === 'unit') { + const completed = await isUnitQuizPassed({ unitId: entityId, userId, t }); + return { status: completed ? 'completed' : 'in_progress', evaluated_via: 'quiz_required', satisfied: [] }; + } + const rows = await CompletionRequirement.findAll({ where: { entity_type: entityType, entity_id: entityId }, order: [['order', 'ASC']],