
# Service Library - `aiCoach`

This document provides a complete reference of the custom code library for the `aiCoach` 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>")`.


### `checkBanStatusFn.js`

```js
module.exports = async function checkBanStatusFn(userId) {
  const { getModerationRecordListByQuery } = require('dbLayer');
  try {
    const records = await getModerationRecordListByQuery({ userId });
    if (!records || records.length === 0) return { isBanned: false };
    /* Check lifetime ban - find most recent ban/reversal action */
    const banRelated = records.filter(r => r.action === 'lifetimeBan' || r.action === 'banReversal');
    if (banRelated.length > 0) {
      banRelated.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
      if (banRelated[0].action === 'lifetimeBan') {
        return { isBanned: true, reason: 'Lifetime ban', banType: 'lifetimeBan' };
      }
    }
    /* Check active suspension */
    const now = new Date();
    const activeSuspension = records.find(r =>
      (r.action === 'suspension24h' || r.action === 'suspension1week') &&
      r.suspensionExpiresAt && new Date(r.suspensionExpiresAt) > now
    );
    if (activeSuspension) {
      return { isBanned: true, reason: 'Chat suspended until ' + activeSuspension.suspensionExpiresAt, banType: activeSuspension.action };
    }
    return { isBanned: false };
  } catch (err) {
    console.error('checkBanStatusFn error:', err);
    return { isBanned: false };
  }
};
```


### `checkQuotaFn.js`

```js
const { Op } = require("sequelize");
module.exports = async function checkQuotaFn(userId) {
  const { getQuotaConfigListByQuery, getUserQuotaByQuery, createUserQuota, updateUserQuotaById,getAdditionalQuotaByQuery } = require('dbLayer');
  try {
    /* Get system quota config */
    const configs = await getQuotaConfigListByQuery({});
    if (!configs || configs.length === 0) {
      /* No quota config set - allow by default */
      return { available: true, remaining: -1, noConfig: true };
    }
    const config = configs[0];
    const now = new Date();
    /* Calculate period boundaries */
    let periodStart, periodEnd;
    if (config.quotaPeriod === 'daily') {
      periodStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      periodEnd = new Date(periodStart.getTime() + 24 * 60 * 60 * 1000);
    } else if (config.quotaPeriod === 'weekly') {
      const day = now.getDay();
      periodStart = new Date(now.getFullYear(), now.getMonth(), now.getDate() - day);
      periodEnd = new Date(periodStart.getTime() + 7 * 24 * 60 * 60 * 1000);
    } else {
      periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
      periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 1);
    }
    /* Get or create user quota */
let existQuota = await getUserQuotaByQuery({
  userId,
  periodStart: { [Op.lte]: now },
  periodEnd: { [Op.gt]: now },
});
    let quota;

// 2. if none → create new (new day)
if (!existQuota) {
  quota = await createUserQuota({
    userId,
    messageCount: 0,
    periodStart,
    periodEnd,
  });
}
    else{
      quota=existQuota
    }
    const additionalQuota=await getAdditionalQuotaByQuery({
  userId,
  status:'active',
  periodStart: { [Op.lte]: now },
  periodEnd: { [Op.gt]: now },
})

// 3. compute remaining
const remaining = (additionalQuota?.additionalMessage??0) +config.quotaLimit - (quota?.messageCount ?? 0);

return {
  available: remaining > 0,
  remaining,
  quotaId: quota.id,
  limit: config.quotaLimit,
  existQuota,
  additionalQuota
};
  } catch (err) {
    console.error('checkQuotaFn error:', err);
    return { available: true, remaining: -1 };
  }
};
```


### `incrementQuotaFn.js`

```js
const { Op } = require("sequelize");
module.exports = async function incrementQuotaFn(userId, context) {
  const { getUserQuotaByQuery, createUserQuota, updateUserQuotaById } = require('dbLayer');

  try {
    const now = new Date();

    // Define DAILY boundaries (midnight → midnight)
    const periodStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
    const periodEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);

    let quota = await getUserQuotaByQuery({
      userId,
  periodStart: { [Op.lte]: now },
  periodEnd: { [Op.gt]: now },
    });

    if (!quota) {
      quota = await createUserQuota({
        userId,
        messageCount: 1,
        periodStart,
        periodEnd,
      }, context);

      return;
    }

    await updateUserQuotaById(
      quota.id,
      { messageCount: (quota.messageCount || 0) + 1 },
      context
    );

  } catch (err) {
    console.error('incrementQuotaFn error:', err);
  }
};
```


### `applyPenaltyFn.js`

