diff --git a/package.json b/package.json index 17349eadc..dcc849d62 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,7 @@ "test:qfRoundService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/qfRoundService.test.ts", "test:project": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/entities/project.test.ts", "test:projectsTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/projectsTab.test.ts", + "test:projectVerificationTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/projectVerificationTab.test.ts", "test:syncUsersModelScore": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/syncUsersModelScore.test.ts", "test:notifyDonationsWithSegment": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/notifyDonationsWithSegment.test.ts", "test:checkProjectVerificationStatus": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/checkProjectVerificationStatus.test.ts", diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index b29694d8b..8c108cd96 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -417,24 +417,20 @@ export const approveProject = async (params: { return project.save(); }; -export const approveMultipleProjects = async (params: { +export const approveMultipleProjects = async ({ + approved, + projectsIds, +}: { approved: boolean; projectsIds: string[] | number[]; }): Promise => { - if (params.approved) { - await Project.query(` - UPDATE project - SET "verificationStatus" = NULL - WHERE id IN (${params.projectsIds?.join(',')}) - `); - } - return Project.createQueryBuilder('project') - .update(Project, { - isGivbackEligible: params.approved, + .update(Project) + .set({ + isGivbackEligible: approved, + ...(approved && { verificationStatus: null, verified: true }), }) - .where('project.id IN (:...ids)') - .setParameter('ids', params.projectsIds) + .where('project.id IN (:...ids)', { ids: projectsIds }) .returning('*') .updateEntity(true) .execute(); diff --git a/src/server/adminJs/tabs/projectVerificationTab.test.ts b/src/server/adminJs/tabs/projectVerificationTab.test.ts new file mode 100644 index 000000000..d1858a443 --- /dev/null +++ b/src/server/adminJs/tabs/projectVerificationTab.test.ts @@ -0,0 +1,151 @@ +import { assert } from 'chai'; +import { + createProjectData, + saveProjectDirectlyToDb, + SEED_DATA, +} from '../../../../test/testUtils'; +import { PROJECT_VERIFICATION_STATUSES } from '../../../entities/projectVerificationForm'; +import { User } from '../../../entities/user'; +import { + createProjectVerificationForm, + findProjectVerificationFormById, +} from '../../../repositories/projectVerificationRepository'; +import { findUserById } from '../../../repositories/userRepository'; +import { approveVerificationForms } from './projectVerificationTab'; +import { findProjectById } from '../../../repositories/projectRepository'; + +describe( + 'approveGivbacksEligibilityForm() TestCases', + approveGivbacksEligibilityFormTestCases, +); + +function approveGivbacksEligibilityFormTestCases() { + it('Should throw error if Givback Eligibility Form is on draft', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: true, + }); + + const projectVerificationForm = await createProjectVerificationForm({ + projectId: project.id, + userId: project.adminUserId, + }); + + projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.DRAFT; + await projectVerificationForm.save(); + + const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); + + await approveVerificationForms( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(projectVerificationForm.id), + }, + }, + true, + ); + + const updatedForm = await findProjectVerificationFormById( + projectVerificationForm.id, + ); + assert.isOk(updatedForm); + assert.equal(updatedForm?.status, PROJECT_VERIFICATION_STATUSES.DRAFT); + }); + + it('Should be able approve Givback Eligibility Form for not draft form', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: true, + isGivbackEligible: false, + }); + + const projectVerificationForm = await createProjectVerificationForm({ + projectId: project.id, + userId: project.adminUserId, + }); + projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.SUBMITTED; + await projectVerificationForm.save(); + + const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); + + await approveVerificationForms( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(projectVerificationForm.id), + }, + }, + true, + ); + + const updatedForm = await findProjectVerificationFormById( + projectVerificationForm.id, + ); + const updatedProject = await findProjectById(project.id); + assert.isOk(updatedForm); + assert.equal(updatedForm?.status, PROJECT_VERIFICATION_STATUSES.VERIFIED); + assert.isTrue(updatedProject?.isGivbackEligible); + // assert.equal(updatedProject?.verificationFormStatus); + }); + + it('Should be able to reject Givback Eligibility Form for not draft form', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + listed: true, + isGivbackEligible: false, + }); + + const projectVerificationForm = await createProjectVerificationForm({ + projectId: project.id, + userId: project.adminUserId, + }); + projectVerificationForm.status = PROJECT_VERIFICATION_STATUSES.SUBMITTED; + await projectVerificationForm.save(); + + const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); + + await approveVerificationForms( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(projectVerificationForm.id), + }, + }, + false, + ); + + const updatedForm = await findProjectVerificationFormById( + projectVerificationForm.id, + ); + const updatedProject = await findProjectById(project.id); + + assert.isOk(updatedForm); + assert.equal(updatedForm?.status, PROJECT_VERIFICATION_STATUSES.REJECTED); + assert.isFalse(updatedProject?.isGivbackEligible); + }); +} diff --git a/src/server/adminJs/tabs/projectVerificationTab.ts b/src/server/adminJs/tabs/projectVerificationTab.ts index 5ba5d14f1..2a06b67b5 100644 --- a/src/server/adminJs/tabs/projectVerificationTab.ts +++ b/src/server/adminJs/tabs/projectVerificationTab.ts @@ -246,6 +246,25 @@ export const approveVerificationForms = async ( ? PROJECT_VERIFICATION_STATUSES.VERIFIED : PROJECT_VERIFICATION_STATUSES.REJECTED; const formIds = request?.query?.recordIds?.split(','); + + logger.info('approveVerificationForms() formIds', formIds); + + // Preliminary check for DRAFT status + const draftProjects = await ProjectVerificationForm.createQueryBuilder( + 'form', + ) + .where('form.id IN (:...formIds)', { formIds }) + .andWhere('form.status = :status', { + status: PROJECT_VERIFICATION_STATUSES.DRAFT, + }) + .getMany(); + + if (draftProjects.length > 0) { + throw new Error( + `Cannot ${approved ? 'approve' : 'reject'} project${draftProjects.length > 1 ? 's' : ''} in DRAFT status.`, + ); + } + // call repositories const projectsForms = await verifyMultipleForms({ verificationStatus, @@ -261,7 +280,7 @@ export const approveVerificationForms = async ( return project.id; }); - // need to requery them as the RAW is not an entity + // need to re-query them as the RAW is not an entity const verificationForms = await ProjectVerificationForm.createQueryBuilder( 'projectVerificationForm', ) @@ -294,7 +313,7 @@ export const approveVerificationForms = async ( }`; } catch (error) { logger.error('verifyVerificationForm() error', error); - responseMessage = `Bulk verify failed ${error.message}`; + responseMessage = `Bulk verify failed: ${error.message}`; responseType = 'danger'; } return { diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 49083065c..3a936ef09 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -48,11 +48,14 @@ describe( ); describe('verifyProjects() test cases', verifyProjectsTestCases); + describe('listDelist() test cases', listDelistTestCases); + describe( 'addToFeaturedProjectUpdate() TestCases', addToFeaturedProjectUpdateTestCases, ); + describe( 'exportProjectsWithFiltersToCsv() test cases', exportProjectsWithFiltersToCsvTestCases, @@ -63,6 +66,74 @@ describe( updateStatusOfProjectsTestCases, ); +describe('unverifyProjects() test cases', unverifyProjectsTestCases); + +function unverifyProjectsTestCases() { + it('Should unverify project if isGivbacksEligible is false', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + verified: true, + listed: true, + reviewStatus: ReviewStatus.Listed, + isGivbackEligible: false, + }); + const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); + await verifyProjects( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(project.id), + }, + }, + false, + ); + + const updatedProject = await findProjectById(project.id); + assert.isOk(updatedProject); + assert.isFalse(updatedProject?.verified); + assert.isTrue(updatedProject?.listed); + assert.equal(updatedProject?.reviewStatus, ReviewStatus.Listed); + }); + + it('Should not unverify project if isGivbacksEligible is true', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + slug: String(new Date().getTime()), + verified: true, + listed: true, + reviewStatus: ReviewStatus.Listed, + isGivbackEligible: true, + }); + const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); + await verifyProjects( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(project.id), + }, + }, + false, + ); + + const updatedProject = await findProjectById(project.id); + assert.isOk(updatedProject); + assert.isTrue(updatedProject?.verified); + assert.isTrue(updatedProject?.listed); + assert.equal(updatedProject?.reviewStatus, ReviewStatus.Listed); + }); +} + function updateStatusOfProjectsTestCases() { it('should deList and unverified project, when changing status of one project to cancelled', async () => { const project = await saveProjectDirectlyToDb({ @@ -421,8 +492,9 @@ function listDelistTestCases() { ); }); } + function verifyProjectsTestCases() { - it('should unverify projects when the badge is revoked and set verification form as draft', async () => { + it('should not change verification status of projects when the badge is revoked and set verification form as draft', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), @@ -474,7 +546,7 @@ function verifyProjectsTestCases() { project.id, ); assert.isOk(updatedProject); - assert.isFalse(updatedProject?.verified); + assert.isTrue(updatedProject?.verified); assert.isTrue(updatedProject?.listed); assert.equal(updatedProject?.reviewStatus, ReviewStatus.Listed); assert.equal( @@ -496,6 +568,7 @@ function verifyProjectsTestCases() { ); assert.equal(updatedProject?.verificationStatus, RevokeSteps.Revoked); }); + it('should not change listed(true) status when verifying project and set verification form as verified', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), @@ -598,6 +671,7 @@ function verifyProjectsTestCases() { listed: true, reviewStatus: ReviewStatus.Listed, verificationStatus: RevokeSteps.Revoked, + isGivbackEligible: false, }); const projectVerificationForm = await createProjectVerificationForm({ projectId: project.id, @@ -654,6 +728,7 @@ function verifyProjectsTestCases() { verified: true, listed: false, reviewStatus: ReviewStatus.NotListed, + isGivbackEligible: false, }); const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); await verifyProjects( @@ -724,6 +799,7 @@ function verifyProjectsTestCases() { title: String(new Date().getTime()), slug: String(new Date().getTime()), verified: true, + isGivbackEligible: false, }); const adminUser = await findUserById(SEED_DATA.ADMIN_USER.id); await verifyProjects( diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index b4675395c..01db3cb45 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -250,6 +250,18 @@ export const verifyProjects = async ( ?.split(',') ?.map(strId => Number(strId)) as number[]; const projectsBeforeUpdating = await findProjectsByIdArray(projectIds); + + // Check if any project is Givback eligible and vouchedStatus is false\ + if (!vouchedStatus) { + for (const project of projectsBeforeUpdating) { + if (project.isGivbackEligible) { + throw new Error( + `The project with ID ${project.id} is Givback-eligible, so the Vouched badge cannot be revoked.`, + ); + } + } + } + const updateParams = { verified: vouchedStatus }; const projects = await Project.createQueryBuilder('project') @@ -280,6 +292,15 @@ export const verifyProjects = async ( ? HISTORY_DESCRIPTIONS.CHANGED_TO_VERIFIED : HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED, }); + if (vouchedStatus) { + await getNotificationAdapter().projectVerified({ + project: project, + }); + } else { + await getNotificationAdapter().projectUnVerified({ + project: project, + }); + } } await Promise.all([ @@ -287,22 +308,27 @@ export const verifyProjects = async ( refreshProjectPowerView(), refreshProjectFuturePowerView(), ]); + return { + redirectUrl: redirectUrl, + records: records.map(record => record.toJSON(context.currentAdmin)), + notice: { + message: `Project(s) successfully ${ + vouchedStatus ? 'vouched' : 'unvouched' + }`, + type: 'success', + }, + }; } catch (error) { logger.error('verifyProjects() error', error); - throw error; + return { + redirectUrl: redirectUrl, + records: records.map(record => record.toJSON(context.currentAdmin)), + notice: { + message: error.message, + type: 'error', + }, + }; } - return { - redirectUrl: redirectUrl, - records: records.map(record => { - record.toJSON(context.currentAdmin); - }), - notice: { - message: `Project(s) successfully ${ - vouchedStatus ? 'vouched' : 'unvouched' - }`, - type: 'success', - }, - }; }; export const updateStatusOfProjects = async (