diff --git a/application/frontend/src/app/core/containers/csv-upload-dialog/csv-upload-dialog.component.ts b/application/frontend/src/app/core/containers/csv-upload-dialog/csv-upload-dialog.component.ts index 5ceac1e4..ddbd4010 100644 --- a/application/frontend/src/app/core/containers/csv-upload-dialog/csv-upload-dialog.component.ts +++ b/application/frontend/src/app/core/containers/csv-upload-dialog/csv-upload-dialog.component.ts @@ -1240,7 +1240,24 @@ export class CsvUploadDialogComponent implements OnDestroy, OnInit { shipments = shipmentsResults.map((result) => result.shipment); } if (this.vehicleFile) { - vehicles = this.service.csvToVehicles(res[1].data, this.mappingFormVehicles.value); + const vehiclesResults = this.service.csvToVehicles( + res[1].data, + this.mappingFormVehicles.value + ); + + if (vehiclesResults.some((result) => result.errors.length)) { + vehiclesResults.forEach((result, index) => { + this.validationErrors.push( + ...result.errors.map( + (error) => + `Vehicle ${result.vehicle.label || ''} at index ${index}: ${error.message}` + ) + ); + }); + throw Error('Vehicle validation error'); + } + + vehicles = vehiclesResults.map((result) => result.vehicle); } // geocode all shipments, then all vehicles diff --git a/application/frontend/src/app/core/services/csv.service.spec.ts b/application/frontend/src/app/core/services/csv.service.spec.ts index e5864ed5..53226aaa 100644 --- a/application/frontend/src/app/core/services/csv.service.spec.ts +++ b/application/frontend/src/app/core/services/csv.service.spec.ts @@ -102,4 +102,157 @@ describe('CsvService', () => { expect(parsedCsv).not.toContain(key); }); }); + + describe('Validate shipments', () => { + it('should not throw error on a valid shipment', () => { + const shipments = [{ label: 'test shipment' }]; + const testMapping = { Label: 'label' }; + const result = service.csvToShipments(shipments, testMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(0); + }); + + it('should not throw error on shipment with a valid load demand', () => { + const shipments = [ + { + LoadDemand1Type: 'weight', + LoadDemand1Value: 10, + }, + ]; + const testMapping = { + LoadDemand1Type: 'LoadDemand1Type', + LoadDemand1Value: 'LoadDemand1Value', + }; + const result = service.csvToShipments(shipments, testMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(0); + }); + + it('should throw an error on shipment with an invalid load demand', () => { + const shipments = [ + { + LoadDemand1Type: 'weight', + LoadDemand1Value: 'invalid', + }, + { + LoadDemand1Type: 'weight', + LoadDemand1Value: 10.5, + }, + ]; + const testMapping = { + LoadDemand1Type: 'LoadDemand1Type', + LoadDemand1Value: 'LoadDemand1Value', + }; + const result = service.csvToShipments(shipments, testMapping); + expect(result.length).toBe(2); + expect(result[0].errors.length).toBe(1); + expect(result[1].errors.length).toBe(1); + }); + }); + + describe('Validate vehicles', () => { + it('should throw no errors on a valid vehicle', () => { + const vehicles = [{ label: 'test vehicle' }]; + const testVehicleMapping = { Label: 'label' }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(0); + }); + + it('should throw no errors on a vehicle with a valid load limit', () => { + const vehicles = [ + { + label: 'test vehicle', + LoadLimit1Type: 'weight', + LoadLimit1Value: 10, + }, + ]; + const testVehicleMapping = { + Label: 'label', + LoadLimit1Type: 'LoadLimit1Type', + LoadLimit1Value: 'LoadLimit1Value', + }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(0); + }); + + it('should throw an error on a vehicle with a negative load limit', () => { + const vehicles = [ + { + label: 'test vehicle', + LoadLimit1Type: 'weight', + LoadLimit1Value: -10, + }, + ]; + const testVehicleMapping = { + Label: 'label', + LoadLimit1Type: 'LoadLimit1Type', + LoadLimit1Value: 'LoadLimit1Value', + }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(1); + }); + + it('should throw an error on a vehicle with a zero load limit', () => { + const vehicles = [ + { + label: 'test vehicle', + LoadLimit1Type: 'weight', + LoadLimit1Value: 0, + }, + ]; + const testVehicleMapping = { + Label: 'label', + LoadLimit1Type: 'LoadLimit1Type', + LoadLimit1Value: 'LoadLimit1Value', + }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(1); + expect(result[0].errors.length).toBe(1); + }); + + it('should throw no errors on a vehicle with a valid travel mode', () => { + const vehicles = [ + { + label: 'test vehicle', + TravelMode: 'DRIVING', + }, + { + label: 'test vehicle', + TravelMode: 'walking', + }, + ]; + const testVehicleMapping = { + Label: 'label', + TravelMode: 'TravelMode', + }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(2); + expect(result[0].errors.length).toBe(0); + expect(result[1].errors.length).toBe(0); + }); + + it('should throw an error on vehicles with an invalid travel mode', () => { + const vehicles = [ + { + label: 'test vehicle', + TravelMode: 'DRIVING', + }, + { + label: 'test vehicle', + TravelMode: 'invalid', + }, + ]; + const testVehicleMapping = { + Label: 'label', + TravelMode: 'TravelMode', + }; + const result = service.csvToVehicles(vehicles, testVehicleMapping); + expect(result.length).toBe(2); + expect(result[0].errors.length).toBe(0); + expect(result[1].errors.length).toBe(1); + }); + }); }); diff --git a/application/frontend/src/app/core/services/csv.service.ts b/application/frontend/src/app/core/services/csv.service.ts index a1a0daf3..d8454bbb 100644 --- a/application/frontend/src/app/core/services/csv.service.ts +++ b/application/frontend/src/app/core/services/csv.service.ts @@ -27,6 +27,7 @@ import { ILatLng, IShipment, IVehicle, + TravelMode, ValidationErrorResponse, } from '../models'; import { FileService } from './file.service'; @@ -256,12 +257,21 @@ export class CsvService { return errors; } - csvToVehicles(csvVehicles: any[], mapping: { [key: string]: string }): IVehicle[] { + csvToVehicles( + csvVehicles: any[], + mapping: { [key: string]: string } + ): { vehicle: IVehicle; errors: ValidationErrorResponse[] }[] { return csvVehicles.map((vehicle) => { // Conditionally add each field to the vehicle object, converting from csv strings as needed const parsedVehicle = { ...this.mapKeyToModelValue('label', 'Label', vehicle, mapping), - ...this.mapKeyToModelValue('travelMode', 'TravelMode', vehicle, mapping), + ...this.mapKeyToModelValue( + 'travelMode', + 'TravelMode', + vehicle, + mapping, + this.parseTravelMode + ), ...this.mapKeyToModelValue('unloadingPolicy', 'UnloadingPolicy', vehicle, mapping), ...this.mapKeyToModelValue('startWaypoint', 'StartWaypoint', vehicle, mapping), ...this.mapKeyToModelValue('endWaypoint', 'EndWaypoint', vehicle, mapping), @@ -298,10 +308,41 @@ export class CsvService { ...this.mapToLoadLimits(vehicle, mapping), ...this.mapToVehicleTimeWindows(vehicle, mapping), }; - return parsedVehicle; + return { + vehicle: parsedVehicle, + errors: this.validateVehicle(parsedVehicle), + }; }); } + private validateVehicle(vehicle: IVehicle): ValidationErrorResponse[] { + const errors = []; + + const loadLimitsError = Object.keys(vehicle.loadLimits).some((limitKey) => { + const limit = vehicle.loadLimits[limitKey]; + const value = Number.parseFloat(limit.maxLoad as string); + return !Number.isInteger(value) || value < 1; + }); + + if (loadLimitsError) { + errors.push({ + error: true, + message: 'Vehicle contains invalid load limits', + vehicle, + }); + } + + if ('travelMode' in vehicle && !vehicle.travelMode) { + errors.push({ + error: true, + message: 'Vehicle has an invalid travel mode', + vehicle, + }); + } + + return errors; + } + private mapToPickup(shipment: any, mapping: { [key: string]: string }, timeWindow: any): any { const pickup = { ...this.mapKeyToModelValue('arrivalWaypoint', 'PickupArrivalWaypoint', shipment, mapping), @@ -536,6 +577,10 @@ export class CsvService { } } + private parseTravelMode(value: string): number { + return TravelMode[value.toUpperCase()]; + } + // Check map has the provided mapKey and if the model object has a value for the converted key // Optionally run the final value through a conversiion function to transform its type private mapKeyToModelValue(