From a408d45a0bcd9906dc2b1592f0dc179909cb3344 Mon Sep 17 00:00:00 2001 From: Jason McCollum <83362099+jasonmccollumwoolpert@users.noreply.github.com> Date: Wed, 7 Dec 2022 10:45:53 -0800 Subject: [PATCH] Validate CSV shipment uploads --- .../csv-upload-dialog.component.ts | 25 ++++++++++++++-- .../frontend/src/app/core/models/csv.ts | 7 +++-- .../src/app/core/services/csv.service.ts | 30 +++++++++++++++++-- 3 files changed, 56 insertions(+), 6 deletions(-) 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 255ec0f8..71798774 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 @@ -1307,7 +1307,23 @@ export class CsvUploadDialogComponent implements OnDestroy, OnInit { .pipe( mergeMap((res: any[]) => { if (this.shipmentFile) { - shipments = this.service.csvToShipments(res[0].data, this.mappingFormShipments.value); + const shipmentsResults = this.service.csvToShipments( + res[0].data, + this.mappingFormShipments.value + ); + if (shipmentsResults.some((result) => result.errors.length)) { + shipmentsResults.forEach((result, index) => { + this.validationErrors.push( + ...result.errors.map( + (error) => + `Shipment ${result.shipment.label || ''} at index ${index}: ${error.message}` + ) + ); + }); + throw Error('Shipment validation error'); + } + + shipments = shipmentsResults.map((result) => result.shipment); } if (this.vehicleFile) { vehicles = this.service.csvToVehicles(res[1].data, this.mappingFormVehicles.value); @@ -1353,7 +1369,12 @@ export class CsvUploadDialogComponent implements OnDestroy, OnInit { (err) => { this.isValidatingWithApi = false; this.errorValidating = true; - this.validationErrors.push(err.error?.error?.message || err.message); + + // Throw default error if none have been provided + if (!this.validationErrors) { + this.validationErrors.push(err.error?.error?.message || err.message); + } + this.changeRef.detectChanges(); } ); diff --git a/application/frontend/src/app/core/models/csv.ts b/application/frontend/src/app/core/models/csv.ts index de3c6ec1..a4beeca5 100644 --- a/application/frontend/src/app/core/models/csv.ts +++ b/application/frontend/src/app/core/models/csv.ts @@ -135,8 +135,7 @@ export const CSV_DATA_LABELS_ABBREVIATED = { timeToNextStop: 'Time to next stop', }; -export interface GeocodeErrorResponse { - location: string; +export interface ValidationErrorResponse { error: true; message?: string; source?: any; @@ -146,6 +145,10 @@ export interface GeocodeErrorResponse { index?: number; } +export interface GeocodeErrorResponse extends ValidationErrorResponse { + location: string; +} + export const EXPERIMENTAL_API_FIELDS_VEHICLES = [ 'requiredOperatorType1', 'requiredOperatorType2', diff --git a/application/frontend/src/app/core/services/csv.service.ts b/application/frontend/src/app/core/services/csv.service.ts index 1430a7c8..870bee72 100644 --- a/application/frontend/src/app/core/services/csv.service.ts +++ b/application/frontend/src/app/core/services/csv.service.ts @@ -21,6 +21,7 @@ import { IShipment, IVehicle, IVehicleOperator, + ValidationErrorResponse, } from '../models'; import { FileService } from './file.service'; import { GeocodingService } from './geocoding.service'; @@ -211,7 +212,10 @@ export class CsvService { }); } - csvToShipments(csvShipments: any[], mapping: { [key: string]: string }): IShipment[] { + csvToShipments( + csvShipments: any[], + mapping: { [key: string]: string } + ): { shipment: IShipment; errors: ValidationErrorResponse[] }[] { return csvShipments.map((shipment) => { const timeWindows = this.mapToShipmentTimeWindows(shipment, mapping); const parsedShipment = { @@ -228,8 +232,30 @@ export class CsvService { this.commaSeparatedStringToIntArray ), }; - return parsedShipment; + + return { + shipment: parsedShipment, + errors: this.validateShipment(parsedShipment), + }; + }); + } + + private validateShipment(shipment: IShipment): ValidationErrorResponse[] { + const errors = []; + const loadDemandsError = Object.keys(shipment.loadDemands).some((demandKey) => { + const demand = shipment.loadDemands[demandKey]; + return !Number.isInteger(Number.parseFloat(demand.amount as string)); }); + + if (loadDemandsError) { + errors.push({ + error: true, + message: 'Shipment contains invalid load demands', + shipment, + }); + } + + return errors; } csvToVehicles(csvVehicles: any[], mapping: { [key: string]: string }): IVehicle[] {