import { Injectable } from '@angular/core'; import { GCMockModule } from '@core/mocks/gc-module.mock'; import { CurrencyService } from '@core/services/currency.service'; import { Budget, BudgetDashboardFundingSource, BudgetDetail, BudgetImportModel, FundingSource, FundingSourceTypes } from '@core/typings/budget.typing'; import { ProcessingTypes } from '@core/typings/payment.typing'; import { BudgetResources } from '@features/budgets/budget.resources'; import { BudgetService } from '@features/budgets/budget.service'; import { DynamicCSVImportService, getKeys } from '@yourcause/common'; import { BeforeEach, Spec, TestCase } from '@yourcause/test-decorators'; import { DescribeAngularService } from '@yourcause/test-decorators/angular'; import { expect, spy } from 'chai'; import { CurrencyFormattingObj } from 'frontend-common/masking/src/currency.typing'; @Injectable({ providedIn: 'root' }) @DescribeAngularService(BudgetService, { imports: [ GCMockModule ] }) export class BudgetServiceSpec implements Spec { budgetId = 20104; budget: BudgetDetail = { id: 20104, name: '02/20 Budget Dash Test', description: '02/20/19', active: true, budgetFundingSources: [{ budgetId: 20104, processingTypeId: ProcessingTypes.Client, fundingSourceId: 4359, fundingSourceType: 1, totalAmount: 1000.0000, totalUnits: 0, active: true, fundingSourceName: 'Corporate Funding Source', numberOfPayments: 2, amountRemaining: 0, amountUnavailable: 0, totalAmountPayments: 1000.0000, totalUnitsPayments: 0, isOverAllocationSource: false, isClosed: false }, { processingTypeId: ProcessingTypes.Client, budgetId: 20104, fundingSourceId: 18203, fundingSourceType: 1, totalAmount: 2000.0000, totalUnits: 0, active: true, amountRemaining: 0, amountUnavailable: 0, fundingSourceName: 'Funding Source A', numberOfPayments: 1, totalAmountPayments: 2000.0000, totalUnitsPayments: 0, isOverAllocationSource: false, isClosed: false }, { budgetId: 20104, fundingSourceId: 38647, processingTypeId: ProcessingTypes.Client, fundingSourceType: 1, totalAmount: 300.0000, totalUnits: 0, active: true, amountRemaining: 0, amountUnavailable: 0, fundingSourceName: 'Overage', numberOfPayments: 1, totalAmountPayments: 100.0000, totalUnitsPayments: 0, isOverAllocationSource: true, isClosed: false }] }; budgets: Budget[] = [ { id: 2968, name: '2018 Grants', description: 'Budget', fundingSourceType: 1, numberOfPayments: 45, numberOfFundingSources: 4, totalAmount: 3065588.0000, totalUnits: 0, totalAmountAvailable: 2973110.0000, totalUnitsAvailable: 0, overageAmount: 162476.1600, totalSpent: 92478.0000, totalSpentUnits: 0, totalSpentOverage: 1500.0000, totalRemainingOverage: 160976.1600, totalUnitsAvailableAmount: 500, totalSpentUnitsAmount: 0, totalRemainingUnitsAmount: 0, totalUnavailableAmount: 0 }, { id: 2969, name: 'Discretionary Fund', description: 'For use in one', fundingSourceType: 1, numberOfPayments: 9, numberOfFundingSources: 2, totalAmount: 750000.0000, totalUnits: 0, totalAmountAvailable: 741824.0000, totalUnitsAvailable: 0, overageAmount: null, totalSpent: 8176.0000, totalSpentUnits: 0, totalSpentOverage: 0.0, totalRemainingOverage: 0.0, totalUnitsAvailableAmount: 500, totalSpentUnitsAmount: 0, totalRemainingUnitsAmount: 0, totalUnavailableAmount: 0 }]; budgetStats = { totalAllocated: 3000.0000, numberOfFundingSources: 2, totalSpent: 3000.0000, numberOfPayments: 4, totalRemaining: 0.0000, totalAllocatedUnits: 0, totalSpentUnits: 0, totalRemainingUnits: 0, totalAllocatedOverage: 300.0000, totalSpentOverage: 100.0000, totalRemainingOverage: 200.0000, totalUnavailableAmount: 0, reservedAmount: 0 }; budgetPrograms = [{ programId: 21806, programName: '02/20 Budget Dash Program', numberOfPayments: 4, paymentsAmount: 3000.0000, paymentsAmountUnit: 0 }]; budgetSources: BudgetDashboardFundingSource[] = [{ fundingSourceId: 4359, fundingSourceName: 'Corporate Funding Source', totalAllocated: 1000.0000, totalSpent: 1000.0000, totalRemaining: 0.0000, totalAllocatedUnits: 0, totalSpentUnits: 0, totalRemainingUnits: 0, isOverage: false, isClosed: false, totalUnavailableAmount: 0, reservedAmount: 0 }]; openSources: FundingSource[] = [{ id: 4359, name: 'Corporate Funding Source', type: 1, totalAmount: 1000000.0000, totalUnits: null, unitCost: null, unitValue: null, active: true, processingTypeId: 2, amountRemaining: 924721.0000, unitsRemaining: 0, amountUnallocated: 410280.8900, unitsUnallocated: 0, isClosed: false }]; closedSources: FundingSource[] = [{ id: 7906, name: 'Tina\'s Funding Source', type: 2, totalAmount: null, totalUnits: 500, unitCost: 5, unitValue: 10, active: false, processingTypeId: 1, amountRemaining: 35235325.0000, unitsRemaining: 0, amountUnallocated: 35235325.0000, unitsUnallocated: 0, isClosed: true }]; EUR = { symbol: '€', precision: 2, thousand: ' ', decimal: ',' }; USD = { symbol: '$', precision: 2, thousand: ',', decimal: '.' }; formattingData: Record = { USD: this.USD, EUR: this.EUR }; constructor ( private budgetResources: BudgetResources, private currencyService: CurrencyService, private dynamicCsvService: DynamicCSVImportService ) { } @BeforeEach() mock (service: BudgetService) { (service as any).budgetResources = this.budgetResources; (service as any).currencyService = this.currencyService; this.budgetResources.getBudgetStats = async () => { return this.budgetStats; }; this.budgetResources.getProgramsForBudgetDashboard = async () => { return this.budgetPrograms; }; this.budgetResources.getFundingSourcesForBudgetDashboard = async () => { return this.budgetSources; }; this.budgetResources.getBudget = async () => { return this.budget; }; this.budgetResources.getBudgets = async () => { return this.budgets; }; this.budgetResources.saveBudget = async () => { return true; }; this.budgetResources.getFundingSources = async () => { return this.openSources.concat(this.closedSources); }; this.budgetResources.getBudgetsSegmented = async () => { return []; }; this.budgetResources.getFundingSourcesSegmented = async () => { return []; }; this.currencyService.formatMoney = (money: number) => { return '' + money; }; } @TestCase('should be able to set budgets') async setBudgetsTest (service: BudgetService) { await service.setBudgets(); const budgets = service.get('budgets'); expect(budgets[0].id).to.be.equal(this.budgets[0].id); expect(budgets[0].numberOfPayments).to.be.equal(this.budgets[0].numberOfPayments); expect(budgets[1].fundingSourceType).to.be.equal(FundingSourceTypes.DOLLARS); } @TestCase('should be able to set budget name map') async setBudgetNameMapTest (service: BudgetService) { await service.setBudgets(); const map = service.get('budgetNameMap'); const budgets = service.get('budgets'); expect(map[budgets[0].id]).to.be.equal(this.budgets[0].name); } @TestCase('should be able to set source name map') async setSourceNameMapTest (service: BudgetService) { await service.setFundingSources(); const map = service.get('fundingSourceNameMap'); expect(map[this.openSources[0].id]).to.be.equal(this.openSources[0].name); } @TestCase('should be able to set budget dashboard map') async setBudgetDashboardTest (service: BudgetService) { await service.setBudgetForDashboard(this.budgetId); const map = service.get('budgetDashboardMap'); const detail = map[this.budgetId]; expect(detail.detail.name).to.be.equal(this.budget.name); expect(detail.stats.totalSpent).to.be.equal(this.budgetStats.totalSpent); expect(detail.programs[0].numberOfPayments).to.be.equal( this.budgetPrograms[0].numberOfPayments ); expect(detail.sources[0].totalAllocated).to.be.equal( this.budgetSources[0].totalAllocated ); } @TestCase('should be able to set budget options') async setBudgetOptionsTest (service: BudgetService) { await service.setBudgetOptions(); const options = service.get('allBudgetOptions'); expect(options[0].value).to.be.equal(this.budgets[0].id); expect(options[1].label).to.be.equal(this.budgets[1].name); } @TestCase('should be able to set budget for edit') async setBudgetForEditTest (service: BudgetService) { await service.getBudget(this.budgetId); const map = service.get('budgetEditMap'); const budget = map[this.budgetId]; expect(budget.name).to.be.equal('02/20 Budget Dash Test'); expect(budget.budgetFundingSources[0].budgetId).to.be.equal( this.budget.budgetFundingSources[0].budgetId ); } @TestCase('should be able to adapt budget for save') async adaptBudgetForSaveTest (service: BudgetService) { const adapted = service.adaptUiBudgetToApiBudget(this.budget); expect(adapted.fundingSources.length).to.be.equal( this.budget.budgetFundingSources.length ); } @TestCase('should be able to reset budget detail') async resetBudgetDetailTest (service: BudgetService) { service.setBudgetMap({ [this.budgetId]: this.budget }); service.resetBudgetDetail([this.budgetId]); const map = service.get('budgetMap'); expect(map[this.budgetId]).to.be.null; } @TestCase('should be able to set funding sources') async setFundingSourcesTest (service: BudgetService) { await service.setFundingSources(); const open = service.openFundingSources; const closed = service.closedFundingSources; expect(open[0].id).to.be.equal(this.openSources[0].id); expect(closed[0].id).to.be.equal(this.closedSources[0].id); } @TestCase('should be able to get unit cost map') async getUnitCostMapTest (service: BudgetService) { const map = await service.getUnitCostMap(); expect(map[this.closedSources[0].id]).to.be.equal( this.closedSources[0].unitCost ); } @TestCase('shoulsetSourceOptions source options') async setSourceOptions (service: BudgetService) { await service.setSourceOptions(); const options = service.get('allSourceOptions'); expect(options[0].value).to.be.equal(this.openSources[0].id); expect(options[0].label).to.be.equal(this.openSources[0].name); } @TestCase('should be able to get url for budget detail') getUrlForBudgetDetail (service: BudgetService) { const budgetId = 1; const url = service.getUrlForDetail( true, budgetId ); const isCorrectUrl = url.includes( `/management/program-setup/budgets/${budgetId}/funding-sources` ); expect(isCorrectUrl).to.be.true; } @TestCase('should be able to get url for fs detail') getUrlForFsDetail (service: BudgetService) { const fsId = 1; const url = service.getUrlForDetail( false, fsId ); const isCorrectUrl = url.includes( `/management/program-setup/funding-sources/${fsId}/audit-trail` ); expect(isCorrectUrl).to.be.true; } @TestCase('should be able to get budget import template') getBudgetImportTemplate (service: BudgetService) { const downloadSpy = spy.on(service['fileService'], 'downloadString', (data: string) => { return data; }); const openSourceIds = this.openSources.map((os) => os.id); const headerString = service.getBudgetImportTemplate( openSourceIds ); const expectedHeaderArray = ['Budget Name', 'Budget Description']; this.openSources.forEach((openSource) => { expectedHeaderArray.push(openSource.name); }); const expectedHeaderString = expectedHeaderArray.join(','); expect(downloadSpy).to.have.been.called.once; expect(headerString).to.equal(expectedHeaderString); } @TestCase('should be able to get budget import model') getBudgetImportModel (service: BudgetService) { const openSourceIds = this.openSources.map((os) => os.id); const importModel = service.getBudgetImportModel(openSourceIds); const keys = getKeys(importModel); const keysIncludeAllOpenSources = this.openSources.every((os) => { return keys.some((key) => key === os.name); }); expect(keysIncludeAllOpenSources).to.be.true; } @TestCase('should be able to validate budget import records - Pass') validateBudgetImportRecordsPass (service: BudgetService) { const validImportRecords = [{ 'Budget Name': 'Budget A', 'Budget Description': 'Another new budget', 'Corporate Funding Source': 123 }]; const errors = service.validateFundingSourceAllocationsForBulkBudgetImport(this.openSources, validImportRecords); expect(errors).to.deep.equal([]); } @TestCase('should be able to validate budget import records - Fail from allocation') validateBudgetImportRecordsFailAllocation (service: BudgetService) { const validImportRecords = [{ 'Budget Name': 'Budget A', 'Budget Description': 'Another new budget', 'Corporate Funding Source': 9910280.8900 }]; const errors = service.validateFundingSourceAllocationsForBulkBudgetImport(this.openSources, validImportRecords); const expectedErrors = [{ context: { amountAllocatedInImport: '9910280.89', amountUnallocated: '410280.89', fsName: 'Corporate Funding Source' }, defaultValue: 'Allocation exceeds available funds for __fsName__. Allocated in this import: __amountAllocatedInImport__. Total Available: __amountUnallocated__', i18nKey: 'BUDGET:textBudgetImportFundingSourceError2', prop: 'Corporate Funding Source' }]; expect(errors).to.deep.equal(expectedErrors); } @TestCase('should be able to validate budget import records - Fail from existing budget name') async validateBudgetImportRecordsFailBudgetName (service: BudgetService) { service.setBudgets(); const validImportRecords = [{ 'Budget Name': '2018 Grants', 'Budget Description': 'Another new budget', 'Corporate Funding Source': 1.00 }]; const errors = await this.dynamicCsvService.validateMultiple( BudgetImportModel, validImportRecords ); const expectedErrors = { context: { name: '2018 Grants' }, defaultValue: 'Budget name must be unique. Budget with name __name__ already exists.', i18nKey: 'BUDGET:textBudgetImportNameError' }; const comparisonError = { defaultValue: errors.recordLevelErrors[0]['Budget Name'][0].defaultValue, context: errors.recordLevelErrors[0]['Budget Name'][0].context, i18nKey: errors.recordLevelErrors[0]['Budget Name'][0].i18nKey }; expect(comparisonError).to.deep.equal(expectedErrors); } }