import { Injectable } from '@angular/core'; import { GCMockModule } from '@core/mocks/gc-module.mock'; import { BatchItemFromApi, CanRecallPaymentPayload, PaymentType, ProcessingTypes } from '@core/typings/payment.typing'; import { BatchStatuses, PaymentStatus } from '@core/typings/status.typing'; import { getOrganizedErrors, validateMultiple } from '@yourcause/common'; import { BeforeEach, Spec, TestCase } from '@yourcause/test-decorators'; import { DescribeAngularService } from '@yourcause/test-decorators/angular'; import { expect, spy } from 'chai'; import { PaymentExistsInGC, PaymentImportModel, PaymentProcessingService } from './payment-processing.service'; @Injectable({ providedIn: 'root' }) @DescribeAngularService(PaymentProcessingService, { imports: [ GCMockModule ] }) export class PaymentProcessingServiceSpec implements Spec { mockIDReturn: number[] = []; paymentStats = { excludedPaymentsAmount: 1500, excludedPaymentsNumber: 3, includedPaymentsAmount: 6000, includedPaymentsNumber: 4, paymentEndDate: '', paymentStartDate: '', processorName: 'YourCause', totalPaymentsAmount: 7500, totalPaymentsNumber: 7 }; inKindPaymentStats = { pendingPaymentsCount: 3, pendingUnitsCount: 3, amountOfPendingUnits: 3, fulfilledPaymentsCount: 3, fulfilledUnitsCount: 3, amountOfFulfilledUnits: 3 }; addBatchForApi = { name: 'batchName', notes: 'batchNotes', processingTypeId: ProcessingTypes.Both, externalBatchId: 'externalBatchId' }; updatePaymentsApi = { paymentIds: [1, 2, 3, 4], checkNumber: '09876', status: PaymentStatus.Scheduled, notes: 'string;', issuedDate: '12-24-2021', clearedDate: '12-25-2021', accountNumber: 'accountNumber', paymentType: PaymentType.Check, statusDate: '12-31-2021' }; automaticallyRoutedObj = { automaticallyRouted: false }; @BeforeEach() mockService ( service: PaymentProcessingService ) { // mock the call to the API to return a fixed set of missing IDs service['paymentProcessingResources'].validatePaymentIds = async () => { return this.mockIDReturn; }; service['paymentProcessingResources'].getInKindPaymentStats = async () => { return this.inKindPaymentStats; }; service['paymentProcessingResources'].addPaymentsToBatchPaginated = async () => { return null; }; service['paymentProcessingResources'].updatePayments = async () => { return this.automaticallyRoutedObj; }; service['fileService'].downloadByFormat = async () => { return true; }; } @TestCase('should be able to validate if a payment ID exists') async testShouldValidateExistingPaymentID ( service: PaymentProcessingService ) { // mock the missing id return this.mockIDReturn = [ 1 ]; // set the ID to the missing ID above // simulating a scenario where a payment being imported is missing const testRecords: PaymentImportModel[] = [{ 'GC Payment ID': 1, 'AP System Payment ID': '123', 'Payment Date': new Date().toString(), 'Status Date': new Date().toString(), 'Payment Status': 'Cleared', 'Payment Number': 123, 'Payment Type': '' }]; // run the validation, mocking the injector const result = await validateMultiple(PaymentImportModel, testRecords, { get () { return service; } }); // get a simplified set of errors, mocking i18n const organizedErrors = getOrganizedErrors(result.recordLevelErrors, { translate: () => '' }); // the errors from the validator library attaches the failed validator decorator // this lets us test for specific validation errors expect(organizedErrors[0].validator).to.equal(PaymentExistsInGC); expect(organizedErrors.length).to.equal(1); } @TestCase('should set proper status text') testShouldSetStatusText ( service: PaymentProcessingService ) { const mockBatch: BatchItemFromApi = { batchId: 1, name: 'test user', notes: 'notes', processingTypeId: 3, batchStatusId: 6, clientName: 'test client', batchStatusLastUpdatedBy: { firstName: 'test', lastName: 'user', email: 'email@email.com', userType: 1, statusLastUpdatedDate: '2-20-2020', impersonatedBy: 'timmy' }, totalPaymentsInBatch: 1, totalPaymentsAmount: 1, batchStatusLastUpdatedDate: '2020-2-20', externalBatchId: 'external batch', grantProgramIds: [1] }; const text = service.getStatusText(mockBatch); expect(text).to.equal('Feb 20, 2020 by test user'); expect(text).to.not.equal('Oct 20, 2020 by test user'); } @TestCase('should be able to adapt stats') adaptStats (service: PaymentProcessingService) { const adapted = service.adaptStats(this.paymentStats, true); expect(adapted.availableDetails.includedPayments.number).to.be.equal( this.paymentStats.includedPaymentsNumber ); expect(adapted.availableDetails.excludedPayments.total).to.be.equal( this.paymentStats.excludedPaymentsAmount ); } @TestCase('should be able to set InKind payment stats') async setInKindPaymentStats (service: PaymentProcessingService) { const response = await service.setInKindPaymentStats(); expect(response).to.not.be.null; expect(response).to.not.throw; expect(service.get('inKindPaymentStats')).to.be.equal(this.inKindPaymentStats); } @TestCase('should be able to handleBatchUpdateModal') async handleBatchUpdateModal (service: PaymentProcessingService) { service['paymentProcessingResources']['updateBatch'] = async () => { return true; }; const data = { batchModalResponse: { name: 'batchModalResponseName', notes: 'batchModalResponseNotes' }, id: 456 };//EditBatchModalResponse; number const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'updateBatch', (a) => a); const response = await service.handleBatchUpdateModal(data.batchModalResponse, data.id); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to handle special handling modal') async handleSpecialHandlingModal (service: PaymentProcessingService) { service['paymentProcessingResources']['updateSpecialHandling'] = async () => { return true; }; const data = { paymentId: 789, specialHandlingName: 'Handler 2', specialHandlingAddress1: '111 Broadway Ave.', specialHandlingAddress2: 'Apt. 2', specialHandlingCountry: 'United States', specialHandlingCity: 'New York', specialHandlingStateProvinceRegion: 'New York', specialHandlingPostalCode: '123456', specialHandlingNotes: 'deliver by carrier pidgeon', specialHandlingReason: 'urgent!', specialHandlingFileUrl: 'google.com', fileUploadId: 0 }; //UpdatePaymentSpecialHandling const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'updateSpecialHandling', (a) => a); const response = await service.handleSpecialHandlingModal(data); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to handle expedite modal') async handleExpediteModal (service: PaymentProcessingService) { service['paymentProcessingResources']['expeditePayment'] = async () => { return 111; }; service['paymentProcessingResources']['processBatch'] = async () => { return true; }; const data = { paymentId: 111, batchName: 'batch 4', batchNotes: 'the fourth batch', batchProcessingTypeId: ProcessingTypes.Both }; //ExpeditePaymentForApi const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'expeditePayment', (a) => a); const resourceMethodSpy2 = spy.on(service['paymentProcessingResources'], 'processBatch', (a) => a); const response = await service.handleExpediteModal(data); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); expect(resourceMethodSpy2).to.have.been.called(); } @TestCase('should be able to update batch status modal') async updateBatchStatusModal (service: PaymentProcessingService) { service['paymentProcessingResources']['updateBatchStatus'] = async () => { return true; }; const data = { item: { id: 222, name: 'Batch Item #3', processorName: 'YourCause', processorType: ProcessingTypes.YourCause, paymentDates: '12-25-2021' }, notes: 'status notes', status: BatchStatuses.Open }; const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'updateBatchStatus', (a) => a); const response = await service.updateBatchStatusModal( data.item, data.notes, data.status );//BatchItem, string, BatchStatuses expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to delete batch') async deleteBatch (service: PaymentProcessingService) { service['paymentProcessingResources']['deleteBatch'] = async () => { return true; }; service['dataHubService']['setBatchesList'] = async () => { return true; }; const data = { id: 333 };//number const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'deleteBatch', (a) => a); const resourceMethodSpy2 = spy.on(service['dataHubService'], 'setBatchesList', (a) => a); const response = await service.deleteBatch(data.id); expect(response).to.not.throw; expect(response).to.be.equal(333); expect(resourceMethodSpy).to.have.been.called(); expect(resourceMethodSpy2).to.have.been.called(); } @TestCase('should be able to handleSendBatchToProcessing') async handleSendBatchToProcessing (service: PaymentProcessingService) { service['paymentProcessingResources']['processBatch'] = async () => { return true; }; const data = { batchId: 444, notes: 'batch notes', status: PaymentStatus.Scheduled };//number, string. PaymentStatus const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'processBatch', (a) => a); const response = await service.handleSendBatchToProcessing( data.batchId, data.notes, data.status, null ); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to handleSendAvailableToProcessing') async handleSendAvailableToProcessing (service: PaymentProcessingService) { service['paymentProcessingResources']['processAllAvailable'] = async () => { return 555; }; service['paymentProcessingResources']['processBatch'] = async () => { return true; }; const item = { id: 222, name: 'Batch Item #3', processorName: 'YourCause', processorType: ProcessingTypes.YourCause, paymentDates: '12-25-2021' };//BatchItem const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'processAllAvailable', (a) => a); const resourceMethodSpy2 = spy.on(service['paymentProcessingResources'], 'processBatch', (a) => a); const response = await service.handleSendAvailableToProcessing( item, { name: 'send to processing modal response name', notes: 'send to processing modal response notes', paymentStatus: PaymentStatus.Scheduled, issueDate: null }, //SendToProcessingModalResponse false //do not use options ); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); expect(resourceMethodSpy2).to.have.been.called(); } @TestCase('should be able to handleHoldModal') async handleHoldModal (service: PaymentProcessingService) { service['paymentProcessingResources']['updatePaymentHold'] = async () => { return true; }; const data = { paymentId: 135, toHold: true, notes: 'handle hold modal notes' }; const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'updatePaymentHold', (a) => a); const response = await service.handleHoldModal( data.paymentId, data.toHold, data.notes ); expect(response).to.not.throw; expect(service.get('currentIncludedOptions')).to.be.undefined; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to handle notifying payees') async handleNotifyPayees (service: PaymentProcessingService) { service['paymentProcessingResources']['notifyPayeees'] = async () => { return true; }; const data = { response: { customMessage: 'custom message', clientEmailTemplateId: 9, emailOptions: { ccEmails: ['email1@gmail.com', 'email2@gmail.com'], bccEmails: ['email3@gmail.com', 'email4@gmail.com'], attachments: [1, 2, 3, 4] } }, paymentIds: [9, 99, 999] };//NotifyPayeesModalResponse, number[] const resourceMethodSpy = spy.on(service['paymentProcessingResources'], 'notifyPayeees', (a) => a); const response = await service.handleNotifyPayees( data.response, data.paymentIds ); expect(response).to.not.throw; expect(resourceMethodSpy).to.have.been.called(); } @TestCase('should be able to get payment type from import') getPaymentTypeFromImport (service: PaymentProcessingService) { const data = { paymentTypeCheck: 'check', paymentTypeACH: 'ach', paymentTypeWireTransfer: 'wire', paymentTypeOther: 'other' }; const checkResponse = service.getPaymentTypeFromImport(data.paymentTypeCheck); expect(checkResponse).to.not.throw; expect(checkResponse).to.be.equal(PaymentType.Check); const achResponse = service.getPaymentTypeFromImport(data.paymentTypeACH); expect(achResponse).to.not.throw; expect(achResponse).to.be.equal(PaymentType.ACH); const wireResponse = service.getPaymentTypeFromImport(data.paymentTypeWireTransfer); expect(wireResponse).to.not.throw; expect(wireResponse).to.be.equal(PaymentType.WireTransfer); const otherResponse = service.getPaymentTypeFromImport(data.paymentTypeOther); expect(otherResponse).to.not.throw; expect(otherResponse).to.be.equal(PaymentType.Other); } @TestCase('should be able to determine if payment can be recalled') testIfPaymentCanBeRecalled (service: PaymentProcessingService) { const eligiblePayload: CanRecallPaymentPayload = { processingType: ProcessingTypes.YourCause, batchStatus: BatchStatuses.Processing, paymentStatus: PaymentStatus.Processing, paymentInBatch: true }; const ineligiblePayload: CanRecallPaymentPayload = { processingType: ProcessingTypes.Client, batchStatus: BatchStatuses.Complete, paymentStatus: PaymentStatus.Fulfilled, paymentInBatch: false }; const eligibleResult = service.canRecallPayment(eligiblePayload); const ineligibleResult = service.canRecallPayment(ineligiblePayload); expect(eligibleResult).to.be.true; expect(ineligibleResult).to.be.false; } @TestCase('should call endpoint for payment eligibility') async testCallingEndpointForPaymentEligibilityForRecall (service: PaymentProcessingService) { const spy1 = spy.on(service['paymentProcessingResources'], 'paymentIsEligibleForRecall'); await service.handlePaymentIsEligibleForRecall(1); expect(spy1).to.have.been.called(); } @TestCase('should call endpoint for payment recall') async testCallingEndpointForPaymentRecall (service: PaymentProcessingService) { const spy1 = spy.on(service['paymentProcessingResources'], 'recallPayment'); await service.handleRecallPayment({ paymentId: 1, reasonForRecall: 'bad payment' }); expect(spy1).to.have.been.called(); } }