Skip to content

Commit

Permalink
Add active status validation for materials in impact and scenario-int…
Browse files Browse the repository at this point in the history
…erventions endpoints
  • Loading branch information
yulia-bel authored and alexeh committed Feb 15, 2023
1 parent fbc0924 commit 5c4d111
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 19 deletions.
44 changes: 30 additions & 14 deletions api/src/modules/impact/impact.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { ActualVsScenarioImpactService } from 'modules/impact/comparison/actual-
import { SetScenarioIdsInterceptor } from 'modules/impact/set-scenario-ids.interceptor';
import { ScenarioVsScenarioImpactService } from 'modules/impact/comparison/scenario-vs-scenario.service';
import { ScenarioVsScenarioPaginatedImpactTable } from 'modules/impact/dto/response-scenario-scenario.dto';
import { IndicatorsService } from 'modules/indicators/indicators.service';
import { MaterialsService } from 'modules/materials/materials.service';

@Controller('/api/v1/impact')
@ApiTags('Impact')
Expand All @@ -41,7 +41,7 @@ export class ImpactController {
private readonly impactService: ImpactService,
private readonly actualVsScenarioImpactService: ActualVsScenarioImpactService,
private readonly scenarioVsScenarioService: ScenarioVsScenarioImpactService,
private readonly indicatorService: IndicatorsService,
private readonly materialsService: MaterialsService,
) {}

