import { Injectable } from '@angular/core'; import { GCMockModule } from '@core/mocks/gc-module.mock'; import { ApplicationFromPaginated } from '@core/typings/application.typing'; import { FundingSourceTypes } from '@core/typings/budget.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { ApplicationStatuses } from '@core/typings/status.typing'; import { WorkflowManagerActions } from '@core/typings/workflow.typing'; import { UpdateProgramModalResponse } from '@features/application-manager/update-program-modal/update-program-modal.component'; import { BulkApproveAwardPayPayload } from '@features/awards/typings/award.typing'; import { AutoTableRepository } from '@yourcause/common'; import { AfterEach, BeforeEach, Spec, TestCase } from '@yourcause/test-decorators'; import { DescribeAngularService } from '@yourcause/test-decorators/angular'; import { expect, spy } from 'chai'; import { OrganizationEligibleForGivingStatus } from 'frontend-common/src/typings/search.typing'; import { ApplicationActionService } from './application-actions.service'; @Injectable({ providedIn: 'root' }) @DescribeAngularService(ApplicationActionService, { imports: [ GCMockModule ] }) export class ApplicationActionServiceSpec implements Spec { appId = 4563; grantProgramCycle = { id: 23, name: 'Cycle name', startDate: '', endDate: '', grantProgramId: 235, isArchived: false, clientOrganizationsProcessingTypeId: ProcessingTypes.YourCause, isClientProcessing: false }; workflowId = 353; orgId = 3; application: ApplicationFromPaginated = { reservedFunds: false, applicationId: this.appId, applicantId: 56, applicantFirstName: '', applicantLastName: '', applicationLastName: '', applicantFullName: '', applicantEmail: '', applicantAddress: '', applicantAddressInfo: null, applicantPhoneNumber: '', applicantProfileImageUrl: '', organizationId: 86, organizationName: '', nonprofitGuid: '', organizationIdentification: '', organizationImageUrl: '', organizationAddressInfo: null, orgPhoneNumber: '', programId: 2352, programName: '', submittedDate: '', applicationStatus: ApplicationStatuses.Approved, isApplicationArchived: true, workflowId: this.workflowId, workflowName: '', currentWorkflowLevelId: 246, currentWorkflowLevelName: '', createdBy: null, lastUpdatedBy: null, dateCreated: '', amountRequested: 500, nominatedBy: null, nominationApplicationId: null, dateAddedToWorkflow: '', daysInWfl: 5, paymentStats: null, canArchive: true, canViewMaskedApplicantInfo: true, isMasked: true, isDraft: false, programAllowCollaboration: true, currencyRequested: '', currencyRequestedAmountEquivalent: 0, inKindItemTotalCount: 0, inKindAmountRequested: 0, isEdVetted: true, isArchived: true, isProgramArchived: true, clientOrganizationsProcessingTypeId: ProcessingTypes.YourCause, grantProgramCycle: this.grantProgramCycle, budgetId: null, fundingSourceId: null, careOf: null, organizationEligibleForGivingStatus: OrganizationEligibleForGivingStatus.ELIGIBLE, recommendedFundingAmount: 0, orgIsInternational: false, parentOrganizationName: '', parentOrganizationGuid: '', organizationNPPIsActive: true, updatedDate: '', grantProgramCycleId: this.grantProgramCycle.id, grantProgramCycleName: 'cycle name', designation: '' }; awardDate = '07/24/2021'; approveAwardPayload: BulkApproveAwardPayPayload = { applications: [{ applicationId: this.appId, awards: [{ sendEmail: true, clientEmailTemplateId: 0, emailOptions: null, awardType: FundingSourceTypes.DOLLARS, awardAmount: 1000, currencyRequested: 'USD', amountEquivalent: 100, currencyExchangeRate: 1, inKindAwardItemsRequested: [], description: '', awardDate: this.awardDate, customMessage: '', payment: null }] }], cashBudgetId: 1, cashFundingSourceId: 2, inKindBudgetId: 3, inKindFundingSourceId: 4, customMessage: '', paymentTagIds: [] }; reviewerRecommendedAmount = 346; @BeforeEach() mock (service: ApplicationActionService) { service['workflowService']['workflowResources'].getMyWorkflowManagerRoles = async () => { return [{ workflowId: this.workflowId, workflowActions: [ WorkflowManagerActions.Approve, WorkflowManagerActions.NotifyOfStatus, WorkflowManagerActions.AddTags, WorkflowManagerActions.UpdateProgram ] }]; }; service['workflowService'].setMyWorkflowManagerRolesMap(); service['applicationActionResources'].approveApplication = async () => { return { automaticallyRouted: true }; }; service['applicationActionResources'].declineApplication = async () => { return { automaticallyRouted: true }; }; service['applicationActionResources'].bulkApproveAwardPay = async () => {}; service['applicationActionResources'].bulkRouteApplications = async () => {}; service['applicationActionResources'].cancelApplication = async () => {}; service['applicationActionResources'].setRecommendedFundingAmount = async () => { return { automaticallyRouted: true }; }; service['applicationActionResources'].routeApplication = async () => {}; service['applicationActionResources'].archiveApplication = async () => {}; service['applicationActionResources'].deleteApplication = async () => {}; service['applicationActionResources'].sendApplicationStatusEmail = async () => {}; service['applicationActionResources'].changeApplicationStatus = async () => {}; service['applicationActionResources'].updateProgram = async () => {}; service['policyService'].grantApplication.canManageAllApplications = () => { return false; }; service['policyService'].grantApplication.canTakeActionsOnAllApps = () => { return false; }; } @AfterEach() restore () { spy.restore(); } @TestCase('should be able to handle approve decline modal - approve') async handleApproveModal (service: ApplicationActionService) { let calledAutoRouteFunc = false; let resetMyWorkspace = false; service['showAutomaticallyRoutedToaster'] = () => { calledAutoRouteFunc = true; }; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; await service.handleApproveDeclineModal( 'Approve', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, false, false ); expect(calledAutoRouteFunc).to.be.true; expect(resetMyWorkspace).to.be.true; } @TestCase('should be able to handle approve decline modal - approve by manager') async handleApproveModalAppManager (service: ApplicationActionService) { let calledAutoRouteFunc = false; let resetMyWorkspace = false; service['showAutomaticallyRoutedToaster'] = () => { calledAutoRouteFunc = true; }; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; await service.handleApproveDeclineModal( 'Approve', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, true, false ); expect(calledAutoRouteFunc).to.be.false; expect(resetMyWorkspace).to.be.true; } @TestCase('should be able to handle approve decline modal - decline') async handleDeclineModal (service: ApplicationActionService) { let calledAutoRouteFunc = false; let resetMyWorkspace = false; service['showAutomaticallyRoutedToaster'] = () => { calledAutoRouteFunc = true; }; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; await service.handleApproveDeclineModal( 'Decline', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, false, false ); expect(calledAutoRouteFunc).to.be.true; expect(resetMyWorkspace).to.be.true; } @TestCase('should be able to handle approve decline modal - decline by manager') async handleDeclineModalManager (service: ApplicationActionService) { let calledAutoRouteFunc = false; let resetMyWorkspace = false; service['showAutomaticallyRoutedToaster'] = () => { calledAutoRouteFunc = true; }; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; await service.handleApproveDeclineModal( 'Decline', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, true, false ); expect(calledAutoRouteFunc).to.be.false; expect(resetMyWorkspace).to.be.true; } @TestCase('should be able to set app action flags') getApplicationActionFlags (service: ApplicationActionService) { const result = service.getApplicationActionFlags(this.application); expect(result.canApprove).to.be.false; expect(result.canNotifyOfStatus).to.be.true; expect(result.canManageCollabs).to.be.false; } @TestCase('should be able to determine if mangaer can take action based on wfl permission') canTakeAction (service: ApplicationActionService) { let canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.Approve ); // Can't approve because status is already approved expect(canTakeAction).to.be.false; // Can't archive because we don't have permission canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.ArchiveUnarchive ); expect(canTakeAction).to.be.false; // in permissions list canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.NotifyOfStatus ); expect(canTakeAction).to.be.true; // not in workflow manager actions canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.SendReminder ); expect(canTakeAction).to.be.false; // in permissions list canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.AddTags ); expect(canTakeAction).to.be.true; // in permissions list canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.UpdateProgram ); expect(canTakeAction).to.be.true; } @TestCase('Should show actions based on canTakeActionsOnAllApps') async checkCanTakeActionsOnAllApps (service: ApplicationActionService) { service['policyService'].grantApplication.canTakeActionsOnAllApps = () => { return true; }; const canTakeUnarchiveAction = service.canTakeAction( this.application, WorkflowManagerActions.ArchiveUnarchive ); // should be true even though we don't have the wfl permission granted expect(canTakeUnarchiveAction).to.be.true; } @TestCase('Should show actions based on canManageAllApplications') async checkCanManageAllApplications (service: ApplicationActionService) { // set canManageAllApplications to true before running test service['policyService'].grantApplication.canManageAllApplications = () => { return true; }; let canTakeAction = service.canTakeAction( this.application, WorkflowManagerActions.ArchiveUnarchive ); // should be true even though we don't have the wfl permission granted expect(canTakeAction).to.be.true; // if action isn't in switchcase, we should allow the action bc of canTakeAction permissions canTakeAction = service.canTakeAction( this.application, null ); expect(canTakeAction).to.be.true; } @TestCase('should prevent canceling applications that are draft') testPreventCancelApplicationsWithAwards (service: ApplicationActionService) { // set this to true so we are testing scenarios where action would be available service['policyService'].grantApplication.canManageAllApplications = () => { return true; }; const adaptedApp = { ...this.application, applicationStatus: ApplicationStatuses.Draft }; // this application has awards and so canTakeAction should be false const canTakeAction = service.canTakeAction(adaptedApp, WorkflowManagerActions.Cancel); expect(canTakeAction).to.be.false; } @TestCase('should prevent canceling applications with invalid status') testPreventCancelApplicationsInWrongStatus (service: ApplicationActionService) { // set this to true so we are testing scenarios where action would be available service['policyService'].grantApplication.canManageAllApplications = () => { return true; }; const appWithNoPaymentsButInvalidStatus = { ...this.application, hasAwards: false, applicationStatus: ApplicationStatuses.Canceled }; // this application has an invalid status for cancelation and so canTakeAction should be false const canTakeAction = service.canTakeAction(appWithNoPaymentsButInvalidStatus, WorkflowManagerActions.Cancel); expect(canTakeAction).to.be.false; } @TestCase('should allow canceling applications with valid status') testAllowCancelApplicationWithValidStatus (service: ApplicationActionService) { // set this to true so we are testing scenarios where action would be available service['policyService'].grantApplication.canManageAllApplications = () => { return true; }; const appWithNoPaymentsButValidStatus = { ...this.application, hasAwards: false, applicationStatus: ApplicationStatuses.AwaitingReview }; // this application has a valid status for cancelation and so canTakeAction should be true const canTakeAction = service.canTakeAction(appWithNoPaymentsButValidStatus, WorkflowManagerActions.Cancel); expect(canTakeAction).to.be.true; } @TestCase('should prevent canceling applications without wfl permission') testAllowCancelApplicationWithoutPermission (service: ApplicationActionService) { // set this to false so we are testing workflow level permissions service['policyService'].grantApplication.canManageAllApplications = () => { return true; }; const appWithNoPaymentsButValidStatus = { ...this.application, hasAwards: false, applicationStatus: ApplicationStatuses.AwaitingReview }; // this application is valid for cancel but we lack the required wfl permission const canTakeAction = service.canTakeAction(appWithNoPaymentsButValidStatus, WorkflowManagerActions.Cancel); expect(canTakeAction).to.be.true; } @TestCase('should allow canceling applications with wfl permission') async testAllowCancelApplicationWithPermission (service: ApplicationActionService) { // set this to false so we are testing workflow level permissions service['policyService'].grantApplication.canManageAllApplications = () => { return false; }; // set wfl permissions to include cancel service['workflowService']['workflowResources'].getMyWorkflowManagerRoles = async () => { return [{ workflowId: this.workflowId, workflowActions: [ WorkflowManagerActions.Approve, WorkflowManagerActions.NotifyOfStatus, WorkflowManagerActions.Cancel ] }, { workflowId: this.workflowId, workflowActions: [ WorkflowManagerActions.Approve, WorkflowManagerActions.NotifyOfStatus, WorkflowManagerActions.Cancel ] }]; }; await service['workflowService'].resetMyWorkflowManagerRolesMap(); const appWithNoPaymentsButValidStatus = { ...this.application, hasAwards: false, applicationStatus: ApplicationStatuses.AwaitingReview }; // this application is valid for cancel and we have the correct workflow level permission const canTakeAction = service.canTakeAction(appWithNoPaymentsButValidStatus, WorkflowManagerActions.Cancel); expect(canTakeAction).to.be.true; } @TestCase('should be able to handle bulk approve award pay') async handleBulkApproveAwardPay (service: ApplicationActionService) { let clearedPaymentProcessingPrograms = false; let resetMyWorkspace = false; let successMessage = ''; service['programService'].clearPaymentProcessingPrograms = () => { clearedPaymentProcessingPrograms = true; }; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleBulkApproveAwardPay( this.approveAwardPayload, true ); expect(clearedPaymentProcessingPrograms).to.be.true; expect(resetMyWorkspace).to.be.true; expect(successMessage).to.be.equal( 'Successfully awarded the applications' ); } @TestCase('should be able to adapt payload for bulk approve award pay') adaptPayloadForBulkAAP (service: ApplicationActionService) { const result = service.adaptPayloadForBulkAAP(this.approveAwardPayload); const award = result.applications[0].awards[0]; const expectedAwardDate = service['timezoneService'].returnMidnightUTCDate(this.awardDate); expect(award.awardDate).to.be.equal(expectedAwardDate); } @TestCase('should be able to handle bulk route apps') async handleBulkRouteApplications (service: ApplicationActionService) { let resetMyWorkspace = false; let successMessage = ''; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleBulkRouteApplications( { applicationIds: [this.appId], nominationIds: [], newWorkflowLevelId: 565, comment: '' }, false ); expect(resetMyWorkspace).to.be.true; expect(successMessage).to.be.equal('Successfully routed the applications'); } @TestCase('should be able to handle route apps') async handleRouteApplications (service: ApplicationActionService) { let resetMyWorkspace = false; let successMessage = ''; service['resetMyWorkspace'] = () => { resetMyWorkspace = true; }; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleRouteApplication( this.appId, { comments: '', workflowLevelId: 23535 }, false, false ); expect(resetMyWorkspace).to.be.true; expect(successMessage).to.be.equal( 'Successfully routed the application' ); } @TestCase('should be able to handle archive modal response') async handleArchiveModalResponse (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleArchiveModalResponse( { ids: [this.appId], notes: 'need to archive', code: 1 }, 'archiveApp' ); expect(successMessage).to.be.equal( 'Successfully archived the application' ); } @TestCase('should be able to handle unarchive modal response') async handleUnarchiveModalResponse (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleArchiveModalResponse( { ids: [this.appId], notes: 'need to unarchive', code: 1 }, 'unarchiveApp' ); expect(successMessage).to.be.equal( 'Successfully unarchived the application' ); } @TestCase('should be able to change application status') async changeApplicationStatus (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.changeApplicationStatus( { applicationId: this.appId, statusId: ApplicationStatuses.AwaitingReview, workflowLevelId: 65, comment: '', sendEmail: true, clientEmailTemplateId: 0, includeCommentInEmail: false, disregardWorkflowAutomationRules: false }, false ); expect(successMessage).to.be.equal( 'Successfully updated the status of the application' ); } @TestCase('should be able to handle send application status email') async handleSendApplicationStatusEmail (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleSendApplicationStatusEmail( { applicationIds: [this.appId], customMessage: '', clientEmailTemplateId: 0, emailOptionsRequest: null }, false ); expect(successMessage).to.be.equal( 'Successfully notified applicant of status' ); } @TestCase('should be able to handle delete application') async deleteApplication (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleDeleteApplication(this.appId, false); expect(successMessage).to.be.equal( 'Successfully deleted the application' ); } @TestCase('should be able to handle set recommended funding') async handleSetRecommendedFunding (service: ApplicationActionService) { let calledAutoRouteFunc = false; service['showAutomaticallyRoutedToaster'] = () => { calledAutoRouteFunc = true; }; let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleSetRecommendedFunding( this.appId, this.reviewerRecommendedAmount ); expect(calledAutoRouteFunc).to.be.true; expect(successMessage).to.be.equal( 'Successfully set the recommended funding amount' ); } @TestCase('should be able to handle update program') async handleUpdateProgram (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.handleUpdateProgram( { programId: 35, cycleId: 46, workflowLevelId: 46, vettingInfo: null, clientEmailTemplateId: 0 }, this.appId, this.orgId ); expect(successMessage).to.be.equal( 'Successfully updated the program' ); } @TestCase('should be able to cancel application') async cancelApplication (service: ApplicationActionService) { let successMessage = ''; service['confirmAndTakeActionService']['notifier']['success'] = (message: string) => { successMessage = message; }; await service.cancelApplication( { applicationId: this.appId, reason: null, comment: '', sendEmailToApplicants: false, customMessage: '', emailOptions: null } ); expect(successMessage).to.be.equal( 'Successfully canceled application' ); } @TestCase('should be able to reset my workspace') resetMyWorkspace (service: ApplicationActionService) { let calledResetOnRepo = false; const repository = { reset: () => { calledResetOnRepo = true; } } as AutoTableRepository; spy.on(service['autoTableFactory'], 'getRepository', () => { return repository; }); service.resetMyWorkspace(); expect(calledResetOnRepo).to.be.true; } @TestCase('should be able to error toastr') testErrorToastr (service: ApplicationActionService) { let error = false; spy.on(service['notifier'], 'error', () => { error = true; }); service.errorToastr('yo'); expect(error).to.be.true; } @TestCase('should be able to handle update program and vetting') async testHandleUpdateProgramAndVetting (service: ApplicationActionService) { spy.on(service['applicationActionResources'], 'updateProgram'); spy.on(service['addOrgService'], 'handleVettingAfterUpdateProgram'); await service.handleUpdateProgramAndVetting( { vettingInfo: { fullName: 'test', email: 'test@test.com', website: 'website.com' }, cycleId: 1 } as UpdateProgramModalResponse, 1, 2 ); expect(service['applicationActionResources']['updateProgram']).to.have.been.called.once; expect(service['addOrgService']['handleVettingAfterUpdateProgram']).to.have.been.called.once; } @TestCase('should be able to handle route app and reset data') async handleRouteAppAndResetData (service: ApplicationActionService) { spy.on(service['applicationActionResources'], 'routeApplicationByAppManager'); await service.handleRouteApplicationAndResetData( 1, 'data' as any, true ); expect(service['applicationActionResources']['routeApplicationByAppManager']).to.have.been.called.once; } @TestCase('should be able to show automatically routed toastr') showAutoRoutedToastr (service: ApplicationActionService) { let msg; service['successToastr'] = (text) => { msg = text; }; service.showAutomaticallyRoutedToaster(false); expect(msg).to.equal('Routing rule criteria met. Application has been routed to the next workflow level.'); } @TestCase('should be able to handle approve decline error - application') testHandleApproveDeclineError (service: ApplicationActionService) { const error = { message: 'error', name: 'error' }; let msg; service['errorToastr'] = (text) => { msg = text; }; let loggedError: unknown; spy.on(service['logger'], 'error', (err) => { loggedError = err; }); let errorCaught; try { service.handleApproveDeclineError(error, 'Approve', false); } catch (e) { errorCaught = e; } expect(errorCaught).to.equal(error); expect(loggedError).to.equal(error); expect(msg).to.equal('There was an error approving the application'); } @TestCase('should be able to handle approve decline error - nomination') testHandleApproveDeclineErrorNomination (service: ApplicationActionService) { const error = { message: 'error', name: 'error' }; let msg; service['errorToastr'] = (text) => { msg = text; }; let loggedErrors: unknown; spy.on(service['logger'], 'error', (err) => { loggedErrors = err; }); let errorCaught; try { service.handleApproveDeclineError(error, 'Decline', true); } catch (e) { errorCaught = e; } expect(errorCaught).to.equal(error); expect(loggedErrors).to.equal(error); expect(msg).to.equal('There was an error declining the nomination'); } @TestCase('should correctly handle errors for approve decline modal - approve') async handleErrorsForApproveModal (service: ApplicationActionService) { let errorReturned: any; spy.on(service['applicationActionResources'], 'approveApplicationByAppManager', () => { throw new Error('problem'); }); spy.on(service, 'handleApproveDeclineError'); try { await service.handleApproveDeclineModal( 'Approve', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, true, false ); } catch (e) { errorReturned = e; } expect(service['handleApproveDeclineError']).to.be.called.once; expect(errorReturned.message).to.equal('problem'); } @TestCase('should correctly handle errors for approve decline modal - decline') async handleErrorsForDeclineModal (service: ApplicationActionService) { let errorReturned: any; spy.on(service['applicationActionResources'], 'declineApplicationByAppManager', () => { throw new Error('problem'); }); spy.on(service, 'handleApproveDeclineError'); try { await service.handleApproveDeclineModal( 'Decline', { applicationIds: [this.appId], sendEmail: true, comments: '', clientEmailTemplateId: 0, emailOptionsModel: null }, true, false ); } catch (e) { errorReturned = e; } expect(service['handleApproveDeclineError']).to.be.called.once; expect(errorReturned.message).to.equal('problem'); } }