```js
module.exports = async function applyPenaltyFn(userId, reason, context,content) {
  const { getModerationRecordListByQuery, createModerationRecord } = require('dbLayer');
  const COMMON = require('common');
  try {
    /* Count previous offenses (excluding reversals) */
    const records = await getModerationRecordListByQuery({ userId });
    const offenses = records.filter(r => r.action !== 'banReversal');
    const offenseCount = offenses.length;
    let action, suspensionExpiresAt = null;
    const now = new Date();
    if (offenseCount === 0) {
      /* First offense: 24h suspension */
      action = 'suspension24h';
      suspensionExpiresAt = new Date(now.getTime() + 24 * 60 * 60 * 1000);
    } else if (offenseCount === 1) {
      /* Second offense: 1 week suspension */
      action = 'suspension1week';
      suspensionExpiresAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
    } else {
      /* Third+ offense: lifetime ban */
      action = 'lifetimeBan';
    }
    await createModerationRecord({
      userId,
      offenseType: reason || 'Content policy violation',
      action,
      reason: 'Automated progressive penalty - offense #' + (offenseCount + 1),
      suspensionExpiresAt,
      content
    }, context);
    /* If lifetime ban, deactivate user in auth service */
    if (action === 'lifetimeBan') {
      try {
        await COMMON.sendRestRequest(
          process.env.AUTH_SERVICE_URL ? process.env.AUTH_SERVICE_URL + '/m2m/user/updateById' : 'http://auth:3000/m2m/user/updateById',
          'PUT',
          { id: userId, dataClause: { isActive: false } },
          { 'x-service-token': process.env.M2M_TOKEN || '' }
        );
      } catch (m2mErr) {
        console.error('Failed to deactivate user in auth service:', m2mErr);
      }
    }
    return { action, suspensionExpiresAt };
  } catch (err) {
    console.error('applyPenaltyFn error:', err);
    throw err;
  }
};
```


### `flagMessageContent.js`

```js
module.exports = async function flagMessageContent(messageContent) {
  const OpenAI = require('openai');
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  try {
    const response = await openai.chat.completions.create({
      model: 'gpt-4.1',
      messages: [
        {
          role: 'system',
          content: 'You are a content moderation system for a fitness and nutrition coaching platform.Youll be given full chat history of messages and the last message as users input. Youll analyze the last message but consider if its within the context or not. Analyze the user message and determine if it is: 1) Off-topic (not related to fitness, nutrition, health, exercise, diet, body composition, or wellness), 2) Trolling or abusive, 3) Attempting to misuse the AI for non-fitness purposes (e.g., asking to write code, generate non-fitness content). Respond with JSON: {"flagged": boolean, "reason": string}. Only flag clearly off-topic or abusive content. Fitness-adjacent topics like sleep, stress, supplements, motivation are NOT off-topic.It may look offtopic but there may be typo or some misunderstanding may happen time to time consider the last 3 messages by user to identify a need for ban. If user usually insists on trolling and of topic then apply the ban dont directly ban a user for a single misuse a typo and total trolling should be exact.'
        },
        ...messageContent
      ],
      response_format: { type: 'json_object' },
      temperature: 0.1,
      max_tokens: 200
    });
    const result = JSON.parse(response.choices[0].message.content);
    return { flagged: result.flagged === true, reason: result.reason || '',content:messageContent[messageContent.length - 1]?.content||'' };
  } catch (err) {
    console.error('flagMessageContent error:', err);
    return { flagged: false, reason: '' };
  }
};
```


### `getSystemPrompt.js`

