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
+15
View File
@@ -8,6 +8,8 @@ const s3 = require("../../services/s3.service");
const mediaToken = require("../../services/mediaToken.service");
const uploadProgress = require("../../services/uploadProgress.service");
const { extractVideoMeta } = require("../../services/ffprobe.service");
const ffmpegSvc = require("../../services/ffmpeg.service");
const assetTranscode = require("../../services/assetTranscode.service");
const documentConversion = require("../../services/documentConversion.service");
const R = require('../../utils/response.util');
const { paginate } = require("../../utils/paginate.util");
@@ -408,6 +410,11 @@ async function finalizeAssetFromStorage({ storage_key, thumbnail_storage_key, or
// ── DB insert ──────────────────────────────────────────────────────────────
// .mov/.mkv videos load slowly in-browser (moov/Cues index at the end of
// the file) — flag them for the background remux job (see
// assetTranscode.service.js) fired below, right after commit.
const needsTranscode = storage_provider === "s3" && file_type === "video" && ffmpegSvc.needsRemux(extension);
const t = await sequelize.transaction();
try {
const asset = await Asset.create({
@@ -434,10 +441,18 @@ async function finalizeAssetFromStorage({ storage_key, thumbnail_storage_key, or
storage_key,
is_public,
createdBy,
transcode_status: needsTranscode ? "pending" : "none",
}, { transaction: t });
await t.commit();
logActivity(user?.user_id, 'upload_asset', { entityType: 'asset', entityId: asset.asset_id, details: { display_name: asset.display_name, file_type: asset.file_type } });
if (needsTranscode) {
assetTranscode.transcodeAsset(asset).catch((err) => {
console.error("[ASSET][TRANSCODE] Background remux failed to start:", err.message);
});
}
return asset;
} catch (dbErr) {
@@ -20,6 +20,7 @@ const CourseReadingProgress = require('../../models/courses/course_reading_progr
const { Course, Unit, Lesson, CourseUnit } = require('../../models/courses/courses.associations');
const mdl_Users = require('../../models/users/users.mdl');
const { countCourseLessons, getCourseUnitIds, flattenUnits } = require('../../utils/courses/hierarchy.util');
const { resolveAvatarUrl } = require('../../utils/resolveAvatar.util');
const notDeleted = { deletedAt: null };
@@ -91,21 +92,22 @@ exports.getCourseReadingProgress = async (req, res) => {
if (row.type === 'lesson' && row.status === 'completed') entry.lessons_completed++;
}
const result = Object.values(summaryMap).map((entry) => {
const result = await Promise.all(Object.values(summaryMap).map(async (entry) => {
const u = userMap[entry.user_id];
const avatar = await resolveAvatarUrl(u?.personal_info?.avatar);
return {
...entry,
user: {
email: u?.email ?? null,
full_name: u?.personal_info?.name?.full_name ?? null,
avatar_url: u?.personal_info?.avatar?.url ?? null,
avatar_url: avatar?.url ?? null,
},
units_total,
lessons_total,
// Fall back to in_progress if the course row hasn't been written yet
course_status: entry.course_status ?? 'in_progress',
};
});
}));
// Sort: completed last, most recent first within each group
result.sort((a, b) => {
+11 -9
View File
@@ -5,6 +5,7 @@ const sequelize = require("../../config/db.config");
const R = require("../../utils/response.util");
const { paginate } = require("../../utils/paginate.util");
const { recomputeDurations, recomputeUnitDuration, recomputeCourseDuration, formatDuration } = require("../../utils/duration.util");
const { resolveAvatarUrl } = require("../../utils/resolveAvatar.util");
const { syncObjectivesCreate, syncObjectivesUpdate } = require("../../utils/courses/objectives.util");
const { resolvePrerequisiteTitles } = require("../../utils/courses/resolvePrerequisiteTitles.util");
const { syncJunction } = require("../../utils/courses/junction.util");
@@ -2653,22 +2654,23 @@ exports.syncInstructors = async (req, res) => {
// ─── COMPLETIONS HELPERS ──────────────────────────────────────────────────────
function extractUserInfo(user) {
async function extractUserInfo(user) {
if (!user) return { full_name: null, email: null, avatar_url: null, deleted: false };
const avatar = await resolveAvatarUrl(user.personal_info?.avatar);
return {
full_name: user.personal_info?.name?.full_name ?? null,
email: user.email ?? null,
avatar_url: user.personal_info?.avatar?.url ?? null,
avatar_url: avatar?.url ?? null,
deleted: !!user.deletedAt,
};
}
function groupByUser(attempts) {
async function groupByUser(attempts) {
const map = new Map();
for (const a of attempts) {
const uid = String(a.user_id);
if (!map.has(uid)) {
const { full_name, email, avatar_url, deleted } = extractUserInfo(a.user);
const { full_name, email, avatar_url, deleted } = await extractUserInfo(a.user);
map.set(uid, {
user_id: a.user_id,
full_name,
@@ -2749,7 +2751,7 @@ exports.getQuizCompletions = async (req, res) => {
const plain = attempts.map((a) => a.toJSON());
return R.success(res, "Quiz completions retrieved.", {
summary: buildSummary(plain),
completions: groupByUser(plain),
completions: await groupByUser(plain),
});
} catch (err) {
console.error("[ADMIN][QUIZ][COMPLETIONS]", err);
@@ -2773,7 +2775,7 @@ exports.getAssessmentCompletions = async (req, res) => {
const plain = attempts.map((a) => a.toJSON());
return R.success(res, "Assessment completions retrieved.", {
summary: buildSummary(plain),
completions: groupByUser(plain),
completions: await groupByUser(plain),
});
} catch (err) {
console.error("[ADMIN][ASSESSMENT][COMPLETIONS]", err);
@@ -2794,9 +2796,9 @@ exports.getAssessmentSessions = async (req, res) => {
order: [["createdAt", "DESC"]],
});
const rows = sessions.map((s) => {
const rows = await Promise.all(sessions.map(async (s) => {
const j = s.toJSON();
const { full_name, email, avatar_url, deleted } = extractUserInfo(j.user);
const { full_name, email, avatar_url, deleted } = await extractUserInfo(j.user);
const time_spent_seconds = j.status !== 'in_progress' && j.started_at
? Math.round((new Date(j.updatedAt) - new Date(j.started_at)) / 1000)
: null;
@@ -2813,7 +2815,7 @@ exports.getAssessmentSessions = async (req, res) => {
time_spent_seconds,
attempt_id: j.attempt_id,
};
});
}));
const summary = {
total_sessions: rows.length,
+47 -4
View File
@@ -37,6 +37,7 @@ const {
} = require("../../models/courses/courses.associations");
const mdl_Users = require("../../models/users/users.mdl");
const { mdl_PlanLessons, mdl_TierPlans } = require("../../models/tiers/tier.associations");
const notDeleted = { deletedAt: null };
const onlyDeleted = { deletedAt: { [Op.not]: null } };
@@ -155,6 +156,46 @@ exports.getLessonsFlat = async (req, res) => {
}
};
// Bundle picker (Tier Plan wizard) — mirrors getCoursesBySubscription in
// controllers/admin/courses.controller.js.
exports.getLessonsBySubscription = async (req, res) => {
try {
const { slug } = req.query;
if (!slug) return R.error(res, 'slug query param is required.', 400);
const rows = await Lesson.findAll({
where: { ...notDeleted, subscription: slug },
attributes: ['lesson_id', 'title', 'description', 'subscription'],
include: [{
model: mdl_PlanLessons,
as: 'planLesson',
required: false,
attributes: ['plan_id'],
include: [{
model: mdl_TierPlans,
as: 'plan',
attributes: ['plan_id', 'label'],
}],
}],
order: [['title', 'ASC']],
});
// Flatten so the frontend can just check `assigned_plan` — a lesson
// belongs to at most one plan (UNIQUE constraint on plan_lessons.lesson_id).
const data = rows.map((l) => {
const plain = l.toJSON();
const assigned_plan = plain.planLesson?.plan ?? null;
delete plain.planLesson;
return { ...plain, assigned_plan };
});
return R.success(res, 'Lessons retrieved.', data);
} catch (err) {
console.error('[LESSON LIB][BY SUBSCRIPTION]', err);
return R.error(res, 'Could not retrieve lessons.', 500);
}
};
exports.getLesson = async (req, res) => {
try {
const { lessonId } = req.params;
@@ -179,11 +220,12 @@ exports.getLesson = async (req, res) => {
exports.createLesson = async (req, res) => {
const t = await sequelize.transaction();
try {
const { title, description, unit_id, order, objectives = [], createdBy } = req.body;
const { title, description, subscription, unit_id, order, objectives = [], createdBy } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
const lesson = await Lesson.create({
title,
subscription: subscription || null,
description: description ?? null,
duration_seconds: 0,
createdBy: createdBy ?? req.user?.user_id ?? null,
@@ -231,10 +273,11 @@ exports.updateLesson = async (req, res) => {
const lesson = await Lesson.findOne({ where: { lesson_id: lessonId, ...notDeleted } });
if (!lesson) return R.error(res, "Lesson not found.", 404);
const { title, description, objectives, updatedBy } = req.body;
const { title, description, subscription, objectives, updatedBy } = req.body;
if (title !== undefined) lesson.title = title;
if (description !== undefined) lesson.description = description;
if (title !== undefined) lesson.title = title;
if (description !== undefined) lesson.description = description;
if (subscription !== undefined) lesson.subscription = subscription || null;
lesson.updatedBy = updatedBy ?? req.user?.user_id ?? null;
await lesson.save({ transaction: t });
+82 -42
View File
@@ -5,54 +5,94 @@ const { Course, CourseProductCategory: mdl_CourseProductCategory } = require('..
const R = require('../../utils/response.util');
const logActivity = require('../../utils/logActivity.util');
// ─── PRODUCT (per course) ─────────────────────────────────────────────────────
// ─── PRODUCT (generic, keyed by purchasable_type + purchasable_id) ───────────
// Course/Unit/Lesson each get their own thin route + exported handler below,
// all delegating to these so the CRUD logic isn't tripled across the three
// content types — see routes/admin/products.routes.js.
exports.getCourseProduct = async (req, res) => {
try {
const product = await mdl_Product.findOne({ where: { course_id: req.params.courseId }, paranoid: false });
return R.success(res, 'Product retrieved.', product ?? null);
} catch (err) {
console.error('[ADMIN][PRODUCTS][GET]', err);
return R.error(res, 'Could not retrieve product.', 500);
async function getProductFor(purchasable_type, purchasable_id) {
return mdl_Product.findOne({ where: { purchasable_type, purchasable_id }, paranoid: false });
}
async function upsertProductFor(purchasable_type, purchasable_id, body, adminUserId) {
const { name, description, price, currency, access_days, is_active } = body;
if (!name || price == null) {
const err = new Error('name and price are required.');
err.status = 400;
throw err;
}
};
exports.upsertCourseProduct = async (req, res) => {
try {
const { courseId } = req.params;
const { name, description, price, currency, access_days, is_active } = req.body;
if (!name || price == null) return R.error(res, 'name and price are required.', 400);
const existing = await mdl_Product.findOne({ where: { purchasable_type, purchasable_id }, paranoid: false });
const existing = await mdl_Product.findOne({ where: { course_id: courseId }, paranoid: false });
if (existing) {
if (existing.deletedAt) await existing.restore();
await existing.update({ name, description: description ?? null, price, currency: currency ?? 'USD', access_days: access_days ?? null, is_active: is_active ?? true });
logActivity(req.user?.user_id, 'upsert_course_product', { entityType: 'product', details: { course_id: courseId, name } });
return R.success(res, 'Product updated.', existing);
}
const product = await mdl_Product.create({ course_id: courseId, name, description: description ?? null, price, currency: currency ?? 'USD', access_days: access_days ?? null, is_active: is_active ?? true });
logActivity(req.user?.user_id, 'upsert_course_product', { entityType: 'product', entityId: product.product_id, details: { course_id: courseId, name } });
return R.success(res, 'Product created.', product, 201);
} catch (err) {
console.error('[ADMIN][PRODUCTS][UPSERT]', err);
return R.error(res, 'Could not save product.', 500);
if (existing) {
if (existing.deletedAt) await existing.restore();
await existing.update({ name, description: description ?? null, price, currency: currency ?? 'USD', access_days: access_days ?? null, is_active: is_active ?? true });
logActivity(adminUserId, `upsert_${purchasable_type}_product`, { entityType: 'product', entityId: existing.id, details: { purchasable_type, purchasable_id, name } });
return { product: existing, created: false };
}
};
exports.removeCourseProduct = async (req, res) => {
try {
const product = await mdl_Product.findOne({ where: { course_id: req.params.courseId } });
if (!product) return R.error(res, 'Product not found.', 404);
await product.destroy();
logActivity(req.user?.user_id, 'remove_course_product', { entityType: 'product', details: { course_id: req.params.courseId } });
return R.success(res, 'Product removed.');
} catch (err) {
console.error('[ADMIN][PRODUCTS][REMOVE]', err);
return R.error(res, 'Could not remove product.', 500);
}
};
const product = await mdl_Product.create({ purchasable_type, purchasable_id, name, description: description ?? null, price, currency: currency ?? 'USD', access_days: access_days ?? null, is_active: is_active ?? true });
logActivity(adminUserId, `upsert_${purchasable_type}_product`, { entityType: 'product', entityId: product.id, details: { purchasable_type, purchasable_id, name } });
return { product, created: true };
}
async function removeProductFor(purchasable_type, purchasable_id, adminUserId) {
const product = await mdl_Product.findOne({ where: { purchasable_type, purchasable_id } });
if (!product) return false;
await product.destroy();
logActivity(adminUserId, `remove_${purchasable_type}_product`, { entityType: 'product', details: { purchasable_type, purchasable_id } });
return true;
}
function makeProductHandlers(purchasable_type, paramName) {
return {
get: async (req, res) => {
try {
const product = await getProductFor(purchasable_type, req.params[paramName]);
return R.success(res, 'Product retrieved.', product ?? null);
} catch (err) {
console.error(`[ADMIN][PRODUCTS][GET][${purchasable_type}]`, err);
return R.error(res, 'Could not retrieve product.', 500);
}
},
upsert: async (req, res) => {
try {
const { product, created } = await upsertProductFor(purchasable_type, req.params[paramName], req.body, req.user?.user_id);
return R.success(res, created ? 'Product created.' : 'Product updated.', product, created ? 201 : 200);
} catch (err) {
if (err.status === 400) return R.error(res, err.message, 400);
console.error(`[ADMIN][PRODUCTS][UPSERT][${purchasable_type}]`, err);
return R.error(res, 'Could not save product.', 500);
}
},
remove: async (req, res) => {
try {
const removed = await removeProductFor(purchasable_type, req.params[paramName], req.user?.user_id);
if (!removed) return R.error(res, 'Product not found.', 404);
return R.success(res, 'Product removed.');
} catch (err) {
console.error(`[ADMIN][PRODUCTS][REMOVE][${purchasable_type}]`, err);
return R.error(res, 'Could not remove product.', 500);
}
},
};
}
const courseProductHandlers = makeProductHandlers('course', 'courseId');
const unitProductHandlers = makeProductHandlers('unit', 'unitId');
const lessonProductHandlers = makeProductHandlers('lesson', 'lessonId');
exports.getCourseProduct = courseProductHandlers.get;
exports.upsertCourseProduct = courseProductHandlers.upsert;
exports.removeCourseProduct = courseProductHandlers.remove;
exports.getUnitProduct = unitProductHandlers.get;
exports.upsertUnitProduct = unitProductHandlers.upsert;
exports.removeUnitProduct = unitProductHandlers.remove;
exports.getLessonProduct = lessonProductHandlers.get;
exports.upsertLessonProduct = lessonProductHandlers.upsert;
exports.removeLessonProduct = lessonProductHandlers.remove;
// ─── CATEGORIES (per course) ──────────────────────────────────────────────────
+4 -3
View File
@@ -18,6 +18,7 @@
const mdl_Users = require('../../models/users/users.mdl');
const R = require('../../utils/response.util');
const { uploadFile, deleteFile } = require('../../services/s3.service');
const { resolveUserAvatar } = require('../../utils/resolveAvatar.util');
// ─── GET own profile ───────────────────────────────────────────────────────────
@@ -26,7 +27,7 @@ exports.getProfile = async (req, res) => {
const user = await mdl_Users.findByPk(req.user.user_id, {
attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] },
});
return R.success(res, 'Profile retrieved.', user);
return R.success(res, 'Profile retrieved.', await resolveUserAvatar(user));
} catch (err) {
return R.error(res, 'Could not retrieve profile.', 500);
}
@@ -53,7 +54,7 @@ exports.updateProfile = async (req, res) => {
const updated = await mdl_Users.findByPk(req.user.user_id, {
attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] },
});
return R.success(res, 'Profile updated.', updated);
return R.success(res, 'Profile updated.', await resolveUserAvatar(updated));
} catch (err) {
console.error('[ADMIN] updateProfile error:', err);
return R.error(res, 'Profile update failed.', 500);
@@ -95,7 +96,7 @@ exports.uploadAvatar = async (req, res) => {
const updated = await mdl_Users.findByPk(req.user.user_id, {
attributes: { exclude: ['password', 'otp_code', 'otp_expires_at'] },
});
return R.success(res, 'Avatar updated.', updated);
return R.success(res, 'Avatar updated.', await resolveUserAvatar(updated));
} catch (err) {
console.error('[ADMIN] uploadAvatar error:', err);
return R.error(res, 'Avatar upload failed.', 500);
+108
View File
@@ -16,7 +16,11 @@ const mdl_UserTiers = require('../../models/tiers/user_tiers.mdl');
const mdl_Payments = require('../../models/tiers/payments.mdl');
const mdl_Users = require('../../models/users/users.mdl');
const mdl_PlanCourses = require('../../models/tiers/plan_courses.mdl');
const mdl_PlanUnits = require('../../models/tiers/plan_units.mdl');
const mdl_PlanLessons = require('../../models/tiers/plan_lessons.mdl');
const { Course } = require('../../models/courses/courses.mdl');
const Unit = require('../../models/courses/units.mdl');
const Lesson = require('../../models/courses/lessons.mdl');
require('../../models/tiers/tier.associations');
@@ -546,3 +550,107 @@ exports.syncPlanCourses = async (req, res) => {
return R.error(res, 'Could not update plan courses.', 500);
}
};
// ─── PLAN UNITS ───────────────────────────────────────────────────────────────
// Mirrors PLAN COURSES above — bundling/display only, not an access-control
// mechanism (see canAccessUnit in controllers/client/courses.controller.js).
exports.getPlanUnits = async (req, res) => {
try {
const entries = await mdl_PlanUnits.findAll({
where: { plan_id: req.params.id },
include: [{
model: Unit,
as: 'unit',
attributes: ['unit_id', 'title', 'subscription'],
}],
order: [['createdAt', 'ASC']],
});
return R.success(res, 'Plan units retrieved.', entries.map(e => e.unit));
} catch (err) {
console.error('[ADMIN][GET PLAN UNITS]', err);
return R.error(res, 'Could not retrieve plan units.', 500);
}
};
exports.syncPlanUnits = async (req, res) => {
try {
const { id } = req.params;
const { unit_ids = [] } = req.body;
if (!Array.isArray(unit_ids))
return R.error(res, 'unit_ids must be an array.', 400);
const plan = await mdl_TierPlans.findByPk(id);
if (!plan) return R.error(res, 'Plan not found.', 404);
await mdl_PlanUnits.destroy({ where: { plan_id: id } });
if (unit_ids.length) {
// unit_id has a UNIQUE constraint — clear any orphaned/other-plan assignments
// for these units before inserting, so the insert isn't silently skipped
await mdl_PlanUnits.destroy({ where: { unit_id: unit_ids } });
await mdl_PlanUnits.bulkCreate(
unit_ids.map(unit_id => ({ plan_id: id, unit_id })),
);
}
logActivity(req.user?.user_id, 'sync_plan_units', { entityType: 'tier_plan', details: { plan_id: id, unit_ids, count: unit_ids.length } });
return R.success(res, 'Plan units updated.');
} catch (err) {
console.error('[ADMIN][SYNC PLAN UNITS]', err);
return R.error(res, 'Could not update plan units.', 500);
}
};
// ─── PLAN LESSONS ─────────────────────────────────────────────────────────────
// Mirrors PLAN COURSES above — bundling/display only, not an access-control
// mechanism (see canAccessLesson in controllers/client/courses.controller.js).
exports.getPlanLessons = async (req, res) => {
try {
const entries = await mdl_PlanLessons.findAll({
where: { plan_id: req.params.id },
include: [{
model: Lesson,
as: 'lesson',
attributes: ['lesson_id', 'title', 'subscription'],
}],
order: [['createdAt', 'ASC']],
});
return R.success(res, 'Plan lessons retrieved.', entries.map(e => e.lesson));
} catch (err) {
console.error('[ADMIN][GET PLAN LESSONS]', err);
return R.error(res, 'Could not retrieve plan lessons.', 500);
}
};
exports.syncPlanLessons = async (req, res) => {
try {
const { id } = req.params;
const { lesson_ids = [] } = req.body;
if (!Array.isArray(lesson_ids))
return R.error(res, 'lesson_ids must be an array.', 400);
const plan = await mdl_TierPlans.findByPk(id);
if (!plan) return R.error(res, 'Plan not found.', 404);
await mdl_PlanLessons.destroy({ where: { plan_id: id } });
if (lesson_ids.length) {
// lesson_id has a UNIQUE constraint — clear any orphaned/other-plan assignments
// for these lessons before inserting, so the insert isn't silently skipped
await mdl_PlanLessons.destroy({ where: { lesson_id: lesson_ids } });
await mdl_PlanLessons.bulkCreate(
lesson_ids.map(lesson_id => ({ plan_id: id, lesson_id })),
);
}
logActivity(req.user?.user_id, 'sync_plan_lessons', { entityType: 'tier_plan', details: { plan_id: id, lesson_ids, count: lesson_ids.length } });
return R.success(res, 'Plan lessons updated.');
} catch (err) {
console.error('[ADMIN][SYNC PLAN LESSONS]', err);
return R.error(res, 'Could not update plan lessons.', 500);
}
};
+41
View File
@@ -41,6 +41,7 @@ const CompletionRequirement = require("../../models/courses/completion_requireme
const { VALID_ENTITY_TYPES } = require("../../utils/courses/completion_requirements.registry");
const mdl_Users = require("../../models/users/users.mdl");
const { mdl_PlanUnits, mdl_TierPlans } = require("../../models/tiers/tier.associations");
const notDeleted = { deletedAt: null };
const onlyDeleted = { deletedAt: { [Op.not]: null } };
@@ -160,6 +161,46 @@ exports.getUnitsFlat = async (req, res) => {
}
};
// Bundle picker (Tier Plan wizard) — mirrors getCoursesBySubscription in
// controllers/admin/courses.controller.js.
exports.getUnitsBySubscription = async (req, res) => {
try {
const { slug } = req.query;
if (!slug) return R.error(res, 'slug query param is required.', 400);
const rows = await Unit.findAll({
where: { ...notDeleted, subscription: slug },
attributes: ['unit_id', 'title', 'description', 'subscription'],
include: [{
model: mdl_PlanUnits,
as: 'planUnit',
required: false,
attributes: ['plan_id'],
include: [{
model: mdl_TierPlans,
as: 'plan',
attributes: ['plan_id', 'label'],
}],
}],
order: [['title', 'ASC']],
});
// Flatten so the frontend can just check `assigned_plan` — a unit belongs
// to at most one plan (UNIQUE constraint on plan_units.unit_id).
const data = rows.map((u) => {
const plain = u.toJSON();
const assigned_plan = plain.planUnit?.plan ?? null;
delete plain.planUnit;
return { ...plain, assigned_plan };
});
return R.success(res, 'Units retrieved.', data);
} catch (err) {
console.error('[UNIT LIB][BY SUBSCRIPTION]', err);
return R.error(res, 'Could not retrieve units.', 500);
}
};
exports.getUnit = async (req, res) => {
try {
const { unitId } = req.params;
@@ -13,6 +13,7 @@ const { Op } = require('sequelize');
const mdl_UserActivity = require('../../models/users/user_activity.mdl');
const mdl_Users = require('../../models/users/users.mdl');
const R = require('../../utils/response.util');
const { resolveAvatarUrl } = require('../../utils/resolveAvatar.util');
const USER_ATTRS = [
'user_id', 'email', 'acc_type',
@@ -66,7 +67,7 @@ exports.getActivity = async (req, res) => {
offset,
});
const data = rows.map(formatRow);
const data = await Promise.all(rows.map(formatRow));
return R.success(res, 'Activity feed retrieved.', {
total: count,
@@ -117,15 +118,16 @@ exports.getUserActivity = async (req, res) => {
// ─── Helpers ──────────────────────────────────────────────────────────────────
function formatRow(row) {
async function formatRow(row) {
const r = row.toJSON();
const info = r.user?.personal_info;
const avatar = await resolveAvatarUrl(info?.avatar);
return {
activity_id: r.activity_id,
user_id: r.user_id,
email: r.user?.email ?? null,
full_name: info?.name?.full_name ?? null,
avatar_url: info?.avatar?.url ?? null,
avatar_url: avatar?.url ?? null,
acc_type: r.user?.acc_type ?? null,
action: r.action,
entity_type: r.entity_type,