{"record":{"id":"4147d452b3ff8fec","repo":"koala73/worldmonitor","slug":"delivery-dates-must-be-inside-the-selected-horizon","errorCode":null,"errorMessage":"Delivery dates must be inside the selected horizon.","messagePattern":"Delivery dates must be inside the selected horizon\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/utils/operational-balance.ts","lineNumber":42,"sourceCode":"  }\n  return value;\n}\n\nexport function parseOperationalInput(value: unknown): OperationalInput {\n  const input = record(value);\n  if (typeof input.operation !== 'string' || !input.operation.trim() || input.operation.length > 100) throw new Error('Enter an operation name of 1-100 characters.');\n  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.');\n  const unit = input.unit.trim();\n  const startDate = date(input.startDate);\n  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.');\n  const horizonDays = input.horizonDays as number;\n  if (input.basis !== 'example' && input.basis !== 'user') throw new Error('Input basis must be example or user.');\n  const deliveries = (value: unknown): OperationalDelivery[] => {\n    if (!Array.isArray(value) || value.length > MAX_OPERATIONAL_DELIVERIES) throw new Error('Use at most 30 deliveries per list.');\n    return value.map(value => {\n      const row = record(value);\n      const deliveryDate = date(row.date);\n      if (deliveryDate < startDate || deliveryDate > dateAt(startDate, horizonDays - 1)) throw new Error('Delivery dates must be inside the selected horizon.');\n      if (row.unit !== unit) throw new Error(`Every delivery must use ${unit}; mixed units are not converted.`);\n      return { date: deliveryDate, unit, quantity: quantity(row.quantity, 'Delivery quantity'), costUsd: row.costUsd === null || row.costUsd === undefined ? null : quantity(row.costUsd, 'Delivery cost') };\n    });\n  };\n  return { operation: input.operation.trim(), basis: input.basis, unit, startDate, horizonDays,\n    startingStock: quantity(input.startingStock, 'Starting stock'), dailyDemand: quantity(input.dailyDemand, 'Daily demand'),\n    deliveries: deliveries(input.deliveries), alternativeDeliveries: deliveries(input.alternativeDeliveries),\n    alternativeDailyDemand: input.alternativeDailyDemand === null ? null : quantity(input.alternativeDailyDemand, 'Alternative daily demand') };\n}\n\nexport function calculateOperationalBalance(value: unknown): OperationalSnapshot {\n  const input = parseOperationalInput(value);\n  const balance = (deliveries: OperationalDelivery[], demand: number): OperationalBalance => {\n    let stock = input.startingStock;\n    const days = Array.from({ length: input.horizonDays }, (_, index) => {\n      const date = dateAt(input.startDate, index);\n      const arrivals = round(deliveries.filter(row => row.date === date).reduce((sum, row) => sum + row.quantity, 0));\n      const available = round(stock + arrivals);","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/src/utils/operational-balance.ts#L24-L60","documentation":"Each delivery row's date, parsed via date(), must fall within the planning window: on or after startDate and no later than dateAt(startDate, horizonDays - 1). Deliveries outside the horizon cannot be attributed to any day in the computed balance, so the parser rejects them explicitly instead of dropping them silently.","triggerScenarios":"Calling parseOperationalInput/calculateOperationalBalance where any entry of deliveries or alternativeDeliveries has a date before the input's startDate, after startDate + horizonDays - 1 days, an invalid date string that parses to an out-of-range value, or a timezone-shifted ISO date that rolls to the next/previous day.","commonSituations":"The user changed the horizon after entering deliveries; a copy-pasted date one year off; UTC vs local timezone shifting '2026-09-10T00:00Z' past a boundary; month/day swapped in a hand-edited JSON export.","solutions":["Clamp or filter delivery dates to [startDate, dateAt(startDate, horizonDays - 1)] before calling the parser.","Recompute the horizon to cover all existing deliveries instead of shrinking it after data entry.","Use plain YYYY-MM-DD date strings consistently to avoid timezone drift at day boundaries.","In the import path, validate each row.date against the horizon and report the offending index to the user."],"exampleFix":"// before\ncalculateOperationalBalance({ ...input, horizonDays: 10, deliveries: [{ date: '2026-10-01', quantity: 5, unit: 'units', costUsd: null }] });\n// after\nconst inHorizon = input.deliveries.filter(d => d.date >= input.startDate && d.date <= '2026-09-19');\ncalculateOperationalBalance({ ...input, horizonDays: 10, deliveries: inHorizon });","handlingStrategy":"validation","validationCode":"const start = input.startDate; // 'YYYY-MM-DD'\nconst end = new Date(start); end.setUTCDate(end.getUTCDate() + input.horizonDays - 1);\nconst endStr = end.toISOString().slice(0, 10);\nconst bad = input.deliveries.filter(d => d.date < start || d.date > endStr);\nif (bad.length) throw new Error(`Delivery dates outside ${start}..${endStr}: ${bad.map(d => d.date).join(', ')}`);","typeGuard":null,"tryCatchPattern":"try {\n  const snapshot = calculateOperationalBalance(input);\n} catch (err) {\n  if (err instanceof Error && err.message === 'Delivery dates must be inside the selected horizon.') {\n    highlightOffendingDatesInUi(input);\n  } else throw err;\n}","preventionTips":["Constrain the delivery date picker's min/max attributes to the horizon whenever the horizon changes.","Use plain YYYY-MM-DD strings everywhere to avoid timezone day-shifts.","Re-sort and re-validate all delivery rows after any change to startDate or horizonDays."],"tags":["validation","date-range","input-validation"],"backgroundTag":"value-out-of-range","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}