```js
module.exports = function getSystemPrompt(userProfile, trainingProgram, exercises, mealPlan, meals, weightLogs) {
  let prompt = 'You are an evidence-based AI fitness coach. You provide personalized training programs and nutrition plans based on scientific principles.\n\n';
  prompt += '## CORE RULES\n';
  prompt += '- NEVER recommend steroids, PEDs, extreme dieting (<1000kcal), or unsafe loads.\n';
  prompt += '- Always use Mifflin-St Jeor for BMR: Males: 10*kg + 6.25*cm - 5*age + 5. Females: 10*kg + 6.25*cm - 5*age - 161.\n';
  prompt += '- TDEE multipliers: Sedentary=1.2, Light=1.375, Moderate=1.55, Heavy=1.725, Athlete=1.9.\n';
  prompt += '- Cutting deficit: -300 to -700 kcal (-10-25% TDEE). Bulking surplus: +200 to +400 kcal (+5-10% TDEE).\n';
  prompt += '- Protein: 1.6-2.2g/kg (up to 2.6g/kg aggressive cut). Fat: min 0.6-1.0g/kg (never <15-20% calories). Remaining = carbs.\n';
  prompt += '- Higher carbs on training days, lower on rest days.\n';
  prompt += '- Training splits: 3 days=full body, 4 days=upper/lower, 5-6 days=PPL.\n';
  prompt += '- Each muscle: 1 lengthened + 1 shortened exercise min. Compounds: 6-12 reps, 1-3 RIR. Isolation: 8-15 reps, 0-2 RIR.\n';
  prompt += '- Volume: maintenance ~6 sets, growth 10-20, specialization 20-25 sets/muscle/week.\n';
  prompt += '- Double progression: hit top rep range all sets -> increase weight.\n';
  prompt += '- Deload every 6-8 weeks: volume -40-50%, intensity -10%.\n';
  prompt += '- Cardio: Zone 2 (60-70% HRmax), 20-40 min, 3-5x/week when goal supports it.\n';
  prompt += '- Daily steps: 7,000-12,000 for NEAT.\n';
  prompt += '- Weight tracking: 3-4 mornings/week, after waking/bathroom/before food. Use weekly averages only.\n';
  prompt += '- Cutting target: 0.5-1% BW/week. Bulking target: 0.25-0.5% BW/week.\n';
  prompt += '- If cutting loss <0.3%/wk: reduce 150-200 kcal. If >1.5%/wk: increase slightly.\n';
  prompt += '- If bulking gain <0.2%/wk: add 150 kcal. If >0.75%/wk: reduce.\n';
  prompt += '- Water retention: early cut drops mostly water (1g glycogen=3g water). Post-cheat 1-3kg is temporary. Wait 2-3 days.\n';
  prompt += '- Recovery check-ins weekly: sleep, soreness, performance. Reduce volume if poor recovery signals.\n';
  prompt += '- Regional food suggestions based on user country. Respect dietary restrictions.\n\n';
  if (userProfile) {
    prompt += '## USER PROFILE\n';
    prompt += 'Name: ' + (userProfile.fullname || 'N/A') + '\n';
    prompt += 'Sex: ' + (userProfile.sex || 'N/A') + ', Height: ' + (userProfile.height || 'N/A') + 'cm, Weight: ' + (userProfile.weight || 'N/A') + 'kg\n';
    prompt += 'DOB: ' + (userProfile.dateOfBirth || 'N/A') + ', Activity: ' + (userProfile.activityLevel || 'N/A') + '\n';
    prompt += 'Experience: ' + (userProfile.trainingExperience || 'N/A') + ', Training days/wk: ' + (userProfile.weeklyTrainingDays || 'N/A') + '\n';
    prompt += 'Goal: ' + (userProfile.fitnessGoal || 'N/A') + ', Country: ' + (userProfile.country || 'N/A') + '\n';
    prompt += 'Dietary restrictions: ' + (userProfile.dietaryRestrictions || 'None') + '\n';
    prompt += 'Equipment: ' + (userProfile.availableEquipment || 'N/A') + '\n\n';
  }
  if (trainingProgram) {
    prompt += '## CURRENT TRAINING PROGRAM\n';
    prompt += 'Split: ' + trainingProgram.splitType + ', Deload every ' + trainingProgram.deloadIntervalWeeks + ' weeks\n'+'id:'+trainingProgram.id;
    if (exercises && exercises.length > 0) {
      prompt += 'Exercises:\n';
      exercises.forEach(e => {
        prompt += '-id: '+e.id+'\n- ' + e.dayLabel + ': ' + e.exerciseName + ' (' + e.muscleGroup + ') ' + e.sets + 'x' + e.repMin + '-' + e.repMax + ' RIR ' + e.rirTarget + '\n';
      });
    }
    prompt += '\n';
  }
  if (mealPlan) {
    prompt += '## CURRENT MEAL PLAN\n';
    prompt += 'Calories: ' + mealPlan.dailyCalorieTarget + ' kcal, P: ' + mealPlan.proteinGrams + 'g, F: ' + mealPlan.fatGrams + 'g, C: ' + mealPlan.carbGrams + 'g\n'+'id:'+mealPlan.id;
    if (meals && meals.length > 0) {
      meals.forEach(m => { prompt += '-id:'+m.id+'\n- ' + m.mealLabel + ': ' + m.foods + ' (' + m.totalCalories + ' kcal)\n'; });
    }
    prompt += '\n';
  }
  if (weightLogs && weightLogs.length > 0) {
    prompt += '## RECENT WEIGHT DATA\n';
    weightLogs.slice(0, 14).forEach(w => { prompt += w.measuredAt + ': ' + w.weightKg + 'kg\n'; });
    prompt += '\n';
  }
  prompt += '## TOOL USAGE\n';
  prompt += 'To use the tools more efficently exercises are saved under training programs so create a training program then create the exercises. If any new training program is created then the old one gets archived so try to updateTrainingProgram if user wants small alterations. If user wants to abandon the training approach then you can create a new one. A new training program will come with 0 exercises when created. For meals same approach here createMealPlan creates a new meal plan with macros and caloric goals logged no meals are should be entered yet. you can add/update meals by addMeal/updateMeal tools. If user just make changes you can updateMealPlan for caloric changes. If user really wants a total different approach then create a new meal plan by createMealPlan tool. creating a new meal plan also comes with 0 meals connected to it so itll need a new meals list. for inner updates such as exercises and plans you can also update the main structure if changes are effective to approach. for example a new lunch changes the approx caloric goal of that meal plan then we can also update the meal plan. Dont forget to make big changes first create the main object createTrainingProgram then add exercises. For meals createMeal plan first then addMeals. Also while creating the main objects dont always create them each step add when necessary. If the operating fails on this the newly added exercises or meals wont be seen under current plan. Consider the active plan if exists first. '
  prompt += 'Use the provided tools to create/update training programs, exercises, meal plans, and meals when the user requests changes. Always use tools for data modifications. As you normally do you return them in text responses too keep that but main ideas are always must be handled via tools meals exercises training program and calories.\n';
  prompt += 'When reporting non-adherence, calculate realistic timeline impact and communicate constructively. If for example training program should be captured first before exercises there is a dependency be clear and tell them if they are trying to get dependent outcome first. exercise lists needs training program ids and plan meal that logs the meals itself need meal planId. always capture these with tools\n';
  return prompt;
};
```