@ApiOperation({
Expand All @@ -57,9 +57,13 @@ export class ImpactController {
@ProcessFetchSpecification() fetchSpecification: FetchSpecification,
@Query(ValidationPipe) impactTableDto: GetImpactTableDto,
): Promise<PaginatedImpactTable> {
await this.indicatorService.checkActiveIndicatorsForCalculations(
impactTableDto.indicatorIds,
);
/* Here we are validating received materialIds to be active, without validating recursively possible descendants,
since the Material Ids come from existing impact data (materials present in Sourcing Locations) and existing descendants
will be active per default */
if (impactTableDto.materialIds)
await this.materialsService.checkActiveMaterials(
impactTableDto.materialIds,
);
return await this.impactService.getImpactTable(
impactTableDto,
fetchSpecification,
Expand All @@ -80,9 +84,13 @@ export class ImpactController {
@Query(ValidationPipe)
scenarioVsScenarioImpactTableDto: GetScenarioVsScenarioImpactTableDto,
): Promise<ScenarioVsScenarioPaginatedImpactTable> {
await this.indicatorService.checkActiveIndicatorsForCalculations(
scenarioVsScenarioImpactTableDto.indicatorIds,
);
/* Here we are validating received materialIds to be active, without validating recursively possible descendants,
since the Material Ids come from existing impact data (materials present in Sourcing Locations) and existing descendants
will be active per default */
if (scenarioVsScenarioImpactTableDto.materialIds)
await this.materialsService.checkActiveMaterials(
scenarioVsScenarioImpactTableDto.materialIds,
);
return await this.scenarioVsScenarioService.getScenarioVsScenarioImpactTable(
scenarioVsScenarioImpactTableDto,
fetchSpecification,
Expand All @@ -104,9 +112,13 @@ export class ImpactController {
@Query(ValidationPipe)
actualVsScenarioImpactTableDto: GetActualVsScenarioImpactTableDto,
): Promise<PaginatedImpactTable> {
await this.indicatorService.checkActiveIndicatorsForCalculations(
actualVsScenarioImpactTableDto.indicatorIds,
);
/* Here we are validating received materialIds to be active, without validating recursively possible descendants,
since the Material Ids come from existing impact data (materials present in Sourcing Locations) and existing descendants
will be active per default */
if (actualVsScenarioImpactTableDto.materialIds)
await this.materialsService.checkActiveMaterials(
actualVsScenarioImpactTableDto.materialIds,
);
return await this.actualVsScenarioImpactService.getActualVsScenarioImpactTable(
actualVsScenarioImpactTableDto,
fetchSpecification,
Expand All @@ -127,9 +139,13 @@ export class ImpactController {
async getRankedImpactTable(
@Query(ValidationPipe) rankedImpactTableDto: GetRankedImpactTableDto,
): Promise<ImpactTable> {
await this.indicatorService.checkActiveIndicatorsForCalculations(
rankedImpactTableDto.indicatorIds,
);
/* Here we are validating received materialIds to be active, without validating recursively possible descendants,
since the Material Ids come from existing impact data (materials present in Sourcing Locations) and existing descendants
will be active per default */
if (rankedImpactTableDto.materialIds)
await this.materialsService.checkActiveMaterials(
rankedImpactTableDto.materialIds,
);
return await this.impactService.getRankedImpactTable(rankedImpactTableDto);
}
}
4 changes: 4 additions & 0 deletions api/src/modules/impact/impact.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { SuppliersModule } from 'modules/suppliers/suppliers.module';
import { MaterialsModule } from 'modules/materials/materials.module';
import { ActualVsScenarioImpactService } from 'modules/impact/comparison/actual-vs-scenario.service';
import { ScenarioVsScenarioImpactService } from 'modules/impact/comparison/scenario-vs-scenario.service';
import { MaterialsService } from '../materials/materials.service';
import { SourcingLocationsModule } from 'modules/sourcing-locations/sourcing-locations.module';
import { IndicatorsService } from 'modules/indicators/indicators.service';

@Module({
Expand All @@ -19,12 +21,14 @@ import { IndicatorsService } from 'modules/indicators/indicators.service';
AdminRegionsModule,
SuppliersModule,
MaterialsModule,
SourcingLocationsModule,
],
providers: [
ImpactService,
ActualVsScenarioImpactService,
ScenarioVsScenarioImpactService,
IndicatorsService,
MaterialsService,
],
controllers: [ImpactController],
exports: [
Expand Down
2 changes: 1 addition & 1 deletion api/src/modules/materials/materials.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ import { MaterialRepository } from 'modules/materials/material.repository';
],
controllers: [MaterialsController],
providers: [MaterialsService, MaterialsToH3sService, MaterialRepository],
exports: [MaterialsService, MaterialsToH3sService],
exports: [MaterialsService, MaterialsToH3sService, MaterialRepository],
})
export class MaterialsModule {}
22 changes: 22 additions & 0 deletions api/src/modules/materials/materials.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BadRequestException,
forwardRef,
HttpException,
Inject,
Expand Down Expand Up @@ -281,4 +282,25 @@ export class MaterialsService extends AppBaseService<
})),
);
}

async checkActiveMaterials(materialIds: string[]): Promise<void> {
const inactiveSelectedMaterials: Material[] =
await this.materialRepository.find({
where: {
id: In(materialIds),
status: MATERIALS_STATUS.INACTIVE,
},
});

if (inactiveSelectedMaterials.length) {
const inactiveMaterialNames: string[] = inactiveSelectedMaterials.map(
(material: Material) => material.name,
);
throw new BadRequestException(
`Following Requested Materials are not activated: ${inactiveMaterialNames.join(
', ',
)}`,
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,15 @@ import { CreateScenarioInterventionDtoV2 } from 'modules/scenario-interventions/
import { UpdateScenarioInterventionDto } from 'modules/scenario-interventions/dto/update.scenario-intervention.dto';
import { PaginationMeta } from 'utils/app-base.service';
import { SetUserInterceptor } from 'decorators/set-user.interceptor';
import { MaterialsService } from 'modules/materials/materials.service';

@Controller(`/api/v1/scenario-interventions`)
@ApiTags(scenarioResource.className)
@ApiBearerAuth()
export class ScenarioInterventionsControllerV2 {
constructor(
public readonly scenarioInterventionsService: ScenarioInterventionsService,
public readonly materialsService: MaterialsService,
) {}

@ApiOperation({
Expand Down Expand Up @@ -95,6 +97,8 @@ export class ScenarioInterventionsControllerV2 {
async create(
@Body() dto: CreateScenarioInterventionDtoV2,
): Promise<Partial<ScenarioIntervention>> {
if (dto.newMaterialId)
await this.materialsService.checkActiveMaterials([dto.newMaterialId]);
return await this.scenarioInterventionsService.serialize(
await this.scenarioInterventionsService.createScenarioIntervention(dto),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ import { ScenarioInterventionsControllerV2 } from 'modules/scenario-intervention
import { ActiveIndicatorValidator } from 'modules/indicators/validators/active-indicator.validator';
import { ScenarioInterventionRepository } from 'modules/scenario-interventions/scenario-intervention.repository';
import { AuthorizationModule } from 'modules/authorization/authorization.module';


import { MaterialsService } from 'modules/materials/materials.service';

@Module({
imports: [
Expand All @@ -42,6 +41,7 @@ import { AuthorizationModule } from 'modules/authorization/authorization.module'
NewSupplierLocationIntervention,
ChangeProductionEfficiencyIntervention,
ActiveIndicatorValidator,
MaterialsService,
],
exports: [ScenarioInterventionRepository, ScenarioInterventionsService],
})
Expand Down
32 changes: 31 additions & 1 deletion api/test/e2e/impact/impact-table/impact.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
INDICATOR_TYPES,
} from 'modules/indicators/indicator.entity';
import { BusinessUnit } from 'modules/business-units/business-unit.entity';
import { Material } from 'modules/materials/material.entity';
import { Material, MATERIALS_STATUS } from 'modules/materials/material.entity';
import {
LOCATION_TYPES,
SourcingLocation,
Expand Down Expand Up @@ -129,6 +129,36 @@ describe('Impact Table and Charts test suite (e2e)', () => {
);
});

test('When I query the API for a Impact Table filtering by Inactive material, then I should get a proper errors message ', async () => {
const unit: Unit = await createUnit({ shortName: 'fakeUnit' });
const indicator: Indicator = await createIndicator({
name: 'Fake Indicator',
unit,
nameCode: INDICATOR_TYPES.DEFORESTATION,
status: INDICATOR_STATUS.ACTIVE,
});

const material: Material = await createMaterial({
name: 'Fake Material',
status: MATERIALS_STATUS.INACTIVE,
});
const response = await request(testApplication.getHttpServer())
.get('/api/v1/impact/table')
.set('Authorization', `Bearer ${jwtToken}`)
.query({
'indicatorIds[]': [indicator.id],
'materialIds[]': [material.id],
endYear: 1,
startYear: 2,
groupBy: 'material',
})
.expect(HttpStatus.BAD_REQUEST);

expect(response.body.errors[0].title).toEqual(
'Following Requested Materials are not activated: Fake Material',
);
});

test('When I query the API for a Impact Table for inactive indicators then I should get a proper error message', async () => {
const inactiveIndicator: Indicator = await createIndicator({
name: 'Inactive Indicator 1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from 'modules/indicators/indicator.entity';
import {
createIndicator,
createMaterial,
createScenarioIntervention,
} from '../../../entity-mocks';
import {
Expand All @@ -22,6 +23,11 @@ import ApplicationManager, {
import { Test } from '@nestjs/testing';
import { AppModule } from 'app.module';
import { ScenarioInterventionsService } from 'modules/scenario-interventions/scenario-interventions.service';
import {
Material,
MATERIALS_STATUS,
} from '../../../../src/modules/materials/material.entity';
import { HttpStatus } from '@nestjs/common';

describe('Interventions E2E Tests (Controller Validations)', () => {
let jwtToken: string;
Expand Down Expand Up @@ -312,4 +318,67 @@ describe('Interventions E2E Tests (Controller Validations)', () => {
).not.toContain(inactiveIndicators.join(', '));
},
);

test(
'When I Create new intervention with replacing material ' +
+'And the replacing material is active ' +
+'Then validation shall pass',

async () => {
const material: Material = await createMaterial();
const response = await request(testApplication.getHttpServer())
.post('/api/v1/scenario-interventions')
.set('Authorization', `Bearer ${jwtToken}`)
.send({
title: 'test scenario intervention',
startYear: 2025,
percentage: 50,
scenarioId: uuidv4(),
materialIds: [uuidv4()],
supplierIds: [uuidv4()],
businessUnitIds: [uuidv4()],
adminRegionIds: [uuidv4()],
type: SCENARIO_INTERVENTION_TYPE.NEW_MATERIAL,
newLocationCountryInput: 'TestCountry',
newLocationType: 'unknown',
newMaterialId: material.id,
});

expect(response.body.errors).toBeUndefined();
},
);

test(
'When I Create new intervention with replacing material ' +
+'But the replacing material is Inactive ' +
+'Then I should get a relevant error message',
async () => {
const material: Material = await createMaterial({
name: 'Inactive Material',
status: MATERIALS_STATUS.INACTIVE,
});
const response = await request(testApplication.getHttpServer())
.post('/api/v1/scenario-interventions')
.set('Authorization', `Bearer ${jwtToken}`)
.send({
title: 'test scenario intervention',
startYear: 2025,
percentage: 50,
scenarioId: uuidv4(),
materialIds: [uuidv4()],
supplierIds: [uuidv4()],
businessUnitIds: [uuidv4()],
adminRegionIds: [uuidv4()],
type: SCENARIO_INTERVENTION_TYPE.NEW_MATERIAL,
newLocationCountryInput: 'TestCountry',
newLocationType: 'unknown',
newMaterialId: material.id,
});

expect(response.status).toBe(HttpStatus.BAD_REQUEST);
expect(response.body.errors[0].title).toEqual(
'Following Requested Materials are not activated: Inactive Material',
);
},
);
});
3 changes: 2 additions & 1 deletion api/test/entity-mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
SCENARIO_INTERVENTION_TYPE,
ScenarioIntervention,
} from 'modules/scenario-interventions/scenario-intervention.entity';
import { Material } from 'modules/materials/material.entity';
import { Material, MATERIALS_STATUS } from 'modules/materials/material.entity';
import { Supplier } from 'modules/suppliers/supplier.entity';
import { SourcingRecord } from 'modules/sourcing-records/sourcing-record.entity';
import { IndicatorRecord } from 'modules/indicator-records/indicator-record.entity';
Expand Down Expand Up @@ -268,6 +268,7 @@ async function createMaterial(
const defaultData: DeepPartial<Material> = {
name: 'Material name',
hsCodeId: uuidv4(),
status: MATERIALS_STATUS.ACTIVE,
};

const material = Material.merge(new Material(), defaultData, additionalData);
Expand Down

0 comments on commit 5c4d111

Please sign in to comment.