diff --git a/src/models/hooks/activityReportGoal.js b/src/models/hooks/activityReportGoal.js index 5b22f0fe62..1552e1402f 100644 --- a/src/models/hooks/activityReportGoal.js +++ b/src/models/hooks/activityReportGoal.js @@ -1,3 +1,4 @@ +const { Op } = require('sequelize'); const { REPORT_STATUSES } = require('@ttahub/common'); const { GOAL_COLLABORATORS } = require('../../constants'); const { @@ -152,6 +153,46 @@ const destroyLinkedSimilarityGroups = async (sequelize, instance, options) => { }); }; +const updateOnARAndOnApprovedARForMergedGoals = async (sequelize, instance) => { + const changed = instance.changed(); + + // Check if both originalGoalId and goalId have been changed and originalGoalId is not null + if (Array.isArray(changed) + && changed.includes('originalGoalId') + && changed.includes('goalId') + && instance.originalGoalId !== null) { + const { goalId } = instance; + + // Check if the ActivityReport linked to this ActivityReportGoal has a + // calculatedStatus of 'approved' + const approvedActivityReports = await sequelize.models.ActivityReport.count({ + where: { + calculatedStatus: 'approved', + id: instance.activityReportId, // Use the activityReportId from the instance + }, + }); + + const onApprovedAR = approvedActivityReports > 0; + + // Update only if the current values differ + await sequelize.models.Goal.update( + { onAR: true, onApprovedAR }, + { + where: { + id: goalId, + [Op.or]: [ + // Update if onAR is not already true + { onAR: { [Op.ne]: true } }, + // Update if onApprovedAR differs + { onApprovedAR: { [Op.ne]: onApprovedAR } }, + ], + }, + individualHooks: true, // Ensure individual hooks are triggered + }, + ); + } +}; + const afterCreate = async (sequelize, instance, options) => { await processForEmbeddedResources(sequelize, instance, options); await autoPopulateLinker(sequelize, instance, options); @@ -177,6 +218,7 @@ const afterDestroy = async (sequelize, instance, options) => { const afterUpdate = async (sequelize, instance, options) => { await processForEmbeddedResources(sequelize, instance, options); await destroyLinkedSimilarityGroups(sequelize, instance, options); + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance); }; export { @@ -185,6 +227,7 @@ export { recalculateOnAR, propagateDestroyToMetadata, destroyLinkedSimilarityGroups, + updateOnARAndOnApprovedARForMergedGoals, afterCreate, beforeDestroy, afterDestroy, diff --git a/src/models/hooks/activityReportGoal.test.js b/src/models/hooks/activityReportGoal.test.js index 882bf46506..f4db5f3778 100644 --- a/src/models/hooks/activityReportGoal.test.js +++ b/src/models/hooks/activityReportGoal.test.js @@ -1,5 +1,9 @@ +const { Op } = require('sequelize'); // Import Sequelize operators const { REPORT_STATUSES } = require('@ttahub/common'); -const { destroyLinkedSimilarityGroups } = require('./activityReportGoal'); +const { + destroyLinkedSimilarityGroups, + updateOnARAndOnApprovedARForMergedGoals, +} = require('./activityReportGoal'); describe('destroyLinkedSimilarityGroups', () => { afterEach(() => { @@ -205,3 +209,174 @@ describe('destroyLinkedSimilarityGroups', () => { expect(sequelize.models.GoalSimilarityGroup.destroy).not.toHaveBeenCalled(); }); }); + +describe('updateOnARAndOnApprovedARForMergedGoals', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + const sequelize = { + models: { + Goal: { + update: jest.fn(), + }, + ActivityReport: { + count: jest.fn(), + }, + }, + }; + + it('should update onAR and onApprovedAR for merged goals when originalGoalId and goalId are changed', async () => { + const instance = { + goalId: 1, + originalGoalId: 2, + activityReportId: 1, + changed: () => ['originalGoalId', 'goalId'], // Simulate that both columns have changed + }; + + const options = { + transaction: 'mockTransaction', + }; + + // Mock the necessary Sequelize methods + // Simulate approved ActivityReport exists + sequelize.models.ActivityReport.count.mockResolvedValue(1); + + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance, options); + + expect(sequelize.models.ActivityReport.count).toHaveBeenCalledWith({ + where: { + calculatedStatus: 'approved', + id: instance.activityReportId, + }, + }); + + expect(sequelize.models.Goal.update).toHaveBeenCalledWith( + { onAR: true, onApprovedAR: true }, + { + where: { + id: instance.goalId, + [Op.or]: [ + // Ensure onAR condition is in the where clause + { onAR: { [Op.ne]: true } }, + // Ensure onApprovedAR condition is in the where clause + { onApprovedAR: { [Op.ne]: true } }, + ], + }, + individualHooks: true, + }, + ); + }); + + it('should update onAR and onApprovedAR with false when there are no approved activity reports', async () => { + const instance = { + goalId: 1, + originalGoalId: 2, + activityReportId: 1, + changed: () => ['originalGoalId', 'goalId'], // Simulate that both columns have changed + }; + + const options = { + transaction: 'mockTransaction', + }; + + // Mock the necessary Sequelize methods + sequelize.models.ActivityReport.count.mockResolvedValue(0); // No approved ActivityReports + + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance, options); + + expect(sequelize.models.ActivityReport.count).toHaveBeenCalledWith({ + where: { + calculatedStatus: 'approved', + id: instance.activityReportId, + }, + }); + + expect(sequelize.models.Goal.update).toHaveBeenCalledWith( + { onAR: true, onApprovedAR: false }, // onApprovedAR is false since no approved reports + { + where: { + id: instance.goalId, + [Op.or]: [ + // Ensure onAR condition is in the where clause + { onAR: { [Op.ne]: true } }, + // Ensure onApprovedAR condition is in the where clause + { onApprovedAR: { [Op.ne]: false } }, + ], + }, + individualHooks: true, + }, + ); + }); + + it('should not update if originalGoalId or goalId is not changed', async () => { + const instance = { + goalId: 1, + originalGoalId: 2, + activityReportId: 1, + changed: () => [], // Simulate no changes + }; + + const options = { + transaction: 'mockTransaction', + }; + + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance, options); + + expect(sequelize.models.ActivityReport.count).not.toHaveBeenCalled(); + expect(sequelize.models.Goal.update).not.toHaveBeenCalled(); + }); + + it('should not update if originalGoalId is null', async () => { + const instance = { + goalId: 1, + originalGoalId: null, + activityReportId: 1, + changed: () => ['originalGoalId', 'goalId'], // Simulate that both columns have changed + }; + + const options = { + transaction: 'mockTransaction', + }; + + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance, options); + + expect(sequelize.models.ActivityReport.count).not.toHaveBeenCalled(); + expect(sequelize.models.Goal.update).not.toHaveBeenCalled(); + }); + + it('should not update if onAR and onApprovedAR are already set to the correct values', async () => { + const instance = { + goalId: 1, + originalGoalId: 2, + activityReportId: 1, + changed: () => ['originalGoalId', 'goalId'], // Simulate that both columns have changed + }; + + const options = { + transaction: 'mockTransaction', + }; + + // Mock the necessary Sequelize methods + // Simulate approved ActivityReport exists + sequelize.models.ActivityReport.count.mockResolvedValue(1); + // Simulate that the update doesn't happen + sequelize.models.Goal.update.mockResolvedValue(0); + + await updateOnARAndOnApprovedARForMergedGoals(sequelize, instance, options); + + expect(sequelize.models.Goal.update).toHaveBeenCalledWith( + { onAR: true, onApprovedAR: true }, + { + where: { + id: instance.goalId, + [Op.or]: [ + { onAR: { [Op.ne]: true } }, // Check if onAR is already true + { onApprovedAR: { [Op.ne]: true } }, // Check if onApprovedAR is already true + ], + }, + individualHooks: true, + }, + ); + }); +});