diff --git a/api/src/modules/impact/base-impact.service.ts b/api/src/modules/impact/base-impact.service.ts index 8ccf2fa46..d3873bee2 100644 --- a/api/src/modules/impact/base-impact.service.ts +++ b/api/src/modules/impact/base-impact.service.ts @@ -144,7 +144,7 @@ export class BaseImpactService { treeOptions, ) ).map((entity: LOCATION_TYPES) => { - return { name: entity, children: [] }; + return { id: entity, name: entity, children: [] }; }); default: @@ -413,34 +413,39 @@ export class BaseImpactService { // as using Math.max(...impactTable.map(...)) since the call stack will be exceeded because of no of arguments const yearsWithData: Set = new Set(); - const indicatorEntityMap: ImpactDataTableAuxMap = new Map(); + const indicatorEntityYearMap: ImpactDataTableAuxMap = new Map(); // Convert the flat structure on array to tree of Maps for easier access for (const impactTableData of dataForImpactTable) { - let entityMap: Map> | undefined = - indicatorEntityMap.get(impactTableData.indicatorId); - - if (!entityMap) { - entityMap = new Map(); - indicatorEntityMap.set(impactTableData.indicatorId, entityMap); + let indicatorEntityMap: Map> | undefined = + indicatorEntityYearMap.get(impactTableData.indicatorId); + + if (!indicatorEntityMap) { + indicatorEntityMap = new Map(); + indicatorEntityYearMap.set( + impactTableData.indicatorId, + indicatorEntityMap, + ); } - let yearMap: Map | undefined = entityMap.get( - impactTableData.name, - ); - if (!yearMap) { - yearMap = new Map(); - entityMap.set(impactTableData.name, yearMap); + let entityYearMap: Map | undefined = + indicatorEntityMap.get(impactTableData.identifier); + if (!entityYearMap) { + entityYearMap = new Map(); + indicatorEntityMap.set(impactTableData.identifier, entityYearMap); } - yearMap.set(impactTableData.year, dataToRowsValuesFunc(impactTableData)); + entityYearMap.set( + impactTableData.year, + dataToRowsValuesFunc(impactTableData), + ); yearsWithData.add(impactTableData.year); } const lastYearWithData: number = Math.max(...yearsWithData.values()); - return [indicatorEntityMap, lastYearWithData]; + return [indicatorEntityYearMap, lastYearWithData]; } /** @@ -498,12 +503,12 @@ export class BaseImpactService { } /** - * Small helper function to get the combined IndicatorId+EntityName+Year to facilitate pre processing of + * Small helper function to get the combined IndicatorId+EntityIdentifier+Year to facilitate pre processing of * Impact Table Data before building the impact table * @param data */ static getImpactTableDataKey(data: ImpactTableData): string { - return data.indicatorId + '-' + data.name + '-' + data.year; + return data.indicatorId + '-' + data.identifier + '-' + data.year; } static sortRowValueByYear( @@ -514,6 +519,9 @@ export class BaseImpactService { } } +// Helper data structure that represents the aggregated data for each combination of Indicator, Entity and Year +// Indicator -> N Entities -> N Years -> RowsValues (data that represents the actual impact) +// Map>> export type ImpactDataTableAuxMap = Map< string, Map> diff --git a/api/src/modules/impact/comparison/actual-vs-scenario.service.ts b/api/src/modules/impact/comparison/actual-vs-scenario.service.ts index f306f1558..5e84f8c3a 100644 --- a/api/src/modules/impact/comparison/actual-vs-scenario.service.ts +++ b/api/src/modules/impact/comparison/actual-vs-scenario.service.ts @@ -76,12 +76,7 @@ export class ActualVsScenarioImpactService { dto.sortingOrder, ); - const paginatedTable: any = BaseImpactService.paginateTable( - impactTable, - fetchSpecification, - ); - - return paginatedTable; + return BaseImpactService.paginateTable(impactTable, fetchSpecification); } private buildActualVsScenarioImpactTable( @@ -131,15 +126,10 @@ export class ActualVsScenarioImpactService { lastYearWithData, ); - // copy and populate tree skeleton for each indicator - const impactTableEntitySkeleton: ActualVsScenarioImpactTableRows[] = - this.buildActualVsScenarioImpactTableRowsSkeleton(entityTree); - - for (const entity of impactTableEntitySkeleton) { - this.populateValuesRecursively(entity, entityMap, rangeOfYears); - } - - impactTableDataByIndicator.rows = impactTableEntitySkeleton; + impactTableDataByIndicator.rows = entityTree.map( + (entity: ImpactTableEntityType) => + this.buildImpactTableRecursively(entity, entityMap, rangeOfYears), + ); impactTableDataByIndicator.yearSum = this.calculateIndicatorSumByYear( entityMap, @@ -269,34 +259,49 @@ export class ActualVsScenarioImpactService { } /** - * @description Recursive function that populates and returns - * aggregated data of parent entity and all its children + * @description Constructs the Impact Table and populates its aggregated values recursively + * according to the given entity and its children + * @param entity contains the entity tree to that will be used to build the Impact Table + * @param entityYearMap contains the actual values data for all entities */ - private populateValuesRecursively( - entity: ActualVsScenarioImpactTableRows, - entityDataMap: Map< + private buildImpactTableRecursively( + entity: ImpactTableEntityType, + entityYearMap: Map< string, Map >, rangeOfYears: number[], - ): ActualVsScenarioImpactTableRowsValues[] { - entity.values = []; - for (const year of rangeOfYears) { - const rowsValues: ActualVsScenarioImpactTableRowsValues = { - year: year, - value: 0, - comparedScenarioValue: 0, - absoluteDifference: 0, - percentageDifference: 0, - isProjected: false, - }; - entity.values.push(rowsValues); - } + ): ActualVsScenarioImpactTableRows { + const impactTableRow: ActualVsScenarioImpactTableRows = { + name: entity.name || '', + values: rangeOfYears.map( + (year: number) => + ({ + year, + value: 0, + comparedScenarioValue: 0, + absoluteDifference: 0, + percentageDifference: 0, + isProjected: false, + } as ActualVsScenarioImpactTableRowsValues), + ), + children: + entity.children?.length > 0 + ? entity.children.map((childEntity: ImpactTableEntityType) => + this.buildImpactTableRecursively( + childEntity, + entityYearMap, + rangeOfYears, + ), + ) + : [], + }; const valuesToAggregate: ActualVsScenarioImpactTableRowsValues[][] = []; const selfData: | Map - | undefined = entityDataMap.get(entity.name); + | undefined = entityYearMap.get(entity.id); + if (selfData) { const sortedSelfData: ActualVsScenarioImpactTableRowsValues[] = Array.from(selfData.values()).sort( @@ -304,18 +309,15 @@ export class ActualVsScenarioImpactService { ); valuesToAggregate.push(sortedSelfData); } - entity.children.forEach((childEntity: ActualVsScenarioImpactTableRows) => { - //first aggregate data of child entity and then add returned value for parents aggregation - const childValues: ActualVsScenarioImpactTableRowsValues[] = - this.populateValuesRecursively( - childEntity, - entityDataMap, - rangeOfYears, - ); - valuesToAggregate.push(childValues); - }); - for (const [valueIndex, entityRowValue] of entity.values.entries()) { + for (const childEntity of impactTableRow.children) { + valuesToAggregate.push(childEntity.values); + } + + for (const [ + valueIndex, + entityRowValue, + ] of impactTableRow.values.entries()) { for (const valueToAggregate of valuesToAggregate) { entityRowValue.value += valueToAggregate[valueIndex].value; entityRowValue.comparedScenarioValue += @@ -340,22 +342,7 @@ export class ActualVsScenarioImpactService { : percentageDifference; } } - return entity.values; - } - - private buildActualVsScenarioImpactTableRowsSkeleton( - entities: ImpactTableEntityType[], - ): ActualVsScenarioImpactTableRows[] { - return entities.map((item: ImpactTableEntityType) => { - return { - name: item.name || '', - children: - item.children?.length > 0 - ? this.buildActualVsScenarioImpactTableRowsSkeleton(item.children) - : [], - values: [], - }; - }); + return impactTableRow; } // For all indicators, entities are sorted by the value of the given sortingYear, in the order given by sortingOrder diff --git a/api/src/modules/impact/comparison/scenario-vs-scenario.service.ts b/api/src/modules/impact/comparison/scenario-vs-scenario.service.ts index b50931fdd..0bb2a73b1 100644 --- a/api/src/modules/impact/comparison/scenario-vs-scenario.service.ts +++ b/api/src/modules/impact/comparison/scenario-vs-scenario.service.ts @@ -95,12 +95,7 @@ export class ScenarioVsScenarioImpactService { dto.sortingOrder, ); - const paginatedTable: any = BaseImpactService.paginateTable( - impactTable, - fetchSpecification, - ); - - return paginatedTable; + return BaseImpactService.paginateTable(impactTable, fetchSpecification); } private buildImpactTable( @@ -150,15 +145,10 @@ export class ScenarioVsScenarioImpactService { lastYearWithData, ); - // copy and populate tree skeleton for each indicator - const impactTableEntitySkeleton: ScenarioVsScenarioImpactTableRows[] = - this.buildScenarioVsScenarioImpactTableRowsSkeleton(entityTree); - - for (const entity of impactTableEntitySkeleton) { - this.populateValuesRecursively(entity, entityMap, rangeOfYears); - } - - impactTableDataByIndicator.rows = impactTableEntitySkeleton; + impactTableDataByIndicator.rows = entityTree.map( + (entity: ImpactTableEntityType) => + this.buildImpactTableRecursively(entity, entityMap, rangeOfYears), + ); impactTableDataByIndicator.yearSum = this.calculateIndicatorSumByYear( entityMap, @@ -179,34 +169,48 @@ export class ScenarioVsScenarioImpactService { } /** - * @description Recursive function that populates and returns - * aggregated data of parent entity and all its children + * @description Constructs the Impact Table and populates its aggregated values recursively + * according to the given entity and its children + * @param entity contains the entity tree to that will be used to build the Impact Table + * @param entityYearMap contains the actual values data for all entities */ - private populateValuesRecursively( - entity: ScenarioVsScenarioImpactTableRows, - entityDataMap: Map< + private buildImpactTableRecursively( + entity: ImpactTableEntityType, + entityYearMap: Map< string, Map >, rangeOfYears: number[], - ): ScenarioVsScenarioImpactTableRowsValues[] { - entity.values = []; - for (const year of rangeOfYears) { - const rowsValues: ScenarioVsScenarioImpactTableRowsValues = { - year: year, - baseScenarioValue: 0, - comparedScenarioValue: 0, - absoluteDifference: 0, - percentageDifference: 0, - isProjected: false, - }; - entity.values.push(rowsValues); - } + ): ScenarioVsScenarioImpactTableRows { + const impactTableRow: ScenarioVsScenarioImpactTableRows = { + name: entity.name || '', + values: rangeOfYears.map( + (year: number) => + ({ + year, + baseScenarioValue: 0, + comparedScenarioValue: 0, + absoluteDifference: 0, + percentageDifference: 0, + isProjected: false, + } as ScenarioVsScenarioImpactTableRowsValues), + ), + children: + entity.children?.length > 0 + ? entity.children.map((childEntity: ImpactTableEntityType) => + this.buildImpactTableRecursively( + childEntity, + entityYearMap, + rangeOfYears, + ), + ) + : [], + }; const valuesToAggregate: ScenarioVsScenarioImpactTableRowsValues[][] = []; const selfData: | Map - | undefined = entityDataMap.get(entity.name); + | undefined = entityYearMap.get(entity.id); if (selfData) { const sortedSelfData: ScenarioVsScenarioImpactTableRowsValues[] = Array.from(selfData.values()).sort( @@ -215,20 +219,14 @@ export class ScenarioVsScenarioImpactService { valuesToAggregate.push(sortedSelfData); } - entity.children.forEach( - (childEntity: ScenarioVsScenarioImpactTableRows) => { - //first aggregate data of child entity and then add returned value for parents aggregation - const childValues: ScenarioVsScenarioImpactTableRowsValues[] = - this.populateValuesRecursively( - childEntity, - entityDataMap, - rangeOfYears, - ); - valuesToAggregate.push(childValues); - }, - ); + for (const childEntity of impactTableRow.children) { + valuesToAggregate.push(childEntity.values); + } - for (const [valueIndex, entityRowValue] of entity.values.entries()) { + for (const [ + valueIndex, + entityRowValue, + ] of impactTableRow.values.entries()) { for (const valueToAggregate of valuesToAggregate) { entityRowValue.baseScenarioValue += valueToAggregate[valueIndex].baseScenarioValue; @@ -257,22 +255,7 @@ export class ScenarioVsScenarioImpactService { : percentageDifference; } } - return entity.values; - } - - private buildScenarioVsScenarioImpactTableRowsSkeleton( - entities: ImpactTableEntityType[], - ): ScenarioVsScenarioImpactTableRows[] { - return entities.map((item: ImpactTableEntityType) => { - return { - name: item.name || '', - children: - item.children?.length > 0 - ? this.buildScenarioVsScenarioImpactTableRowsSkeleton(item.children) - : [], - values: [], - }; - }); + return impactTableRow; } private static processTwoScenariosData( diff --git a/api/src/modules/impact/impact.repository.ts b/api/src/modules/impact/impact.repository.ts index 01027810b..6fc4fc62c 100644 --- a/api/src/modules/impact/impact.repository.ts +++ b/api/src/modules/impact/impact.repository.ts @@ -253,34 +253,34 @@ export class ImpactRepository { switch (impactDataDto.groupBy) { case GROUP_BY_VALUES.MATERIAL: selectQueryBuilder - .addSelect('material.name', 'name') - .groupBy('material.name'); + .addSelect('material.id', 'identifier') + .groupBy('material.id'); break; case GROUP_BY_VALUES.REGION: selectQueryBuilder - .addSelect('adminRegion.name', 'name') - .groupBy('adminRegion.name'); + .addSelect('adminRegion.id', 'identifier') + .groupBy('adminRegion.id'); break; case GROUP_BY_VALUES.T1_SUPPLIER: selectQueryBuilder - .addSelect('supplier.name', 'name') + .addSelect('supplier.id', 'identifier') .andWhere('supplier.name IS NOT NULL') - .groupBy('supplier.name'); + .groupBy('supplier.id'); break; case GROUP_BY_VALUES.PRODUCER: selectQueryBuilder - .addSelect('supplier.name', 'name') + .addSelect('supplier.id', 'identifier') .andWhere('supplier.name IS NOT NULL') - .groupBy('supplier.name'); + .groupBy('supplier.id'); break; case GROUP_BY_VALUES.BUSINESS_UNIT: selectQueryBuilder - .addSelect('businessUnit.name', 'name') - .groupBy('businessUnit.name'); + .addSelect('businessUnit.id', 'identifier') + .groupBy('businessUnit.id'); break; case GROUP_BY_VALUES.LOCATION_TYPE: selectQueryBuilder - .addSelect('sourcingLocation.locationType', 'name') + .addSelect('sourcingLocation.locationType', 'identifier') .groupBy('sourcingLocation.locationType'); break; default: @@ -292,7 +292,7 @@ export class ImpactRepository { `sourcingRecords.year, indicator.id, sourcingLocation.interventionType`, ) .orderBy('year', 'ASC') - .addOrderBy('name'); + .addOrderBy('identifier'); return selectQueryBuilder; } diff --git a/api/src/modules/impact/impact.service.ts b/api/src/modules/impact/impact.service.ts index eedc9ded6..6739992da 100644 --- a/api/src/modules/impact/impact.service.ts +++ b/api/src/modules/impact/impact.service.ts @@ -54,16 +54,19 @@ export class ImpactService { // Get full entity tree in cate ids are not passed, otherwise get trees based on // given ids and add children and parent ids to them to get full data for aggregations - const entities: ImpactTableEntityType[] = + const entitiesTree: ImpactTableEntityType[] = await this.baseService.getEntityTree(impactTableDto); this.baseService.getFlatListOfEntityIdsForLaterFiltering( impactTableDto, - entities, + entitiesTree, ); let dataForImpactTable: ImpactTableData[] = - await this.baseService.getDataForImpactTable(impactTableDto, entities); + await this.baseService.getDataForImpactTable( + impactTableDto, + entitiesTree, + ); if (impactTableDto.scenarioId) { dataForImpactTable = @@ -74,7 +77,7 @@ export class ImpactService { impactTableDto, indicators, dataForImpactTable, - entities, + entitiesTree, ); this.sortEntitiesByImpactOfYear( @@ -234,7 +237,7 @@ export class ImpactService { // construct result impact Table const impactTable: ImpactTableDataByIndicator[] = []; - for (const [indicatorId, entityMap] of indicatorEntityMap.entries()) { + for (const [indicatorId, entityYearMap] of indicatorEntityMap.entries()) { const indicator: Indicator = auxIndicatorMap.get( indicatorId, ) as Indicator; @@ -247,18 +250,13 @@ export class ImpactService { const yearSumMap: Map = this.postProcessYearIndicatorData( rangeOfYears, lastYearWithData, - entityMap, + entityYearMap, ); - // copy and populate tree skeleton for each indicator - const impactTableEntitySkeleton: ImpactTableRows[] = - this.buildImpactTableRowsSkeleton(entityTree); - - for (const entity of impactTableEntitySkeleton) { - this.populateValuesRecursively(entity, entityMap, rangeOfYears); - } - - impactTableDataByIndicator.rows = impactTableEntitySkeleton; + impactTableDataByIndicator.rows = entityTree.map( + (entity: ImpactTableEntityType) => + this.buildImpactTableRecursively(entity, entityYearMap, rangeOfYears), + ); impactTableDataByIndicator.yearSum.push( ...Array.from(yearSumMap).map(([year, sum]: [number, number]) => { @@ -280,22 +278,22 @@ export class ImpactService { /** * This functions does 2 things - * - fill any missing years in the entityies' yearMap, with the calculation based on previous years' data + * - fill any missing years in the entities' yearMap, with the calculation based on previous years' data * - calculate the value sum for all years, across all entities * @param rangeOfYears * @param lastYearWithData - * @param entityMap + * @param entityYearMap * @private */ private postProcessYearIndicatorData( rangeOfYears: number[], lastYearWithData: number, - entityMap: Map>, + entityYearMap: Map>, ): Map { - //We also calculate the yearsum for each indicator + //We also calculate the yearSum for each indicator const yearSumMap: Map = new Map(); - for (const yearMap of entityMap.values()) { + for (const yearMap of entityYearMap.values()) { const auxYearValues: number[] = []; for (const [index, year] of rangeOfYears.entries()) { @@ -312,11 +310,7 @@ export class ImpactService { lastYearsValue + (lastYearsValue * this.baseService.growthRate) / 100; - dataForYear = { - year, - value, - isProjected, - }; + dataForYear = { year, value, isProjected }; yearMap.set(year, dataForYear); } @@ -332,40 +326,56 @@ export class ImpactService { } /** - * @description Recursive function that populates and returns - * aggregated data of parent entity and all its children + * @description Constructs the Impact Table and populates its aggregated values recursively + * according to the given entity and its children + * @param entity contains the entity tree to that will be used to build the Impact Table + * @param entityYearMap contains the actual values data for all entities */ - private populateValuesRecursively( - entity: ImpactTableRows, - entityMap: Map>, + private buildImpactTableRecursively( + entity: ImpactTableEntityType, + entityYearMap: Map>, rangeOfYears: number[], - ): ImpactTableRowsValues[] { - entity.values = []; - for (const year of rangeOfYears) { - const rowsValues: ImpactTableRowsValues = { - year: year, - value: 0, - isProjected: false, - }; - entity.values.push(rowsValues); - } + ): ImpactTableRows { + // construct the ImpactTableRow instance, and its children recursively + const impactTableRow: ImpactTableRows = { + name: entity.name || '', + values: rangeOfYears.map( + (year: number) => + ({ year, value: 0, isProjected: false } as ImpactTableRowsValues), + ), + children: + entity.children?.length > 0 + ? entity.children.map((childEntity: ImpactTableEntityType) => + this.buildImpactTableRecursively( + childEntity, + entityYearMap, + rangeOfYears, + ), + ) + : [], + }; + // Prepare the values to be aggregated (self, if it exists, and children) const valuesToAggregate: ImpactTableRowsValues[][] = []; const selfData: Map | undefined = - entityMap.get(entity.name); + entityYearMap.get(entity.id); + if (selfData) { const sortedSelfData: ImpactTableRowsValues[] = Array.from( selfData.values(), ).sort(BaseImpactService.sortRowValueByYear); valuesToAggregate.push(sortedSelfData); } - entity.children.forEach((childEntity: ImpactTableRows) => { - valuesToAggregate.push( - // first aggregate data of child entity and then add returned value for parents aggregation - this.populateValuesRecursively(childEntity, entityMap, rangeOfYears), - ); - }); - for (const [valueIndex, entityRowValue] of entity.values.entries()) { + + for (const childEntity of impactTableRow.children) { + valuesToAggregate.push(childEntity.values); + } + + // Populate the RowsValues with the aggregation of the entity and its children + for (const [ + valueIndex, + entityRowValue, + ] of impactTableRow.values.entries()) { for (const valueToAggregate of valuesToAggregate) { entityRowValue.value += valueToAggregate[valueIndex].value; entityRowValue.isProjected = @@ -373,22 +383,8 @@ export class ImpactService { entityRowValue.isProjected; } } - return entity.values; - } - private buildImpactTableRowsSkeleton( - entities: ImpactTableEntityType[], - ): ImpactTableRows[] { - return entities.map((item: ImpactTableEntityType) => { - return { - name: item.name || '', - children: - item.children?.length > 0 - ? this.buildImpactTableRowsSkeleton(item.children) - : [], - values: [], - }; - }); + return impactTableRow; } private static processImpactDataWithScenario( diff --git a/api/src/modules/sourcing-records/sourcing-record.repository.ts b/api/src/modules/sourcing-records/sourcing-record.repository.ts index b7393b012..70bd167d5 100644 --- a/api/src/modules/sourcing-records/sourcing-record.repository.ts +++ b/api/src/modules/sourcing-records/sourcing-record.repository.ts @@ -15,7 +15,7 @@ export class ImpactTableData { year: number; indicatorId: string; indicatorShortName: string; - name: string; + identifier: string; tonnes: string; impact: number; typeByIntervention: SOURCING_LOCATION_TYPE_BY_INTERVENTION | null; diff --git a/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts b/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts index f707a49c5..f379504db 100644 --- a/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts +++ b/api/test/e2e/impact/mocks/actual-vs-scenario-responses/new-coefficients-intervention.response.ts @@ -180,10 +180,10 @@ export const newCoefficientsScenarioInterventionTable = { }, { year: 2022, - value: 5872.282499999999, + value: expect.closeTo(5872.282500000001), comparedScenarioValue: 5408.68125, - absoluteDifference: -463.6012499999997, - percentageDifference: -8.219178082191776, + absoluteDifference: expect.closeTo(-463.6012499999997), + percentageDifference: expect.closeTo(-8.219178082191776), isProjected: true, }, {