### `getToolDefinitions.js`

```js
module.exports = function getToolDefinitions() {
  return [
    { type: 'function', function: { name: 'createTrainingProgram', description: 'Create a new training program for the user. Archives any existing active program first.', parameters: { type: 'object', properties: { splitType: { type: 'string' }, deloadIntervalWeeks: { type: 'integer' }, cardioType: { type: 'string' }, cardioDurationMinutes: { type: 'integer' }, cardioFrequencyPerWeek: { type: 'integer' }, dailyStepTarget: { type: 'integer' }, notes: { type: 'string' } }, required: ['splitType'] } } },
    { type: 'function', function: { name: 'addExercise', description: 'Add an exercise to the current training program.', parameters: { type: 'object', properties: { dayLabel: { type: 'string' }, exerciseName: { type: 'string' }, muscleGroup: { type: 'string' }, movementType: { type: 'string', enum: ['lengthened', 'shortened', 'compound'] }, sets: { type: 'integer' }, repMin: { type: 'integer' }, repMax: { type: 'integer' }, rirTarget: { type: 'integer' }, progressionRule: { type: 'string' }, sortOrder: { type: 'integer' } }, required: ['dayLabel', 'exerciseName', 'muscleGroup', 'movementType', 'sets', 'repMin', 'repMax', 'rirTarget'] } } },
    { type: 'function', function: { name: 'updateExercise', description: 'Update an existing exercise by its ID.', parameters: { type: 'object', properties: { exerciseId: { type: 'string' }, exerciseName: { type: 'string' }, sets: { type: 'integer' }, repMin: { type: 'integer' }, repMax: { type: 'integer' }, rirTarget: { type: 'integer' }, progressionRule: { type: 'string' } }, required: ['exerciseId'] } } },
    { type: 'function', function: { name: 'removeExercise', description: 'Remove an exercise by its ID.', parameters: { type: 'object', properties: { exerciseId: { type: 'string' } }, required: ['exerciseId'] } } },
    { type: 'function', function: { name: 'createMealPlan', description: 'Create a new meal plan for the user. Archives any existing active plan first.', parameters: { type: 'object', properties: { dailyCalorieTarget: { type: 'integer' }, proteinGrams: { type: 'number' }, fatGrams: { type: 'number' }, carbGrams: { type: 'number' }, trainingDayCalories: { type: 'integer' }, restDayCalories: { type: 'integer' }, trainingDayCarbGrams: { type: 'number' }, restDayCarbGrams: { type: 'number' }, notes: { type: 'string' } }, required: ['dailyCalorieTarget', 'proteinGrams', 'fatGrams', 'carbGrams'] } } },
    { type: 'function', function: { name: 'addMeal', description: 'Add a meal to the current meal plan. while adding always mention the individual grams for each food while listing the foods. weights are always should be raw where people can easily weigh their foods. Or quantity for simple things such as nuts almonds eggs etc', parameters: { type: 'object', properties: { mealLabel: { type: 'string' }, foods: { type: 'string' }, totalCalories: { type: 'integer' }, protein: { type: 'number' }, fat: { type: 'number' }, carbs: { type: 'number' }, sortOrder: { type: 'integer' } }, required: ['mealLabel', 'foods', 'totalCalories', 'protein', 'fat', 'carbs'] } } },
    { type: 'function', function: { name: 'updateMealPlan', description: 'Update the current meal plan macros/calories.', parameters: { type: 'object', properties: { dailyCalorieTarget: { type: 'integer' }, proteinGrams: { type: 'number' }, fatGrams: { type: 'number' }, carbGrams: { type: 'number' }, trainingDayCalories: { type: 'integer' }, restDayCalories: { type: 'integer' }, trainingDayCarbGrams: { type: 'number' }, restDayCarbGrams: { type: 'number' }, notes: { type: 'string' } }, required: [] } } },
    { type: 'function', function: { name: 'updateMeal', description: 'Update an existing meal by its ID.', parameters: { type: 'object', properties: { mealId: { type: 'string' }, mealLabel: { type: 'string' }, foods: { type: 'string' }, totalCalories: { type: 'integer' }, protein: { type: 'number' }, fat: { type: 'number' }, carbs: { type: 'number' } }, required: ['mealId'] } } },
    { type: 'function', function: { name: 'removeMeal', description: 'Remove a meal by its ID.', parameters: { type: 'object', properties: { mealId: { type: 'string' } }, required: ['mealId'] } } },
    { type: 'function', function: { name: 'updateTrainingProgram', description: 'Update the current training program settings.', parameters: { type: 'object', properties: { deloadIntervalWeeks: { type: 'integer' }, cardioType: { type: 'string' }, cardioDurationMinutes: { type: 'integer' }, cardioFrequencyPerWeek: { type: 'integer' }, dailyStepTarget: { type: 'integer' }, notes: { type: 'string' } }, required: [] } } }
  ];
};
```


