Functional Weave
Code in Rust

inventory.valuation-fifo@1.0.0

impl/typescript.ts

3,072 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 { epochDayFromIso } from "./dates_add_days.ts";  ← from dates.add-days ^1.0.0 · built alongside by fune
import { assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type FifoValuation, type StockLayer, type StockMovement } from "./inventory_valuation_fifo_types.ts";

/**
 * Value stock first in, first out, and cost each issue from the oldest layers.
 *
 * Every cost is a whole quantity times a unit cost, so nothing is rounded and
 * closing value plus cost of sales always equals what was received.
 */
export function fifoValuation(movements: readonly StockMovement[], currency: string): FifoValuation {
  const zero = money(0, currency);
  const layers: { date: string; quantity: number; unitCost: StockLayer["unitCost"] }[] = [];
  const issueCosts = [];
  let costOfSales = 0;
  let previousDay: number | null = null;
  let previousDate = "";
  for (const line of movements) {
    const day = epochDayFromIso(line.date);
    if (previousDay !== null && day < previousDay) {
      throw new RangeError(`movements must be in date order: ${line.date} comes after ${previousDate}`);
    }
    previousDay = day;
    previousDate = line.date;
    if (!Number.isInteger(line.quantity) || line.quantity === 0) {
      throw new RangeError(`quantity must be a non-zero whole number, received ${line.quantity} on ${line.date}`);
    }
    if (line.quantity > 0) {
      if (line.unitCost === null || line.unitCost === undefined) {
        throw new RangeError(`a receipt needs a unitCost: ${line.quantity} on ${line.date}`);
      }
      assertSameCurrency(zero, line.unitCost);
      if (line.unitCost.minor < 0) {
        throw new RangeError(`unitCost must not be negative, received ${line.unitCost.minor} on ${line.date}`);
      }
      layers.push({ date: line.date, quantity: line.quantity, unitCost: line.unitCost });
      continue;
    }
    if (line.unitCost !== null && line.unitCost !== undefined) {
      throw new RangeError(`an issue takes its cost from stock, so its unitCost must be null: ${line.quantity} on ${line.date}`);
    }
    let wanted = -line.quantity;
    const onHand = layers.reduce((sum, layer) => sum + layer.quantity, 0);
    if (wanted > onHand) {
      throw new RangeError(`insufficient stock: an issue of ${wanted} on ${line.date} exceeds the ${onHand} on hand`);
    }
    let cost = 0;
    while (wanted > 0) {
      const oldest = layers[0];
      const used = Math.min(wanted, oldest.quantity);
      cost += used * oldest.unitCost.minor;
      oldest.quantity -= used;
      wanted -= used;
      if (oldest.quantity === 0) layers.shift();
    }
    issueCosts.push(money(cost, currency));
    costOfSales += cost;
  }
  let closingQuantity = 0;
  let closingValue = 0;
  for (const layer of layers) {
    closingQuantity += layer.quantity;
    closingValue += layer.quantity * layer.unitCost.minor;
  }
  return {
    closingQuantity,
    closingValue: money(closingValue, currency),
    costOfSales: money(costOfSales, currency),
    issueCosts,
    layers: layers.map((layer) => ({ date: layer.date, quantity: layer.quantity, unitCost: layer.unitCost })),
  };
}