Functional Weave
Code in TypeScript

inventory.expiry-fefo@1.0.0

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 };
}