### `executeToolCall.js`

```js
module.exports = async function executeToolCall(toolCall, userId, currentProgram, currentMealPlan, context) {
  const { createTrainingProgram, updateTrainingProgramById, updateTrainingProgramByQuery, createProgramExercise, updateProgramExerciseById, deleteProgramExerciseById, createMealPlan, updateMealPlanById, updateMealPlanByQuery, createPlanMeal, updatePlanMealById, deletePlanMealById, getTrainingProgramByQuery, getMealPlanByQuery } = require('dbLayer');
  const fnName = toolCall.function.name;
  const args = JSON.parse(toolCall.function.arguments);
  try {
    if (fnName === 'createTrainingProgram') {
      /* Archive existing active program */
      if (currentProgram) {
        await updateTrainingProgramById(currentProgram.id, { status: 'archived' }, context);
      }
      const prog = await createTrainingProgram({ userId, status: 'active', ...args }, context);
      return { success: true, programId: prog.id };
    }
    if (fnName === 'addExercise') {
      const prog =  await getTrainingProgramByQuery({ userId, status: 'active' });
      if (!prog) return { success: false, error: 'No active training program' };
      const ex = await createProgramExercise({ trainingProgramId: prog.id, ...args, sortOrder: args.sortOrder || 0 }, context);
      return { success: true, exerciseId: ex.id };
    }
    if (fnName === 'updateExercise') {
      const { exerciseId, ...data } = args;
      await updateProgramExerciseById(exerciseId, data, context);
      return { success: true };
    }
    if (fnName === 'removeExercise') {
      await deleteProgramExerciseById(args.exerciseId, context);
      return { success: true };
    }
    if (fnName === 'createMealPlan') {
      if (currentMealPlan) {
        await updateMealPlanById(currentMealPlan.id, { status: 'archived' }, context);
      }
      const plan = await createMealPlan({ userId, status: 'active', ...args }, context);
      return { success: true, mealPlanId: plan.id };
    }
    if (fnName === 'addMeal') {
      const plan =  await getMealPlanByQuery({ userId, status: 'active' });
      if (!plan) return { success: false, error: 'No active meal plan' };
      const meal = await createPlanMeal({ mealPlanId: plan.id, ...args, sortOrder: args.sortOrder || 0 }, context);
      return { success: true, mealId: meal.id };
    }
    if (fnName === 'updateMealPlan') {
      const plan = currentMealPlan || await getMealPlanByQuery({ userId, status: 'active' });
      if (!plan) return { success: false, error: 'No active meal plan' };
      await updateMealPlanById(plan.id, args, context);
      return { success: true };
    }
    if (fnName === 'updateMeal') {
      const { mealId, ...data } = args;
      await updatePlanMealById(mealId, data, context);
      return { success: true };
    }
    if (fnName === 'removeMeal') {
      await deletePlanMealById(args.mealId, context);
      return { success: true };
    }
    if (fnName === 'updateTrainingProgram') {
      const prog = currentProgram || await getTrainingProgramByQuery({ userId, status: 'active' });
      if (!prog) return { success: false, error: 'No active training program' };
      await updateTrainingProgramById(prog.id, args, context);
      return { success: true };
    }
    return { success: false, error: 'Unknown tool: ' + fnName };
  } catch (err) {
    console.error('executeToolCall error:', fnName, err);
    return { success: false, error: err.message };
  }
};
```


### `processMessagePipeline.js`

