impl/typescript.ts
1,597 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { type FefoPick, type PickLine, type StockBatch } from "./inventory_expiry_fefo_types.ts";
function wholeUnits(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${name} must be a whole number, not negative, received ${value}`);
}
}
/**
* Pick `quantity` units earliest expiry first, from batches with at least
* `minShelfLifeDays` of life left on `asAt`. Short stock is a shortfall, not
* an error.
*/
export function fefoPick(batches: readonly StockBatch[], quantity: number, asAt: string, minShelfLifeDays: number): FefoPick {
wholeUnits("quantity", quantity);
wholeUnits("minShelfLifeDays", minShelfLifeDays);
const dated = batches.map((batch, index) => {
wholeUnits(`quantity of batch "${batch.batch}"`, batch.quantity);
return { batch, index, life: daysBetween(asAt, batch.expiry) };
});
// Array.prototype.sort is stable, so equal expiry dates keep their order.
dated.sort((a, b) => a.life - b.life);
const lines: PickLine[] = [];
const skipped: string[] = [];
let remaining = quantity;
for (const { batch, life } of dated) {
if (batch.quantity === 0) continue;
if (life < minShelfLifeDays) {
skipped.push(batch.batch);
continue;
}
if (remaining === 0) continue;
const take = Math.min(remaining, batch.quantity);
lines.push({ batch: batch.batch, expiry: batch.expiry, quantity: take });
remaining -= take;
}
return { lines, picked: quantity - remaining, shortfall: remaining, skipped };
}