koala73/worldmonitor · error
Every delivery must use ${unit}; mixed units are not convert
Error message
Every delivery must use ${unit}; mixed units are not converted. What it means
Every delivery row must repeat exactly the same unit string chosen for the worksheet (input.unit). The parser deliberately does no unit conversion, so a row carrying a different unit would silently corrupt totals; instead it throws with the expected unit interpolated into the message.
Solutions
- Normalize every delivery row's unit to exactly the top-level input.unit string before parsing.
- Convert quantities to one canonical unit yourself and set each row.unit to the worksheet unit.
- Trim/case-fold unit strings at the form layer so all rows share one canonical spelling.
- In the import path, detect mixed units early and ask the user which unit to keep.
Example fix
// before
calculateOperationalBalance({ ...input, unit: 'units', deliveries: [{ date: '2026-09-13', quantity: 40, unit: 'pcs', costUsd: null }] });
// after
const deliveries = rows.map(r => ({ ...r, unit: 'units' }));
calculateOperationalBalance({ ...input, unit: 'units', deliveries }); Defensive patterns
Strategy: validation
Validate before calling
if (input.deliveries.some(d => d.unit !== input.unit)) {
throw new Error(`All delivery rows must use unit "${input.unit}".`);
} Type guard
const allRowsUseUnit = (rows: { unit: string }[], unit: string): boolean =>
rows.every(r => r.unit === unit); Try / catch
try {
const snapshot = calculateOperationalBalance(input);
} catch (err) {
if (/^Every delivery must use .+; mixed units are not converted\.$/.test(err.message)) {
input.deliveries = input.deliveries.map(d => ({ ...d, unit: input.unit }));
} else throw err;
} Prevention
- Render delivery rows with a read-only unit field mirroring the worksheet unit.
- Normalize unit strings (trim + lowercase-compare, keep one canonical spelling) on entry.
- When importing CSV with a unit column, convert quantities to the worksheet unit first.
When it happens
Trigger: Calling parseOperationalInput/calculateOperationalBalance where any delivery in deliveries or alternativeDeliveries has row.unit !== input.unit — e.g. worksheet unit 'liters' but a delivery row with unit 'L', 'gallons', or a stale value from an earlier edit.
Common situations: Mixing metric and imperial rows in an imported schedule; the top-level unit was renamed but old delivery rows kept the previous string; whitespace/case differences like 'Units' vs 'units'; CSV imports with a per-row unit column.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Invalid scorecard bloc selection.
- countryCode must be a 2-letter ISO 3166-1 alpha-2 code
- chokepointId must be a canonical chokepoint id
- iso2 must be a 2-letter uppercase ISO country code
- INVALID_NAME
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/6f16b0bc14299346.
Report an issue: GitHub.
Appendix: source
Thrown at src/utils/operational-balance.ts:43
return value;
}
export function parseOperationalInput(value: unknown): OperationalInput {
const input = record(value);
if (typeof input.operation !== 'string' || !input.operation.trim() || input.operation.length > 100) throw new Error('Enter an operation name of 1-100 characters.');
if (typeof input.unit !== 'string' || !/^[\p{L}\p{N} %./-]{1,24}$/u.test(input.unit) || !input.unit.trim()) throw new Error('Enter one quantity unit of 1-24 characters.');
const unit = input.unit.trim();
const startDate = date(input.startDate);
if (!Number.isInteger(input.horizonDays) || (input.horizonDays as number) < 1 || (input.horizonDays as number) > MAX_OPERATIONAL_DAYS) throw new Error('Horizon must be 1-90 whole days.');
const horizonDays = input.horizonDays as number;
if (input.basis !== 'example' && input.basis !== 'user') throw new Error('Input basis must be example or user.');
const deliveries = (value: unknown): OperationalDelivery[] => {
if (!Array.isArray(value) || value.length > MAX_OPERATIONAL_DELIVERIES) throw new Error('Use at most 30 deliveries per list.');
return value.map(value => {
const row = record(value);
const deliveryDate = date(row.date);
if (deliveryDate < startDate || deliveryDate > dateAt(startDate, horizonDays - 1)) throw new Error('Delivery dates must be inside the selected horizon.');
if (row.unit !== unit) throw new Error(`Every delivery must use ${unit}; mixed units are not converted.`);
return { date: deliveryDate, unit, quantity: quantity(row.quantity, 'Delivery quantity'), costUsd: row.costUsd === null || row.costUsd === undefined ? null : quantity(row.costUsd, 'Delivery cost') };
});
};
return { operation: input.operation.trim(), basis: input.basis, unit, startDate, horizonDays,
startingStock: quantity(input.startingStock, 'Starting stock'), dailyDemand: quantity(input.dailyDemand, 'Daily demand'),
deliveries: deliveries(input.deliveries), alternativeDeliveries: deliveries(input.alternativeDeliveries),
alternativeDailyDemand: input.alternativeDailyDemand === null ? null : quantity(input.alternativeDailyDemand, 'Alternative daily demand') };
}
export function calculateOperationalBalance(value: unknown): OperationalSnapshot {
const input = parseOperationalInput(value);
const balance = (deliveries: OperationalDelivery[], demand: number): OperationalBalance => {
let stock = input.startingStock;
const days = Array.from({ length: input.horizonDays }, (_, index) => {
const date = dateAt(input.startDate, index);
const arrivals = round(deliveries.filter(row => row.date === date).reduce((sum, row) => sum + row.quantity, 0));
const available = round(stock + arrivals);
const unmetDemand = round(Math.max(0, demand - available));View on GitHub (pinned to 7d06c8633d)