```js
module.exports = async function processMessagePipeline(context) {
  const { createChatMessage, updateChatMessageById, getChatMessageListByQuery, getTrainingProgramByQuery, getMealPlanByQuery, getProgramExerciseListByQuery, getPlanMealListByQuery, getWeightLogListByQuery } = require('dbLayer');
  const { fetchRemoteObjectByMQuery } = require('serviceCommon');
  const OpenAI = require('openai');
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  const chatMsg = context.chatMessage;
  const conversationId = chatMsg.conversationId;
  const userId = context.session.userId;
  const messageContent = chatMsg.content;
  /* Step 1: Content flagging */
   const recentMessages = await getChatMessageListByQuery({ conversationId });
    const flagCheckHistory = recentMessages.filter(m => !m.flagged).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)).slice(-20);
    const flagCheckMessages=[]
  for (const msg of flagCheckHistory) {
     flagCheckMessages.push({ role: msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'system', content: msg.content }); 
  }
  const flagResult = await LIB.flagMessageContent(flagCheckMessages);
  if (flagResult.flagged) {
    await updateChatMessageById(chatMsg.id, { flagged: true }, context);
    const penalty = await LIB.applyPenaltyFn(userId, flagResult.reason, context,flagResult.content);
    await LIB.incrementQuotaFn(userId, context);
    return { flagged: true, message: 'Your message was flagged as off-topic or inappropriate. ' + (penalty.action === 'suspension24h' ? 'You are suspended from chat for 24 hours.' : penalty.action === 'suspension1week' ? 'You are suspended from chat for 1 week.' : penalty.action === 'lifetimeBan' ? 'Your account has been permanently banned.' : ''), penalty: penalty.action };
  }
  /* Step 2: Load conversation history */
  const history = recentMessages.filter(m => !m.flagged).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)).slice(-20);
  /* Step 3: Load user profile from auth */
  const userProfile = await fetchRemoteObjectByMQuery('User', { id: userId });
  /* Step 4: Load current programs */
  const trainingProgram = await getTrainingProgramByQuery({ userId, status: 'active' });
  let exercises = [];
  if (trainingProgram) { exercises = await getProgramExerciseListByQuery({ trainingProgramId: trainingProgram.id }); }
  const mealPlan = await getMealPlanByQuery({ userId, status: 'active' });
  let meals = [];
  if (mealPlan) { meals = await getPlanMealListByQuery({ mealPlanId: mealPlan.id }); }
  /* Load recent weight logs */
  const weightLogs = await getWeightLogListByQuery({ userId });
  const sortedWeights = weightLogs.sort((a, b) => new Date(b.measuredAt) - new Date(a.measuredAt)).slice(0, 14);
  /* Step 5: Build messages */
  const systemPrompt = LIB.getSystemPrompt(userProfile, trainingProgram, exercises, mealPlan, meals, sortedWeights);
  const messages = [{ role: 'system', content: systemPrompt }];
  for (const msg of history) {
    if (msg.id !== chatMsg.id) { messages.push({ role: msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'system', content: msg.content }); }
  }
  messages.push({ role: 'user', content: messageContent });
  /* Step 6: Call OpenAI with tools */
  const tools = LIB.getToolDefinitions();
  let response = await openai.chat.completions.create({ model: 'gpt-4.1', messages, tools, temperature: 0.7, max_tokens: 10000 });
  let assistantMsg = response.choices[0].message;
  let toolCallsExecuted = [];
  /* Step 7: Handle tool call loop */
  while (assistantMsg.tool_calls && assistantMsg.tool_calls.length > 0) {
    messages.push(assistantMsg);
    for (const tc of assistantMsg.tool_calls) {
      const result = await LIB.executeToolCall(tc, userId, trainingProgram, mealPlan, context);
      toolCallsExecuted.push({ name: tc.function.name, result });
      messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result) });
    }
    response = await openai.chat.completions.create({ model: 'gpt-4.1', messages, tools, temperature: 0.7, max_tokens: 10000 });
    assistantMsg = response.choices[0].message;
  }
  const aiContent = assistantMsg.content || '';
  /* Step 8: Save AI response */
  await createChatMessage({ conversationId, role: 'assistant', content: aiContent, flagged: false, toolCallData: toolCallsExecuted.length > 0 ? JSON.stringify(toolCallsExecuted) : null }, context);
  /* Step 9: Increment quota */
  await LIB.incrementQuotaFn(userId, context);
  return { flagged: false, message: aiContent, toolCalls: toolCallsExecuted };
};
```


### `fetchAdditionalQuota.js`

```js
const { Op } = require("sequelize");
module.exports = async function fetchAdditionalQuota(userId) {
  const { getAdditionalQuotaByQuery } = require('dbLayer');
  try {
   
    const additionalQuota=await getAdditionalQuotaByQuery({
  userId,
  status:'active',
  periodStart: { [Op.lte]: now },
  periodEnd: { [Op.gt]: now },
})

// 3. compute remaining


return additionalQuota
  
  } catch (err) {
    console.error('fetchAdditionalQuota error:', err);
    return {}
      ;
  }
};
```


### `streamChatCompletion.js`

