src/orders/ingestion/validators/order.validator.ts
Methods |
| validate | ||||||
validate(orders: ExtractedOrder[])
|
||||||
|
Parameters :
Returns :
ValidationResult
|
import { ExtractedOrder } from '../ai-extractor/claude.extractor';
export interface ValidationError {
orderIndex: number;
field: string;
message: string;
severity: 'error' | 'warning';
}
export interface ValidationResult {
valid: boolean;
errors: ValidationError[];
warnings: ValidationError[];
validOrders: ExtractedOrder[];
rejectedCount: number;
}
const VALID_VEHICLE_TYPES = [
'DRY_VAN',
'REFRIGERATED',
'FLATBED',
'TANKER',
'CURTAIN_SIDE',
'BOX_TRUCK',
'SPRINTER',
'HAZMAT',
];
const VALID_PRIORITIES = ['LOW', 'NORMAL', 'HIGH', 'CRITICAL'];
const MAX_WEIGHT_KG = 44000;
export class OrderValidator {
validate(orders: ExtractedOrder[]): ValidationResult {
const errors: ValidationError[] = [];
const warnings: ValidationError[] = [];
const validOrders: ExtractedOrder[] = [];
let rejectedCount = 0;
for (let i = 0; i < orders.length; i++) {
const order = orders[i];
const orderErrors: ValidationError[] = [];
// Origin is optional — many beer distribution files only have PLANT column
// which may not always map. Destination is what truly matters.
if (!order.origin_address) {
warnings.push({
orderIndex: i,
field: 'origin_address',
message: 'Origin address is missing',
severity: 'warning',
});
}
if (!order.destination_address) {
orderErrors.push({
orderIndex: i,
field: 'destination_address',
message: 'Destination address is required',
severity: 'error',
});
}
// Weight validation
if (order.weight_kg !== null && order.weight_kg !== undefined) {
if (order.weight_kg <= 0) {
warnings.push({
orderIndex: i,
field: 'weight_kg',
message: `Weight is non-positive (${order.weight_kg} kg)`,
severity: 'warning',
});
}
if (order.weight_kg > MAX_WEIGHT_KG) {
orderErrors.push({
orderIndex: i,
field: 'weight_kg',
message: `Weight exceeds max of ${MAX_WEIGHT_KG} kg (got ${order.weight_kg} kg)`,
severity: 'error',
});
}
}
// Date validation
if (order.pickup_date && order.delivery_date) {
const pickup = new Date(order.pickup_date);
const delivery = new Date(order.delivery_date);
if (!isNaN(pickup.getTime()) && !isNaN(delivery.getTime())) {
if (delivery < pickup) {
warnings.push({
orderIndex: i,
field: 'delivery_date',
message: 'Delivery date is before pickup date',
severity: 'warning',
});
}
}
}
// Vehicle type validation
if (
order.vehicle_type &&
!VALID_VEHICLE_TYPES.includes(order.vehicle_type.toUpperCase())
) {
warnings.push({
orderIndex: i,
field: 'vehicle_type',
message: `Unknown vehicle type: "${order.vehicle_type}"`,
severity: 'warning',
});
// Normalize to null if invalid
order.vehicle_type = null;
} else if (order.vehicle_type) {
order.vehicle_type = order.vehicle_type.toUpperCase();
}
// Priority validation
if (order.priority) {
order.priority = order.priority.toUpperCase();
if (!VALID_PRIORITIES.includes(order.priority)) {
warnings.push({
orderIndex: i,
field: 'priority',
message: `Unknown priority: "${order.priority}", defaulting to NORMAL`,
severity: 'warning',
});
order.priority = 'NORMAL';
}
} else {
order.priority = 'NORMAL';
}
// If there are hard errors, reject this order
if (orderErrors.length > 0) {
errors.push(...orderErrors);
rejectedCount++;
} else {
validOrders.push(order);
}
}
// Duplicate detection: same origin + dest + weight + pickup date
const seen = new Map<string, number>();
for (let i = 0; i < validOrders.length; i++) {
const o = validOrders[i];
const key = [
o.origin_address?.toLowerCase(),
o.destination_address?.toLowerCase(),
o.weight_kg,
o.pickup_date,
].join('|');
if (seen.has(key)) {
warnings.push({
orderIndex: i,
field: '_duplicate',
message: `Possible duplicate of order ${seen.get(key)! + 1} (same origin, destination, weight, pickup date)`,
severity: 'warning',
});
} else {
seen.set(key, i);
}
}
return {
valid: errors.length === 0,
errors,
warnings,
validOrders,
rejectedCount,
};
}
}