From a24db6af1cd1cbf157d3a62c5333c06ffa482585 Mon Sep 17 00:00:00 2001 From: Kevin Date: Thu, 21 Dec 2023 17:40:30 +0100 Subject: [PATCH] chore(Features): Applies special rounding to non legacy features' amount range when retrieving features --- .../src/modules/projects/projects.service.ts | 35 +++++--- api/apps/api/test/geo-features.e2e-spec.ts | 89 ++++++++++++++++++- 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/api/apps/api/src/modules/projects/projects.service.ts b/api/apps/api/src/modules/projects/projects.service.ts index 24faf3d097..d5f755347e 100644 --- a/api/apps/api/src/modules/projects/projects.service.ts +++ b/api/apps/api/src/modules/projects/projects.service.ts @@ -109,10 +109,7 @@ import { GetScenarioFailure } from '@marxan-api/modules/blm/values/blm-repos'; import stream from 'stream'; import { AppConfig } from '@marxan-api/utils/config.utils'; import { WebshotBasicPdfConfig } from '@marxan/webshot/webshot.dto'; -import { - ScenariosService, - SubmitProtectedAreaError, -} from '@marxan-api/modules/scenarios/scenarios.service'; +import { ScenariosService } from '@marxan-api/modules/scenarios/scenarios.service'; import { OutputProjectSummariesService, outputProjectSummaryNotFound, @@ -126,6 +123,7 @@ import { import { ensureShapefileHasRequiredFiles } from '@marxan-api/utils/file-uploads.utils'; import { CostSurfaceService } from '@marxan-api/modules/cost-surface/cost-surface.service'; import { GeoFeature } from '../geo-features/geo-feature.api.entity'; + export { validationFailed } from '../planning-areas'; export const projectNotFound = Symbol(`project not found`); @@ -192,7 +190,10 @@ export class ProjectsService { data: result.data.map((feature) => { return { ...feature, - amountRange: this.transformMinMaxAmountsFromSquareMetresToSquareKmsForFeaturesFromShapefile(feature), + amountRange: + this.transformMinMaxAmountsFromSquareMetresToSquareKmsForFeaturesFromShapefile( + feature, + ), }; }), metadata: result.metadata, @@ -210,12 +211,24 @@ export class ProjectsService { * report them in square km rather than in square metres (they are stored * in square metres in the platform's backend). */ - transformMinMaxAmountsFromSquareMetresToSquareKmsForFeaturesFromShapefile(feature: Partial | undefined): { min: number | null, max: number | null } { - const min = feature?.amountMin ? (feature.isLegacy ? feature.amountMin : feature.amountMin / 1_000_000) : null; - const max = feature?.amountMax ? (feature.isLegacy ? feature.amountMax : feature.amountMax / 1_000_000) : null; - return { - min, max - }; + transformMinMaxAmountsFromSquareMetresToSquareKmsForFeaturesFromShapefile( + feature: Partial | undefined, + ): { min: number | null; max: number | null } { + let minResult = feature?.amountMin ? feature?.amountMin : null; + let maxResult = feature?.amountMax ? feature?.amountMax : null; + if (!feature?.isLegacy) { + if (feature?.amountMin) { + const minKm2 = feature.amountMin / 1_000_000; + minResult = + minKm2 < 1 ? parseFloat(minKm2.toFixed(4)) : Math.round(minKm2); + } + if (feature?.amountMax) { + const maxKm2 = feature.amountMax / 1_000_000; + maxResult = + maxKm2 < 1 ? parseFloat(maxKm2.toFixed(4)) : Math.round(maxKm2); + } + } + return { min: minResult, max: maxResult }; } async findAll(fetchSpec: FetchSpecification, info: ProjectsServiceRequest) { diff --git a/api/apps/api/test/geo-features.e2e-spec.ts b/api/apps/api/test/geo-features.e2e-spec.ts index 265a60a725..6993ed8f0d 100644 --- a/api/apps/api/test/geo-features.e2e-spec.ts +++ b/api/apps/api/test/geo-features.e2e-spec.ts @@ -20,7 +20,6 @@ import { range } from 'lodash'; import { GeoFeatureGeometry } from '@marxan/geofeatures'; import { Scenario } from '@marxan-api/modules/scenarios/scenario.api.entity'; import { FixtureType } from '@marxan/utils/tests/fixture-type'; -import { GivenUserExists } from './steps/given-user-exists'; import * as fs from 'fs/promises'; import * as path from 'path'; @@ -71,6 +70,74 @@ describe('GeoFeaturesModule (e2e)', () => { }); }); + test('should include the min and max amount of the features in the proper units of measure', async () => { + const projectId = world.projectWithCountry; // doesn't really matter how the project is created for this test + const sqlName = 'generic_namidia_feature_data'; + const featureId1 = await fixtures.GivenFeatureWithData( + 'legacy', + sqlName, + projectId, + ); + const featureId2 = await fixtures.GivenFeatureWithData( + 'nonLegacyBelow1', + sqlName, + projectId, + ); + const featureId3 = await fixtures.GivenFeatureWithData( + 'nonLegacyOver1', + sqlName, + projectId, + ); + await fixtures.GivenMinMaxAmountForFeature( + featureId1, + true, + 22304094, + 0.1234, + ); + await fixtures.GivenMinMaxAmountForFeature( + featureId2, + false, + 123456, + 567890, + ); + await fixtures.GivenMinMaxAmountForFeature( + featureId3, + false, + 123456789, + 567891234, + ); + const response = await request(app.getHttpServer()) + .get(`/api/v1/projects/${world.projectWithCountry}/features`) + .set('Authorization', `Bearer ${jwtToken}`) + .expect(HttpStatus.OK); + + const geoFeaturesForProject: any[] = response.body.data + .filter((element: any) => + [featureId1, featureId2, featureId3].includes(element.id), + ) + .sort((element: any) => element.name); + expect(geoFeaturesForProject.length).toBe(3); + expect(geoFeaturesForProject[0].attributes.featureClassName).toBe('legacy'); + expect(geoFeaturesForProject[0].attributes.amountRange).toEqual({ + min: 22304094, + max: 0.1234, + }); + expect(geoFeaturesForProject[1].attributes.featureClassName).toBe( + 'nonLegacyBelow1', + ); + expect(geoFeaturesForProject[1].attributes.amountRange).toEqual({ + min: parseFloat((0.123456).toFixed(4)), + max: parseFloat((0.56789).toFixed(4)), + }); + expect(geoFeaturesForProject[2].attributes.featureClassName).toBe( + 'nonLegacyOver1', + ); + expect(geoFeaturesForProject[2].attributes.amountRange).toEqual({ + min: Math.round(123.456789), + max: Math.round(567.891234), + }); + }); + test('should include correct scenarioUsageCounts for the given project', async () => { // This tests accounts for both cases of having platform wide features and custom features assigned to projects @@ -215,6 +282,21 @@ export const getGeoFeatureFixtures = async (app: INestApplication) => { ); return { + GivenMinMaxAmountForFeature: async ( + featureId: string, + isLegacy: boolean, + min: number, + max: number, + ) => { + const feature = await featureRepo.findOneOrFail({ + where: { id: featureId }, + }); + feature.isLegacy = isLegacy; + feature.amountMin = min; + feature.amountMax = max; + + await featureRepo.save(feature); + }, GivenFeatureWithData: async ( name: string, sqlFilename: string, @@ -244,9 +326,8 @@ export const getGeoFeatureFixtures = async (app: INestApplication) => { ); const content = await fs.readFile(filePath, 'utf-8'); - return geoEntityManager.query( - content.replace(/\$feature_id/g, featureId), - ); + await geoEntityManager.query(content.replace(/\$feature_id/g, featureId)); + return featureId; }, GivenScenarioFeaturesData: async ( featureClassName: string,