```js
module.exports = async function* streamChatCompletion(context) {
  const { createChatMessage, updateChatMessageById, getChatMessageListByQuery, getTrainingProgramByQuery, getMealPlanByQuery, getProgramExerciseListByQuery, getPlanMealListByQuery, getWeightLogListByQuery } = require('dbLayer');
  const { fetchRemoteObjectByMQuery } = require('serviceCommon');
  const OpenAI = require('openai');
  const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
  
  const conversationId = context.conversationId;
  const userId = context.session.userId;
  const messageContent = context.content;
  
  /* Step 1: Content flagging with recent messages */
  const recentMessages = await getChatMessageListByQuery({ conversationId });
  const flagCheckHistory = recentMessages.filter(m => !m.flagged).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)).slice(-20);
  const flagCheckMessages = [];
  for (const msg of flagCheckHistory) {
    flagCheckMessages.push({ role: msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'system', content: msg.content });
  }
  
  const flagResult = await LIB.flagMessageContent(flagCheckMessages);
  if (flagResult.flagged) {
    yield { type: 'flagged', flagged: true, reason: flagResult.reason };
    return;
  }
  
  /* Step 2: Load conversation history */
  const history = recentMessages.filter(m => !m.flagged).sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt)).slice(-20);
  
  /* Step 3: Load user profile from auth */
  const userProfile = await fetchRemoteObjectByMQuery('User', { id: userId });
  
  /* Step 4: Load current programs */
  const trainingProgram = await getTrainingProgramByQuery({ userId, status: 'active' });
  let exercises = [];
  if (trainingProgram) { 
    exercises = await getProgramExerciseListByQuery({ trainingProgramId: trainingProgram.id }); 
  }
  
  const mealPlan = await getMealPlanByQuery({ userId, status: 'active' });
  let meals = [];
  if (mealPlan) { 
    meals = await getPlanMealListByQuery({ mealPlanId: mealPlan.id }); 
  }
  
  /* Load recent weight logs */
  const weightLogs = await getWeightLogListByQuery({ userId });
  const sortedWeights = weightLogs.sort((a, b) => new Date(b.measuredAt) - new Date(a.measuredAt)).slice(0, 14);
  
  /* Step 5: Build messages */
  const systemPrompt = LIB.getSystemPrompt(userProfile, trainingProgram, exercises, mealPlan, meals, sortedWeights);
  const messages = [{ role: 'system', content: systemPrompt }];
  for (const msg of history) {
    messages.push({ role: msg.role === 'user' ? 'user' : msg.role === 'assistant' ? 'assistant' : 'system', content: msg.content });
  }
  messages.push({ role: 'user', content: messageContent });
  
  /* Step 6: Call OpenAI with streaming */
  const tools = LIB.getToolDefinitions();
  const stream = await openai.chat.completions.create({ 
    model: 'gpt-4.1', 
    messages, 
    tools, 
    temperature: 0.7, 
    max_tokens: 10000,
    stream: true 
  });
  
  let fullContent = '';
  let toolCallsBuffer = [];
  
  /* Stream tokens */
  for await (const chunk of stream) {
    const delta = chunk.choices[0]?.delta;
    
    if (delta?.content) {
      fullContent += delta.content;
      yield { type: 'token', token: delta.content };
    }
    
    if (delta?.tool_calls) {
      for (const tc of delta.tool_calls) {
        if (tc.id) {
          toolCallsBuffer.push({ id: tc.id, index: tc.index, function: { name: tc.function?.name || '', arguments: tc.function?.arguments || '' } });
        } else if (tc.function?.arguments) {
          const existing = toolCallsBuffer.find(t => t.index === tc.index);
          if (existing) {
            existing.function.arguments += tc.function.arguments;
          }
        }
      }
    }
  }
  
  /* Handle tool calls if any */
  if (toolCallsBuffer.length > 0) {
    yield { type: 'tool_calls', toolCalls: toolCallsBuffer };
    
    /* Execute tool calls */
    const toolResults = [];
    for (const tc of toolCallsBuffer) {
      const result = await LIB.executeToolCall(
        { id: tc.id, function: { name: tc.function.name, arguments: tc.function.arguments }, type: 'function' },
        userId,
        trainingProgram,
        mealPlan,
        context
      );
      toolResults.push({ tool_call_id: tc.id, role: 'tool', content: JSON.stringify(result) });
      yield { type: 'tool_result', name: tc.function.name, result };
    }
    
    /* Continue conversation with tool results */
    messages.push({ role: 'assistant', content: fullContent, tool_calls: toolCallsBuffer.map(tc => ({ id: tc.id, type: 'function', function: tc.function })) });
    for (const tr of toolResults) {
      messages.push(tr);
    }
    
    /* Get final response */
    const finalStream = await openai.chat.completions.create({ 
      model: 'gpt-4.1', 
      messages, 
      tools, 
      temperature: 0.7, 
      max_tokens: 10000,
      stream: true 
    });
    
    fullContent = '';
    for await (const chunk of finalStream) {
      const delta = chunk.choices[0]?.delta;
      if (delta?.content) {
        fullContent += delta.content;
        yield { type: 'token', token: delta.content };
      }
    }
  }
  
  /* Yield final completion */
  yield { type: 'complete', content: fullContent };
};
```





## Edge Functions

Edge functions are custom HTTP endpoint handlers that run outside the standard Business API pipeline. Each edge function is paired with an Edge Controller that defines its REST endpoint.


### `reverseBanFn.js`


**Edge Controller:**
- **Path:** `/admin/reverse-ban`
- **Method:** `GET`
- **Login Required:** No


