Skip to content

Commit

Permalink
fix: CSV upload parsing and validation (#183)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmccollum-woolpert authored Sep 17, 2024
1 parent 513a57c commit fb99a7f
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
153 changes: 153 additions & 0 deletions application/frontend/src/app/core/services/csv.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
51 changes: 48 additions & 3 deletions application/frontend/src/app/core/services/csv.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ILatLng,
IShipment,
IVehicle,
TravelMode,
ValidationErrorResponse,
} from '../models';
import { FileService } from './file.service';
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit fb99a7f

Please sign in to comment.