Files
Cibello-app/packages/inventory-engine/src/forecast.ts
T
2026-08-05 19:21:11 +07:00

53 lines
1.8 KiB
TypeScript

/**
* Pantry Forecast (spec §40): "Ni brukar använda mjölk var sjätte dag."
* Enkel deterministisk konsumtionstakt ur transaktionshistorik.
* Prognoser märks ALLTID som prognoser i UI.
*/
export interface ConsumptionEvent {
/** ISO-datum */
date: string;
quantity: number;
}
export interface ForecastResult {
/** Genomsnittlig förbrukning per dag (i postens enhet). */
dailyRate: number;
/** Dagar tills nuvarande saldo beräknas ta slut. */
daysUntilEmpty: number | null;
/** Föreslaget inköpsdatum (2 dagars marginal). */
suggestedRestockDate: string | null;
confidence: "low" | "medium" | "high";
}
export function forecastDepletion(
currentBalance: number,
consumption: ConsumptionEvent[],
today: Date = new Date(),
): ForecastResult {
if (consumption.length < 2) {
return { dailyRate: 0, daysUntilEmpty: null, suggestedRestockDate: null, confidence: "low" };
}
const sorted = [...consumption].sort((a, b) => a.date.localeCompare(b.date));
const first = Date.parse(sorted[0]!.date);
const last = Date.parse(sorted[sorted.length - 1]!.date);
const spanDays = Math.max(1, (last - first) / 86_400_000);
const total = sorted.reduce((sum, e) => sum + Math.abs(e.quantity), 0);
const dailyRate = total / spanDays;
if (dailyRate <= 0) {
return { dailyRate: 0, daysUntilEmpty: null, suggestedRestockDate: null, confidence: "low" };
}
const daysUntilEmpty = currentBalance / dailyRate;
const restock = new Date(today.getTime() + Math.max(0, daysUntilEmpty - 2) * 86_400_000);
const confidence = consumption.length >= 6 ? "high" : consumption.length >= 4 ? "medium" : "low";
return {
dailyRate: Math.round(dailyRate * 1000) / 1000,
daysUntilEmpty: Math.round(daysUntilEmpty * 10) / 10,
suggestedRestockDate: restock.toISOString().slice(0, 10),
confidence,
};
}