
# 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`

```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`

```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`

```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`

```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`

```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`

```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.*
