Service Library - tutorCatalog

This document provides a complete reference of the custom code library for the tutorCatalog service. It includes all library functions, edge functions with their REST endpoints, templates, and assets.

Library Functions

Library functions are reusable modules available to all business APIs and other custom code within the service via require("lib/<moduleName>").

getTutorProfileIdsBySessionUserId.js

module.exports = async function getTutorProfileIdsBySessionUserId(userId) {
  if (!userId) return [];
  const db = require("dbLayer");
  const items = await db.getTutorProfileListByMQuery({ tutorId: userId, isActive: true });
  return items.map(i => i.id);
}

getCoursePackIdsBySessionUserId.js

module.exports = async function getCoursePackIdsBySessionUserId(userId) {
  if (!userId) return [];
  const tutorProfileIds = await LIB.getTutorProfileIdsBySessionUserId(userId);
  if (!tutorProfileIds.length) return [];
  const db = require("dbLayer");
  const packs = await db.getCoursePackListByMQuery({ tutorProfileId: { $in: tutorProfileIds }, isActive: true });
  return packs.map(p => p.id);
}

isMaterialViewAllowedBySession.js

module.exports = async function isMaterialViewAllowedBySession(courseMaterialId, userId, roleId) {
  if (!userId || !courseMaterialId) return false;
  if (roleId === "admin") return true;
  const MODELS = require("models");
  const cm = await MODELS.CourseMaterial.findOne({ where: { id: courseMaterialId, isActive: true } });
  if (!cm) return false;
  const pack = await MODELS.CoursePack.findOne({ where: { id: cm.coursePackId, isActive: true } });
  if (!pack) return false;
  if (roleId === "tutor") {
    const tprofiles = await MODELS.TutorProfile.findAll({ where: { tutorId: userId, isActive: true } });
    if (tprofiles.some(tpf => tpf.id === pack.tutorProfileId)) return true;
  }
  if (roleId === "student") {
    const { fetchRemoteListByMQuery } = require("serviceCommon");
    const enrolls = await fetchRemoteListByMQuery("enrollmentManagement:enrollment", { studentId: userId, coursePackId: pack.id, isActive: true, enrollmentStatus: "active" }, 0, 1);
    if (enrolls && enrolls.length > 0) return true;
  }
  return false;
}

getAllowedMaterialWhereClause.js

module.exports = async function getAllowedMaterialWhereClause(userId, roleId) {
  if (!userId) return { id: null };
  if (roleId === "admin") return {};
  const db = require("dbLayer");
  if (roleId === "tutor") {
    const tprofiles = await db.getTutorProfileListByMQuery({ tutorId: userId, isActive: true });
    if (!tprofiles.length) return { id: null };
    const packs = await db.getCoursePackListByMQuery({ tutorProfileId: { $in: tprofiles.map(p => p.id) }, isActive: true });
    const accessiblePackIds = packs.map(p => p.id);
    if (!accessiblePackIds.length) return { id: null };
    return { coursePackId: { $in: accessiblePackIds } };
  }
  if (roleId === "student") {
    const { fetchRemoteListByMQuery } = require("serviceCommon");
    const enrolls = await fetchRemoteListByMQuery("enrollmentManagement:enrollment", { studentId: userId, isActive: true, enrollmentStatus: "active" }, 0, 100);
    const enrolledPackIds = enrolls ? enrolls.map(e => e.coursePackId) : [];
    if (!enrolledPackIds.length) return { id: null };
    return { coursePackId: { $in: enrolledPackIds } };
  }
  return { id: null };
}

checkCoursePackActiveEnrollments.js

module.exports = async function checkCoursePackActiveEnrollments(coursePackId) {
  if (!coursePackId) return { hasActiveEnrollments: false, count: 0, enrollments: [] };
  try {
    // Use interservice HTTP call to enrollmentManagement's internal fetch endpoint
    // This reads from the DB (source of truth), not ES which may be stale
    const axios = require('axios');
    const { getServiceSecret } = require('common');

    // In preview, all services share the same base host with different routes
    const baseUrl = process.env.ENROLLMENTMANAGEMENT_API_URL || (process.env.PREVIEW_BASE_URL ? process.env.PREVIEW_BASE_URL + '/enrollmentmanagement-api' : null);

    // Fallback: use the _fetchList internal endpoint via M2M or query ES
    if (!baseUrl) {
      // Try ES as fallback
      const { elasticClient } = require('common');
      const res = await elasticClient.search({
        index: 'tutorhub_enrollment',
        body: {
          query: {
            bool: {
              must: [
                { term: { coursePackId: coursePackId } },
                { term: { enrollmentStatus: 'active' } },
                { term: { isActive: true } }
              ]
            }
          },
          size: 100
        }
      });
      const enrollments = res.hits.hits.map(h => h._source);
      return { hasActiveEnrollments: enrollments.length > 0, count: enrollments.length, enrollments };
    }

    const serviceSecret = getServiceSecret ? getServiceSecret() : process.env.SERVICE_SECRET_KEY;
    const url = `${baseUrl}/m2m/getEnrollmentListByMQuery`;
    const response = await axios.post(url, {
      query: { coursePackId, enrollmentStatus: 'active', isActive: true }
    }, {
      headers: {
        'Content-Type': 'application/json',
        ...(serviceSecret ? { 'x-service-secret': serviceSecret } : {})
      },
      timeout: 10000
    });

    const enrollments = response.data || [];
    return { hasActiveEnrollments: enrollments.length > 0, count: enrollments.length, enrollments };
  } catch (err) {
    console.log('ERROR in checkCoursePackActiveEnrollments:', err.message);
    // FAIL SAFE: if we can't check, assume there ARE enrollments to prevent accidental deletion
    return { hasActiveEnrollments: true, count: -1, enrollments: [], error: err.message };
  }
}

