diff --git a/src/controllers/educationPortal/browsableLessonPlansController.js b/src/controllers/educationPortal/browsableLessonPlansController.js index 2be6c85758..fac188040d 100644 --- a/src/controllers/educationPortal/browsableLessonPlansController.js +++ b/src/controllers/educationPortal/browsableLessonPlansController.js @@ -1,12 +1,21 @@ const mongoose = require('mongoose'); const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfile) { - const ALLOWED_SORT_FIELDS = new Set(['createdAt', 'updatedAt', 'title', 'difficulty', 'popularity']); + const ALLOWED_SORT_FIELDS = new Set([ + 'createdAt', + 'updatedAt', + 'title', + 'difficulty', + 'popularity', + ]); function parseArrayParam(val) { if (!val) return undefined; if (Array.isArray(val)) return val; - return String(val).split(',').map((s) => s.trim()).filter(Boolean); + return String(val) + .split(',') + .map((s) => s.trim()) + .filter(Boolean); } const getLessonPlans = async (req, res) => { @@ -33,7 +42,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil // Handle both singular and plural subject params const subjectList = parseArrayParam(subjects || subject); if (subjectList && subjectList.length) { - filter.subjects = { $in: subjectList.map(s => new RegExp(s, 'i')) }; + filter.subjects = { $in: subjectList.map((s) => new RegExp(s, 'i')) }; } if (difficulty) { @@ -44,7 +53,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil // Handle both singular and plural tag params const tagList = parseArrayParam(tags || tag); if (tagList && tagList.length) { - filter.tags = { $in: tagList.map(t => new RegExp(t, 'i')) }; + filter.tags = { $in: tagList.map((t) => new RegExp(t, 'i')) }; } if (author && mongoose.Types.ObjectId.isValid(author)) { @@ -64,8 +73,10 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil // Enhanced search with text index or regex fallback if (search) { - const hasTextIndex = await BrowsableLessonPlan.collection.indexExists('title_text_description_text_content_text_tags_text'); - + const hasTextIndex = await BrowsableLessonPlan.collection.indexExists( + 'title_text_description_text_content_text_tags_text', + ); + if (hasTextIndex) { filter.$text = { $search: search }; } else { @@ -75,7 +86,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil { description: regex }, { content: regex }, { tags: regex }, - { subjects: regex } + { subjects: regex }, ]; } } @@ -136,14 +147,16 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil }); } catch (error) { console.error('Error getting lesson plans:', error); - return res.status(500).json({ success: false, error: 'Failed to fetch lesson plans', details: error.message }); + return res + .status(500) + .json({ success: false, error: 'Failed to fetch lesson plans', details: error.message }); } }; const getLessonPlanById = async (req, res) => { try { const { id } = req.params; - + if (!mongoose.Types.ObjectId.isValid(id)) { return res.status(400).json({ success: false, error: 'Invalid lesson plan ID' }); } @@ -151,7 +164,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil const lessonPlan = await BrowsableLessonPlan.findByIdAndUpdate( id, { $inc: { views: 1 } }, - { new: true } + { new: true }, ) .populate('author', 'firstName lastName profilePic email') .lean(); @@ -170,21 +183,28 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil const saveStudentInterest = async (req, res) => { try { const { studentId, lessonPlanId } = req.body; - + if (!studentId || !lessonPlanId) { - return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' }); + return res + .status(400) + .json({ success: false, error: 'studentId and lessonPlanId are required' }); } - - if (!mongoose.Types.ObjectId.isValid(studentId) || !mongoose.Types.ObjectId.isValid(lessonPlanId)) { + + if ( + !mongoose.Types.ObjectId.isValid(studentId) || + !mongoose.Types.ObjectId.isValid(lessonPlanId) + ) { return res.status(400).json({ success: false, error: 'Invalid ID format' }); } const lesson = await BrowsableLessonPlan.findByIdAndUpdate( lessonPlanId, { $inc: { savedCount: 1 } }, - { new: true } - ).select('_id title').lean(); - + { new: true }, + ) + .select('_id title') + .lean(); + if (!lesson) { return res.status(404).json({ success: false, error: 'Lesson plan not found' }); } @@ -198,7 +218,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil .populate({ path: 'educationProfiles.student.savedInterests', select: 'title description subjects difficulty tags thumbnail createdAt savedCount views', - populate: { path: 'author', select: 'firstName lastName' } + populate: { path: 'author', select: 'firstName lastName' }, }); if (!updated) { @@ -212,7 +232,9 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil }); } catch (error) { console.error('Error saving student interest:', error); - return res.status(500).json({ success: false, error: 'Failed to save interest', details: error.message }); + return res + .status(500) + .json({ success: false, error: 'Failed to save interest', details: error.message }); } }; @@ -220,19 +242,21 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil try { const { lessonPlanId } = req.params; const studentId = req.query.studentId || req.body.studentId; - + if (!studentId || !lessonPlanId) { - return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' }); + return res + .status(400) + .json({ success: false, error: 'studentId and lessonPlanId are required' }); } - - if (!mongoose.Types.ObjectId.isValid(studentId) || !mongoose.Types.ObjectId.isValid(lessonPlanId)) { + + if ( + !mongoose.Types.ObjectId.isValid(studentId) || + !mongoose.Types.ObjectId.isValid(lessonPlanId) + ) { return res.status(400).json({ success: false, error: 'Invalid ID format' }); } - await BrowsableLessonPlan.findByIdAndUpdate( - lessonPlanId, - { $inc: { savedCount: -1 } } - ); + await BrowsableLessonPlan.findByIdAndUpdate(lessonPlanId, { $inc: { savedCount: -1 } }); const updated = await UserProfile.findByIdAndUpdate( studentId, @@ -243,7 +267,7 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil .populate({ path: 'educationProfiles.student.savedInterests', select: 'title description subjects difficulty tags thumbnail createdAt savedCount views', - populate: { path: 'author', select: 'firstName lastName' } + populate: { path: 'author', select: 'firstName lastName' }, }); if (!updated) { @@ -257,18 +281,20 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil }); } catch (error) { console.error('Error removing saved interest:', error); - return res.status(500).json({ success: false, error: 'Failed to remove interest', details: error.message }); + return res + .status(500) + .json({ success: false, error: 'Failed to remove interest', details: error.message }); } }; const getStudentSavedInterests = async (req, res) => { try { const studentId = req.query.studentId || req.headers.studentid || req.body.studentId; - + if (!studentId) { return res.status(400).json({ success: false, error: 'studentId is required' }); } - + if (!mongoose.Types.ObjectId.isValid(studentId)) { return res.status(400).json({ success: false, error: 'Invalid studentId format' }); } @@ -277,8 +303,9 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil .select('educationProfiles.student.savedInterests firstName lastName') .populate({ path: 'educationProfiles.student.savedInterests', - select: 'title description subjects difficulty tags thumbnail createdAt updatedAt savedCount views', - populate: { path: 'author', select: 'firstName lastName profilePic' } + select: + 'title description subjects difficulty tags thumbnail createdAt updatedAt savedCount views', + populate: { path: 'author', select: 'firstName lastName profilePic' }, }) .lean(); @@ -298,28 +325,32 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil }); } catch (error) { console.error('Error fetching saved interests:', error); - return res.status(500).json({ success: false, error: 'Failed to fetch saved interests', details: error.message }); + return res + .status(500) + .json({ success: false, error: 'Failed to fetch saved interests', details: error.message }); } }; const checkIfSaved = async (req, res) => { try { const { studentId, lessonPlanId } = req.query; - + if (!studentId || !lessonPlanId) { - return res.status(400).json({ success: false, error: 'studentId and lessonPlanId are required' }); + return res + .status(400) + .json({ success: false, error: 'studentId and lessonPlanId are required' }); } const user = await UserProfile.findById(studentId) .select('educationProfiles.student.savedInterests') .lean(); - + if (!user) { return res.status(404).json({ success: false, error: 'Student not found' }); } const savedInterests = user.educationProfiles?.student?.savedInterests || []; - const isSaved = savedInterests.some(id => id.toString() === lessonPlanId); + const isSaved = savedInterests.some((id) => id.toString() === lessonPlanId); return res.status(200).json({ success: true, isSaved }); } catch (error) { @@ -338,4 +369,4 @@ const browsableLessonPlansController = function (BrowsableLessonPlan, UserProfil }; }; -module.exports = browsableLessonPlansController; \ No newline at end of file +module.exports = browsableLessonPlansController; diff --git a/src/helpers/userHelper.js b/src/helpers/userHelper.js index 5786fff263..e7eab4c840 100644 --- a/src/helpers/userHelper.js +++ b/src/helpers/userHelper.js @@ -534,8 +534,6 @@ const userHelper = function () { const assignBlueSquareForTimeNotMet = async () => { try { const currentFormattedDate = moment().tz('America/Los_Angeles').format(); - moment.tz('America/Los_Angeles').startOf('day').toISOString(); - logger.logInfo( `Job for assigning blue square for commitment not met starting at ${currentFormattedDate}`, ); @@ -544,7 +542,6 @@ const userHelper = function () { .tz('America/Los_Angeles') .startOf('week') .subtract(1, 'week'); - const pdtEndOfLastWeek = moment().tz('America/Los_Angeles').endOf('week').subtract(1, 'week'); const users = await userProfile.find( @@ -1015,6 +1012,7 @@ const userHelper = function () { } }; + // Function to calculate carry-forward hours for Core Team const applyMissedHourForCoreTeam = async () => { try { const currentDate = moment().tz('America/Los_Angeles').format(); @@ -1137,6 +1135,20 @@ const userHelper = function () { } }; + // Email body generator for Core Team notifications + const generateCoreTeamEmailBody = ( + user, + infringement, + additionalHours, + ) => `

Dear ${user.firstName},

+

You have received a new blue square for not meeting the required hours.

+

Infringement details:

+

${infringement.description}

+

Additional hours for next week: ${additionalHours} hours.

+

Thank you for your continued dedication.

+

Best regards,

+

One Community

`; + const deleteBlueSquareAfterYear = async () => { const nowLA = moment().tz('America/Los_Angeles'); diff --git a/src/models/educationPortal/browsableLessonPlanModel.js b/src/models/educationPortal/browsableLessonPlanModel.js index c414e58013..ec5795b107 100644 --- a/src/models/educationPortal/browsableLessonPlanModel.js +++ b/src/models/educationPortal/browsableLessonPlanModel.js @@ -26,7 +26,7 @@ const BrowsableLessonPlanSchema = new mongoose.Schema( materials: [{ type: String }], metadata: { type: mongoose.Schema.Types.Mixed }, }, - { timestamps: true } + { timestamps: true }, ); // Text index for search functionality @@ -44,4 +44,8 @@ BrowsableLessonPlanSchema.index({ featured: 1, createdAt: -1 }); BrowsableLessonPlanSchema.index({ savedCount: -1 }); BrowsableLessonPlanSchema.index({ views: -1 }); -module.exports = mongoose.model('BrowsableLessonPlan', BrowsableLessonPlanSchema, 'browsableLessonPlans'); \ No newline at end of file +module.exports = mongoose.model( + 'BrowsableLessonPlan', + BrowsableLessonPlanSchema, + 'browsableLessonPlans', +); diff --git a/src/routes/educationPortal/browsableLessonPlanRouter.js b/src/routes/educationPortal/browsableLessonPlanRouter.js index 7dd13b3131..11ffe6b508 100644 --- a/src/routes/educationPortal/browsableLessonPlanRouter.js +++ b/src/routes/educationPortal/browsableLessonPlanRouter.js @@ -16,4 +16,4 @@ module.exports = function (BrowsableLessonPlanModel, UserProfileModel) { router.get('/student/saved-interests/check', controller.checkIfSaved); return router; -}; \ No newline at end of file +}; diff --git a/src/test/factories/testTimeEntryBuilder.js b/src/test/factories/testTimeEntryBuilder.js new file mode 100644 index 0000000000..151a8d5419 --- /dev/null +++ b/src/test/factories/testTimeEntryBuilder.js @@ -0,0 +1,26 @@ +const TimeEntry = require('../../models/timeentry'); + +class TimeEntryBuilder { + constructor() { + this.data = []; + return this; + } + addEntry(userId, dateOfWork, totalHours, isTangible = true, entryType = 'task') { + this.data.push({ + personId: userId, + dateOfWork: dateOfWork, + isTangible: isTangible, + entryType: entryType, + totalSeconds: totalHours * 3600, + createdDateTime: new Date(), + isActive: true, + }); + return this; + } + async buildAndSave() { + const entries = await TimeEntry.create(this.data); + return entries; + } +} + +module.exports = TimeEntryBuilder; \ No newline at end of file diff --git a/src/test/factories/testUserBuilder.js b/src/test/factories/testUserBuilder.js new file mode 100644 index 0000000000..760fe9803a --- /dev/null +++ b/src/test/factories/testUserBuilder.js @@ -0,0 +1,103 @@ +const UserProfile = require('../../models/userProfile'); +const TimeEntry = require('../../models/timeentry'); +const createUser = require('../db/createUser'); + +const moment = require('moment-timezone'); + +class UserBuilder { + /** + * Default user: + * 1. Core Team + * 2. Created 2 weeks ago + * 3. Weekly committed hours: 10 + * 4. Missed hours: 0 + */ + constructor() { + this.now = moment.tz('America/Los_Angeles'); + + this.data = { + infringements: [], + weeklycommittedHours: 10, + role: 'Core Team', + missedHours: 0, + startDate: this.now.clone().subtract(2, 'weeks').toDate(), + weeklySummaries: [], + infringements: [], + }; + + return this; + } + + asCoreTeam() { + this.data.role = 'Core Team'; + return this; + } + + asVolunteer() { + this.data.role = 'Volunteer'; + return this; + } + + withCommittedHours(hours) { + this.data.weeklycommittedHours = hours; + return this; + } + + withStartDate(date) { + this.data.startDate = date; + return this; + } + + withInfringements(number) { + this.data.infringements = Array(number) + .fill() + .map((_, i) => ({ + date: this.now.clone().subtract(i + 1, 'weeks').toDate(), + description: `Infringement ${i + 1}`, + })); + return this; + } + + withMissessedHours(hours) { + this.data.missedHours = hours; + return this; + } + + withWeeklySummary(text) { + this.data.weeklySummaries.push({ + dueDate: this.now.clone().endOf('week').toDate(), + summary: text, + uploadDate: this.now.clone().toDate() + }); + return this; + } + + override(fields) { + Object.assign(this.data, fields); + return this; + } + + async buildAndSave() { + // Create fresh user + const user = await createUser(); + + // Apply all builder fields directly to the user instance + for (const [key, value] of Object.entries(this.data)) { + user[key] = value; + } + + await user.save(); + return user; + } + + /** + * Build an unsaved user object + */ + async build() { + const user = await createUser(); + Object.assign(user, this.data); + return user; + } +} + +module.exports = UserBuilder; diff --git a/src/test/timeNotMetCoreTeamTest.test.js b/src/test/timeNotMetCoreTeamTest.test.js new file mode 100644 index 0000000000..bfc67482a6 --- /dev/null +++ b/src/test/timeNotMetCoreTeamTest.test.js @@ -0,0 +1,275 @@ +/* eslint-disable */ +const { MongoMemoryServer } = require('mongodb-memory-server'); +const mongoose = require('mongoose'); +const moment = require('moment-timezone'); +const UserBuilder = require("./factories/testUserBuilder"); +const TimeEntryBuilder = require("./factories/testTimeEntryBuilder"); + +// Import models first +const UserProfile = require('../models/userProfile'); +const TimeEntry = require('../models/timeentry'); +const createUser = require('./db/createUser'); + +const userHelper = require('../helpers/userHelper')(); + +const WEEKLY_SUMMARY = "Lorem ipsum dolor sit amet consectetur adipiscing elit quisque faucibus ex sapien vitae pellentesque sem placerat in id cursus mi pretium tellus duis convallis tempus leo eu aenean sed diam urna tempor pulvinar vivamus fringilla lacus nec metus bibendum egestas iaculis massa nisl malesuada lacinia integer nunc posuere ut hendrerit semper vel class aptent taciti." +// Mock other dependencies +jest.mock('../helpers/dashboardhelper'); +jest.mock('../utilities/emailSender'); +jest.mock('../startup/logger'); + +describe('Time Not Met Core Team Test', () => { + let mongoServer; + let userProfileModel; + let timeEntryModel; + let realDate; + + let now, lastWeekStart, lastWeekEnd; + + beforeAll(async () => { + mongoServer = await MongoMemoryServer.create(); + await mongoose.connect(mongoServer.getUri()); + + // Initialize models + userProfileModel = UserProfile; + timeEntryModel = TimeEntry; + }); + + beforeEach(async () => { + await UserProfile.deleteMany({}); + await TimeEntry.deleteMany({}); + const sundayPST = moment.tz('2025-11-09 00:10:00', 'America/Los_Angeles'); + realDate = Date.now; + global.Date.now = jest.fn(() => sundayPST.valueOf()); + + now = moment.tz('America/Los_Angeles'); + lastWeekStart = now.clone().startOf('week').subtract(1, 'week'); + lastWeekEnd = now.clone().endOf('week').subtract(1, 'week'); + }); + + afterEach(() => { + global.Date.now = realDate; + }); + + afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + }); + + + describe('Less than 5 Blue Squares', () => { + it('should not assign blue square if no missed hours', async () => { + // 1. Create test user + + const user = await new UserBuilder().withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // 2. Create test entries with properly formatted string dates + + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(1, 'day').format('YYYY-MM-DD'), // Monday + 4 // 4 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(3, 'days').format('YYYY-MM-DD'), // Wednesday + 4 // 4 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(5, 'days').format('YYYY-MM-DD'), // Friday + 2 // 2 hours + ).buildAndSave(); + + // 4. Final assertions + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + const updatedUser = await UserProfile.findById(user._id); + + expect(updatedUser.infringements.length).toBe(0); + expect(updatedUser.missedHours).toBe(0); + }); + + it('should assign first blue square when 2 hours missed (8/10)', async () => { + // 1. Create test user with no existing infringements + + const user = await new UserBuilder().withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // 2. Create time entries totaling 8 hours (2 hours short) + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(1, 'day').format('YYYY-MM-DD'), // Monday + 3 // 3 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(3, 'days').format('YYYY-MM-DD'), // Wednesday + 5 // 5 hours + ).buildAndSave(); + + // 3. Execute the blue square assignment + await userHelper.applyMissedHourForCoreTeam(); + await userHelper.assignBlueSquareForTimeNotMet(); + + // 4. Verify results + const updatedUser = await UserProfile.findById(user._id); + + // Should have exactly 1 new infringement (blue square) + expect(updatedUser.infringements.length).toBe(1); + // Should show 2 missed hours (10 committed - 8 worked) + expect(updatedUser.missedHours).toBe(2); + }); + + }); + + describe('More than 5 Blue Squares ', () => { + it('should maintain 6 infringements and 0 missed hours when logging exact committed hours', async () => { + // 6 existing infringements + const user = await new UserBuilder().withInfringements(6).withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // 10 hours (2 entries of 5 hours) - exactly meets commitment + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(1, 'day').format('YYYY-MM-DD'), + 5 // 5 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(3, 'days').format('YYYY-MM-DD'), + 5 // 5 hours + ).buildAndSave(); + + // Execute + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + // Verify + const updatedUser = await UserProfile.findById(user._id); + // did not miss any hours so nothing changes + expect(updatedUser.infringements.length).toBe(6); + expect(updatedUser.missedHours).toBe(0); + }); + + it('should only carry forward missed hours with a penalty hours', async () => { + // Create user with 6 existing blue squares and 3 missed hours + + const user = await new UserBuilder().withInfringements(6).withMissessedHours(3).withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // Log only 8 hours this week (2 hours short) + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(2, 'day').format('YYYY-MM-DD'), + 4 // 4 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(4, 'days').format('YYYY-MM-DD'), + 4 // 4 hours + ).buildAndSave(); + + // Execute the functions + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + // Verify Week 6 results + const updatedUser = await UserProfile.findById(user._id); + + // Should have 7 infringements (added new one for this week) + expect(updatedUser.infringements.length).toBe(7); + + // Should have 6 missed hours (3 previous + 2 new + 1 penalty (more than 5 blue squares)) + expect(updatedUser.missedHours).toBe(6); + }); + + it('should only add missed hours (no penalty) for 5th blue square', async () => { + // 1. Create user with 4 Blue Squares + const user = await new UserBuilder().withInfringements(4).withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // Log only 7 hours (3 hours short) + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(2, 'day').format('YYYY-MM-DD'), + 3 // 3 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(4, 'days').format('YYYY-MM-DD'), + 4 // 4 hours + ).buildAndSave(); + + // Execute the functions + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + // Verify results + const updatedUser = await UserProfile.findById(user._id); + + // Should have exactly 5 infringements (added the 5th) + expect(updatedUser.infringements.length).toBe(5); + + // Should only carry forward missed hours (no penalty yet) + // Missed hours: 10 - 7 = 3 + expect(updatedUser.missedHours).toBe(3); + }); + }); + + describe('Edge Cases', () => { + it('should handle zero committed hours scenario', async () => { + // User with 0 committed hours shouldn't get infringements + const user = await new UserBuilder().withCommittedHours(0).withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + const updatedUser = await UserProfile.findById(user._id); + expect(updatedUser.infringements.length).toBe(0); + expect(updatedUser.missedHours).toBe(0); + }); + + it('should handle user with no time entries at all', async () => { + // User with no time entries should get full missed hours + const user = await new UserBuilder().withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + // No time entries + + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + const updatedUser = await UserProfile.findById(user._id); + expect(updatedUser.infringements.length).toBe(1); + expect(updatedUser.missedHours).toBe(10); + }); + + it('should not assign additional hours for non-Core Team members', async () => { + const user = await new UserBuilder().withWeeklySummary(WEEKLY_SUMMARY).asVolunteer().buildAndSave(); + + // No time entries = would normally trigger blue square + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + const updatedUser = await UserProfile.findById(user._id); + expect(updatedUser.infringements.length).toBe(1); + expect(updatedUser.missedHours).toBe(0); // Non-Core Team doesn't accumulate missed hours + }); + + + it('should not assign penalty hours when blue squares > 5 but time entries meet committed hours', async () => { + // Create user with 5 existing blue squares + const user = await new UserBuilder().withInfringements(6).withWeeklySummary(WEEKLY_SUMMARY).buildAndSave(); + + const timeEntryBuilder = await new TimeEntryBuilder().addEntry( + user._id, + lastWeekStart.clone().add(1, 'day').format('YYYY-MM-DD'), + 5 // 5 hours + ).addEntry( + user._id, + lastWeekStart.clone().add(3, 'days').format('YYYY-MM-DD'), + 5 // 5 hours + ).buildAndSave(); + + await userHelper.assignBlueSquareForTimeNotMet(); + await userHelper.applyMissedHourForCoreTeam(); + + const updatedUser = await UserProfile.findById(user._id); + + // Should not get a new blue square since hours were met + expect(updatedUser.infringements.length).toBe(6); + expect(updatedUser.missedHours).toBe(0); + }); + }); +});