From 18dc8d0df0580b66a98f443b1b5bdd0612a32f76 Mon Sep 17 00:00:00 2001 From: Catalin Oancea Date: Tue, 5 Nov 2024 19:22:23 +0200 Subject: [PATCH] Add filters --- .../modules/projects/projects.controller.ts | 14 +++- api/src/modules/projects/projects.service.ts | 68 ++++++++++++++++- .../integration/projects/projects.spec.ts | 75 +++++++++++++++++++ 3 files changed, 154 insertions(+), 3 deletions(-) diff --git a/api/src/modules/projects/projects.controller.ts b/api/src/modules/projects/projects.controller.ts index 21689cc1..54771009 100644 --- a/api/src/modules/projects/projects.controller.ts +++ b/api/src/modules/projects/projects.controller.ts @@ -23,7 +23,19 @@ export class ProjectsController { @TsRestHandler(projectsContract.getProjects) async getProjects(): ControllerResponse { return tsRestHandler(projectsContract.getProjects, async ({ query }) => { - const data = await this.projectsService.findAllPaginated(query); + // The following filters do not work out of the box with the BaseService implementation + const otherFilters = { + costRange: query.filter?.costRange, + abatementPotentialRange: query.filter?.abatementPotentialRange, + costRangeSelector: query.filter?.costRangeSelector, + }; + delete query.filter?.costRange; + delete query.filter?.abatementPotentialRange; + delete query.filter?.costRangeSelector; + + const data = await this.projectsService.findAllPaginated(query, { + otherFilters: otherFilters, + }); return { body: data, status: HttpStatus.OK }; }); } diff --git a/api/src/modules/projects/projects.service.ts b/api/src/modules/projects/projects.service.ts index aa214f5e..e8d9dc9b 100644 --- a/api/src/modules/projects/projects.service.ts +++ b/api/src/modules/projects/projects.service.ts @@ -1,9 +1,17 @@ +import { FetchSpecification } from 'nestjs-base-service'; import { Injectable } from '@nestjs/common'; import { AppBaseService } from '@api/utils/app-base.service'; import { Project } from '@shared/entities/projects.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, SelectQueryBuilder } from 'typeorm'; -import { FetchSpecification } from 'nestjs-base-service'; + +interface ExtendedFetchSpecification extends FetchSpecification { + otherFilters?: { + abatementPotentialRange?: number[]; + costRange?: number[]; + costRangeSelector?: 'npv' | 'total'; + }; +} @Injectable() export class ProjectsService extends AppBaseService< @@ -21,7 +29,7 @@ export class ProjectsService extends AppBaseService< async extendFindAllQuery( query: SelectQueryBuilder, - fetchSpecification: FetchSpecification, + fetchSpecification: ExtendedFetchSpecification, ): Promise> { // Filter by project name if (fetchSpecification?.filter?.projectName) { @@ -29,6 +37,62 @@ export class ProjectsService extends AppBaseService< projectName: `%${fetchSpecification.filter.projectName}%`, }); } + + // Filter by abatement potential + if (this.isAbatementPotentialFilterValid(fetchSpecification)) { + query = query.andWhere( + 'abatement_potential >= :minAP AND abatement_potential <= :maxAP', + { + minAP: fetchSpecification.otherFilters.abatementPotentialRange[0], + maxAP: fetchSpecification.otherFilters.abatementPotentialRange[1], + }, + ); + } + + // Filter by cost (total or NPV) + if (this.isCostFilterValid(fetchSpecification)) { + let filteredCostColumn: string; + switch (fetchSpecification.otherFilters.costRangeSelector) { + case 'npv': + filteredCostColumn = 'total_cost_npv'; + break; + case 'total': + default: + filteredCostColumn = 'total_cost'; + break; + } + + query = query.andWhere( + `${filteredCostColumn} >= :minCost AND ${filteredCostColumn} <= :maxCost`, + { + minCost: fetchSpecification.otherFilters.costRange[0], + maxCost: fetchSpecification.otherFilters.costRange[1], + }, + ); + } return query; } + + private isCostFilterValid( + fetchSpecification: ExtendedFetchSpecification, + ): boolean { + return !!( + fetchSpecification?.otherFilters?.costRange && + fetchSpecification?.otherFilters?.costRange.length === 2 && + fetchSpecification?.otherFilters?.costRange[0] <= + fetchSpecification?.otherFilters?.costRange[1] && + fetchSpecification?.otherFilters?.costRangeSelector + ); + } + + private isAbatementPotentialFilterValid( + fetchSpecification: ExtendedFetchSpecification, + ): boolean { + return !!( + fetchSpecification?.otherFilters?.abatementPotentialRange && + fetchSpecification?.otherFilters?.abatementPotentialRange.length === 2 && + fetchSpecification?.otherFilters?.abatementPotentialRange[0] <= + fetchSpecification?.otherFilters?.abatementPotentialRange[1] + ); + } } diff --git a/api/test/integration/projects/projects.spec.ts b/api/test/integration/projects/projects.spec.ts index 9079f8dd..370332fe 100644 --- a/api/test/integration/projects/projects.spec.ts +++ b/api/test/integration/projects/projects.spec.ts @@ -87,6 +87,81 @@ describe('Projects', () => { ); }); + test('Should return a list of projects filtered by min/max NPV cost', async () => { + const projects: Project[] = []; + projects.push( + await testManager.mocks().createProject({ totalCostNPV: 25 }), + await testManager.mocks().createProject({ totalCostNPV: 15 }), + await testManager.mocks().createProject({ totalCostNPV: 45 }), + await testManager.mocks().createProject({ totalCostNPV: 10 }), + ); + + const response = await testManager + .request() + .get(projectsContract.getProjects.path) + .query({ + filter: { + costRangeSelector: 'npv', + costRange: [12, 26], + }, + }); + expect(response.body.data).toHaveLength(2); + expect( + response.body.data + .map((project: Project) => project.totalCostNPV) + .sort(), + ).toEqual(['15', '25']); + }); + + test('Should return a list of projects filtered by min/max total cost', async () => { + const projects: Project[] = []; + projects.push( + await testManager.mocks().createProject({ totalCost: 25 }), + await testManager.mocks().createProject({ totalCost: 15 }), + await testManager.mocks().createProject({ totalCost: 45 }), + await testManager.mocks().createProject({ totalCost: 10 }), + ); + + const response = await testManager + .request() + .get(projectsContract.getProjects.path) + .query({ + filter: { + costRangeSelector: 'total', + costRange: [12, 26], + }, + }); + expect(response.body.data).toHaveLength(2); + expect( + response.body.data.map((project: Project) => project.totalCost).sort(), + ).toEqual(['15', '25']); + }); + + test('Should return a list of projects filtered by min/max abatement potential', async () => { + const projects: Project[] = []; + projects.push( + await testManager.mocks().createProject({ abatementPotential: 25 }), + await testManager.mocks().createProject({ abatementPotential: 15 }), + await testManager.mocks().createProject({ abatementPotential: 45 }), + await testManager.mocks().createProject({ abatementPotential: 10 }), + ); + + const response = await testManager + .request() + .get(projectsContract.getProjects.path) + .query({ + filter: { + abatementPotentialRange: [12, 26], + }, + }); + expect(response.body.data).toHaveLength(2); + expect( + response.body.data + .map((project: Project) => project.abatementPotential) + .sort(), + ).toEqual(['15', '25']); + }); + test('Should return a list of projects filtered by project name', async () => { const projects: Project[] = []; projects.push(