triggerAdminCourseRemovalCascade.js

module.exports = async function triggerAdminCourseRemovalCascade(coursePackId, removalReason, enrollmentCheck, session) {
  const { fetchRemoteListByMQuery, callRemoteApi } = require('serviceCommon');
  const { elasticClient } = require('common');

  if (!enrollmentCheck || !enrollmentCheck.hasActiveEnrollments) {
    return { processed: 0, total: 0, refunds: [] };
  }

  const enrollments = enrollmentCheck.enrollments || [];
  const results = { processed: 0, total: enrollments.length, refunds: [], errors: [] };

  for (const enrollment of enrollments) {
    try {
      // Fetch lesson slots to calculate completion percentage
      const slotIds = enrollment.lessonSlotIds || [];
      let completedCount = 0;
      let totalSlots = slotIds.length;

      if (totalSlots > 0) {
        try {
          const slotsRes = await elasticClient.search({
            index: 'tutorhub_lessonslot',
            body: {
              query: { terms: { id: slotIds } },
              _source: ['id', 'status'],
              size: 500
            }
          });
          const slots = slotsRes.hits.hits.map(h => h._source);
          completedCount = slots.filter(s => s.status === 'completed').length;
        } catch (esErr) {
          console.log('Warning: ES slot lookup failed, assuming 0 completed:', esErr.message);
        }
      }

      // Calculate pro-rata refund amount
      const completionRatio = totalSlots > 0 ? completedCount / totalSlots : 0;
      let refundAmount = 0;
      let refundNote = '';

      if (completionRatio === 0) {
        refundAmount = enrollment.totalAmount;
        refundNote = 'Full refund - no lessons completed';
      } else if (completionRatio <= 0.5) {
        const unusedSlots = totalSlots - completedCount;
        refundAmount = Math.round((unusedSlots / totalSlots) * enrollment.totalAmount * 100) / 100;
        refundNote = `Pro-rata refund: ${completedCount}/${totalSlots} lessons completed, refunding ${unusedSlots} unused lessons`;
      } else {
        refundAmount = 0;
        refundNote = `No refund: ${completedCount}/${totalSlots} lessons completed (over 50%)`;
      }

      // Fetch student data for email notifications
      let studentData = null;
      try {
        const studentRes = await elasticClient.search({
          index: 'tutorhub_user',
          body: { query: { term: { id: enrollment.studentId } }, _source: ['fullname', 'email'], size: 1 }
        });
        if (studentRes.hits.hits.length > 0) studentData = studentRes.hits.hits[0]._source;
      } catch (e) { console.log('Warning: student lookup failed:', e.message); }

      if (refundAmount > 0) {
        try {
          await callRemoteApi('enrollmentManagement', 'adminRefundEnrollment', {
            enrollmentId: enrollment.id,
            reason: `Course removed by admin. Reason: ${removalReason || 'No reason provided'}. ${refundNote}`
          }, session);
        } catch (refundErr) {
          console.log('Warning: adminRefundEnrollment failed for', enrollment.id, ':', refundErr.message);
          results.errors.push({ enrollmentId: enrollment.id, error: refundErr.message });
        }
      } else {
        try {
          await callRemoteApi('enrollmentManagement', 'cancelEnrollment', {
            enrollmentId: enrollment.id,
            reason: `Course removed by admin. Reason: ${removalReason || 'No reason provided'}. ${refundNote}`
          }, session);
        } catch (cancelErr) {
          console.log('Warning: cancelEnrollment failed for', enrollment.id, ':', cancelErr.message);
          results.errors.push({ enrollmentId: enrollment.id, error: cancelErr.message });
        }
      }

      results.refunds.push({
        enrollmentId: enrollment.id,
        studentId: enrollment.studentId,
        studentEmail: studentData ? studentData.email : null,
        studentName: studentData ? studentData.fullname : null,
        totalAmount: enrollment.totalAmount,
        currency: enrollment.currency || 'USD',
        refundAmount,
        completedLessons: completedCount,
        totalLessons: totalSlots,
        note: refundNote
      });
      results.processed++;
    } catch (err) {
      results.errors.push({ enrollmentId: enrollment.id, error: err.message });
    }
  }

  // Publish individual Kafka events per student for email notifications
  try {
    const { sendMessageToKafka } = require('common');
    const db = require('dbLayer');
    const coursePack = await db.getCoursePackById(coursePackId);
    const courseTitle = coursePack ? coursePack.title : 'Unknown Course';

    for (const refund of results.refunds) {
      if (refund.studentEmail) {
        await sendMessageToKafka('tutorhub-tutorcatalog-service-coursepack-removed-by-admin', {
          coursePackId,
          courseTitle,
          removalReason: removalReason || 'No reason provided',
          student: { email: refund.studentEmail, fullname: refund.studentName },
          refundAmount: refund.refundAmount,
          totalAmount: refund.totalAmount,
          currency: refund.currency,
          completedLessons: refund.completedLessons,
          totalLessons: refund.totalLessons,
          refundNote: refund.note,
          removedAt: new Date().toISOString()
        });
      }
    }
  } catch (kafkaErr) {
    console.log('Warning: failed to publish course removal events:', kafkaErr.message);
  }

  return results;
}

This document was generated from the service library configuration and should be kept in sync with design changes.