```js
module.exports = async (request) => {
  const COMMON = require('common');
  const { createModerationRecord, getModerationRecordListByQuery } = require('dbLayer');
  /* Validate admin access: check admin token from env or admin session */
  const adminToken = process.env.ADMIN_TOKEN;
  const providedToken = request.headers ? (request.headers['x-admin-token'] || request.headers['X-Admin-Token']) : null;
  const isAdminSession = request.session && ['admin', 'superAdmin'].includes(request.session.roleId);
  if (!isAdminSession && (!adminToken || providedToken !== adminToken)) {
    return { status: 403, message: 'Unauthorized: Admin access required' };
  }
  const userId = request.body ? request.body.userId : null;
  if (!userId) {
    return { status: 400, message: 'userId is required in request body' };
  }
  /* Check if user has a lifetime ban */
  const records = await getModerationRecordListByQuery({ userId });
  const banRelated = records.filter(r => r.action === 'lifetimeBan' || r.action === 'banReversal');
  banRelated.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  if (banRelated.length === 0 || banRelated[0].action !== 'lifetimeBan') {
    return { status: 404, message: 'No active lifetime ban found for this user' };
  }
  /* Create ban reversal record */
  await createModerationRecord({
    userId,
    offenseType: 'admin_reversal',
    action: 'banReversal',
    reason: 'Ban reversed by admin' + (isAdminSession ? ' (' + request.session.email + ')' : ' (via admin token)')
  });
  /* Reactivate user in auth service */
  try {
    await COMMON.sendRestRequest(
      process.env.AUTH_SERVICE_URL ? process.env.AUTH_SERVICE_URL + '/m2m/user/updateById' : 'http://auth:3000/m2m/user/updateById',
      'PUT',
      { id: userId, dataClause: { isActive: true } },
      { 'x-service-token': process.env.M2M_TOKEN || '' }
    );
  } catch (m2mErr) {
    console.error('Failed to reactivate user in auth service:', m2mErr);
    return { status: 500, message: 'Ban reversed in moderation records but failed to reactivate user in auth service' };
  }
  return { status: 200, message: 'Ban reversed successfully', userId };
};
```


### `getUsageAnalyticsFn.js`


**Edge Controller:**
- **Path:** `/admin/usage-analytics`
- **Method:** `GET`
- **Login Required:** No


```js
module.exports = async (request) => {
  /* Validate admin access */
  const adminToken = process.env.ADMIN_TOKEN;
  const providedToken = request.headers ? (request.headers['x-admin-token'] || request.headers['X-Admin-Token']) : null;
  const isAdminSession = request.session && ['admin', 'superAdmin'].includes(request.session.roleId);
  if (!isAdminSession && (!adminToken || providedToken !== adminToken)) {
    return { status: 403, message: 'Unauthorized: Admin access required' };
  }
  const { getChatMessageStatsByQuery, getUserQuotaListByQuery, getQuotaConfigListByQuery } = require('dbLayer');
  try {
    /* Total messages */
    const totalMessages = await getChatMessageStatsByQuery({}, 'count');
    /* Flagged messages */
    const flaggedMessages = await getChatMessageStatsByQuery({ flagged: true }, 'count');
    /* User messages vs assistant messages */
    const userMessages = await getChatMessageStatsByQuery({ role: 'user' }, 'count');
    const assistantMessages = await getChatMessageStatsByQuery({ role: 'assistant' }, 'count');
    /* Quota config */
    const configs = await getQuotaConfigListByQuery({});
    const quotaConfig = configs.length > 0 ? configs[0] : null;
    /* User quotas - top consumers */
    const quotas = await getUserQuotaListByQuery({});
    const sortedQuotas = quotas.sort((a, b) => b.messageCount - a.messageCount).slice(0, 20);
    const totalQuotaUsage = quotas.reduce((sum, q) => sum + (q.messageCount || 0), 0);
    const avgQuotaUsage = quotas.length > 0 ? totalQuotaUsage / quotas.length : 0;
    return {
      status: 200,
      analytics: {
        totalMessages: totalMessages || 0,
        flaggedMessages: flaggedMessages || 0,
        userMessages: userMessages || 0,
        assistantMessages: assistantMessages || 0,
        flagRate: totalMessages > 0 ? ((flaggedMessages || 0) / totalMessages * 100).toFixed(2) + '%' : '0%',
        quotaConfig: quotaConfig ? { limit: quotaConfig.quotaLimit, period: quotaConfig.quotaPeriod } : null,
        activeUsers: quotas.length,
        avgQuotaUsage: Math.round(avgQuotaUsage),
        totalQuotaUsage,
        topConsumers: sortedQuotas.map(q => ({ userId: q.userId, messageCount: q.messageCount }))
      }
    };
  } catch (err) {
    console.error('getUsageAnalyticsFn error:', err);
    return { status: 500, message: 'Failed to retrieve analytics', error: err.message };
  }
};
```





## Edge Controllers Summary

| Function Name | Method | Path | Login Required |
|--------------|--------|------|----------------|
| `getUsageAnalyticsFn` | `GET` | `/admin/usage-analytics` | No |
| `reverseBanFn` | `GET` | `/admin/reverse-ban` | No |









---

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