diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index c1b8131..fbe8ac1 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -12,6 +12,7 @@ import { UsersModule } from './users/users.module'; import { User } from './users/entities/user.entity'; import { SearchModule } from './search/search.module'; import { AuthModule } from './auth/auth.module'; +import { RiskModule } from './risk/risk.module'; import { ReportingModule } from './reporting/reporting.module'; @Module({ @@ -47,6 +48,7 @@ import { ReportingModule } from './reporting/reporting.module'; UsersModule, SearchModule, AuthModule, + RiskModule, ReportingModule, ], controllers: [AppController, NotificationsController], diff --git a/backend/src/risk/dto/calculate-risk.dto.ts b/backend/src/risk/dto/calculate-risk.dto.ts new file mode 100644 index 0000000..5f7c564 --- /dev/null +++ b/backend/src/risk/dto/calculate-risk.dto.ts @@ -0,0 +1,22 @@ +import { IsIn, IsNotEmpty, IsString } from 'class-validator'; + +const validCargoTypes = ['GENERAL', 'HAZARDOUS', 'PERISHABLE'] as const; +type CargoType = (typeof validCargoTypes)[number]; + +export class CalculateRiskDto { + @IsNotEmpty() + @IsIn(validCargoTypes) + cargoType: CargoType; + + @IsString() + @IsNotEmpty() + originCountry: string; + + @IsString() + @IsNotEmpty() + destinationCountry: string; + + @IsString() + @IsNotEmpty() + carrierId: string; +} diff --git a/backend/src/risk/risk.controller.ts b/backend/src/risk/risk.controller.ts new file mode 100644 index 0000000..7a20c87 --- /dev/null +++ b/backend/src/risk/risk.controller.ts @@ -0,0 +1,15 @@ +import { Body, Controller, Post, ValidationPipe } from '@nestjs/common'; +import { CalculateRiskDto } from './dto/calculate-risk.dto'; +import { RiskScoreResponse, RiskService } from './risk.service'; + +@Controller('risk') +export class RiskController { + constructor(private readonly riskService: RiskService) {} + + @Post('score') + getRiskScore( + @Body(new ValidationPipe()) details: CalculateRiskDto, + ): RiskScoreResponse { + return this.riskService.calculateRiskScore(details); + } +} diff --git a/backend/src/risk/risk.module.ts b/backend/src/risk/risk.module.ts new file mode 100644 index 0000000..4d9f1fe --- /dev/null +++ b/backend/src/risk/risk.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { RiskController } from './risk.controller'; +import { RiskService } from './risk.service'; + +@Module({ + controllers: [RiskController], + providers: [RiskService], +}) +export class RiskModule {} diff --git a/backend/src/risk/risk.service.ts b/backend/src/risk/risk.service.ts new file mode 100644 index 0000000..bf3b2b4 --- /dev/null +++ b/backend/src/risk/risk.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from '@nestjs/common'; +import { CalculateRiskDto } from './dto/calculate-risk.dto'; + +export interface RiskScoreResponse { + finalScore: number; + riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + breakdown: { + cargo: number; + geopolitical: number; + carrier: number; + }; +} + +@Injectable() +export class RiskService { + private highRiskCountries = ['Somalia', 'Yemen', 'Syria']; + private unreliableCarriers = ['carrier-xyz', 'carrier-abc']; + + calculateRiskScore(details: CalculateRiskDto): RiskScoreResponse { + const weights = { cargo: 0.4, geopolitical: 0.4, carrier: 0.2 }; + + const cargoScore = this.scoreCargo(details.cargoType); + const geopoliticalScore = this.scoreGeopolitical( + details.destinationCountry, + ); + const carrierScore = this.scoreCarrier(details.carrierId); + + const finalScore = + cargoScore * weights.cargo + + geopoliticalScore * weights.geopolitical + + carrierScore * weights.carrier; + + return { + finalScore: Math.round(finalScore), + riskLevel: this.getRiskLevel(finalScore), + breakdown: { + cargo: cargoScore, + geopolitical: geopoliticalScore, + carrier: carrierScore, + }, + }; + } + + private scoreCargo(cargoType: CalculateRiskDto['cargoType']): number { + switch (cargoType) { + case 'HAZARDOUS': + return 90; + case 'PERISHABLE': + return 60; + case 'GENERAL': + return 10; + default: + return 0; + } + } + + private scoreGeopolitical(country: string): number { + return this.highRiskCountries.includes(country) ? 100 : 10; + } + + private scoreCarrier(carrierId: string): number { + return this.unreliableCarriers.includes(carrierId) ? 80 : 20; + } + + private getRiskLevel(score: number): RiskScoreResponse['riskLevel'] { + if (score > 80) return 'CRITICAL'; + if (score > 60) return 'HIGH'; + if (score > 30) return 'MEDIUM'; + return 'LOW'; + } +}