Functional Weave
Code in TypeScript

inventory.valuation-fifo@1.0.0

impl/rust.rs

5,090 bytes · the Rust 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::epoch_day_from_iso;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// 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.
///
/// # Panics
/// Panics on a ledger out of date order, a zero quantity, a receipt without a
/// unit cost or with a negative one, an issue with a unit cost, a cost in
/// another currency, or an issue larger than the stock on hand.
pub fn fifo_valuation(movements: &[StockMovement], currency: &str) -> FifoValuation {
    let zero = money(0, currency);
    let mut layers: Vec<StockLayer> = Vec::new();
    let mut issue_costs = Vec::new();
    let mut cost_of_sales: i64 = 0;
    let mut previous: Option<(i64, &str)> = None;
    for line in movements {
        let day = epoch_day_from_iso(&line.date);
        if let Some((previous_day, previous_date)) = previous {
            if day < previous_day {
                panic!("movements must be in date order: {} comes after {}", line.date, previous_date);
            }
        }
        previous = Some((day, &line.date));
        if line.quantity == 0 {
            panic!("quantity must be a non-zero whole number, received 0 on {}", line.date);
        }
        if line.quantity > 0 {
            let unit_cost = match &line.unit_cost {
                Some(c) => c,
                None => panic!("a receipt needs a unitCost: {} on {}", line.quantity, line.date),
            };
            assert_same_currency(&zero, unit_cost);
            if unit_cost.minor < 0 {
                panic!("unitCost must not be negative, received {} on {}", unit_cost.minor, line.date);
            }
            layers.push(StockLayer {
                date: line.date.clone(),
                quantity: line.quantity,
                unit_cost: unit_cost.clone(),
            });
            continue;
        }
        if line.unit_cost.is_some() {
            panic!(
                "an issue takes its cost from stock, so its unitCost must be null: {} on {}",
                line.quantity, line.date
            );
        }
        let mut wanted = -line.quantity;
        let on_hand: i64 = layers.iter().map(|l| l.quantity).sum();
        if wanted > on_hand {
            panic!(
                "insufficient stock: an issue of {} on {} exceeds the {} on hand",
                wanted, line.date, on_hand
            );
        }
        let mut cost: i64 = 0;
        while wanted > 0 {
            let oldest = &mut layers[0];
            let used = wanted.min(oldest.quantity);
            cost += used * oldest.unit_cost.minor;
            oldest.quantity -= used;
            wanted -= used;
            if oldest.quantity == 0 {
                layers.remove(0);
            }
        }
        issue_costs.push(money(cost, currency));
        cost_of_sales += cost;
    }
    let closing_quantity: i64 = layers.iter().map(|l| l.quantity).sum();
    let closing_value: i64 = layers.iter().map(|l| l.quantity * l.unit_cost.minor).sum();
    FifoValuation {
        closing_quantity,
        closing_value: money(closing_value, currency),
        cost_of_sales: money(cost_of_sales, currency),
        issue_costs,
        layers,
    }
}

pub fn stock_movement_from_value(v: &Value) -> StockMovement {
    let cost = v.get("unitCost");
    StockMovement {
        date: v.get("date").as_str().to_string(),
        quantity: v.get("quantity").as_i64(),
        unit_cost: if cost.is_null() { None } else { Some(money_from_value(cost)) },
    }
}

pub fn fifo_valuation_to_value(result: &FifoValuation) -> Value {
    Value::obj(vec![
        ("closingQuantity", Value::Int(result.closing_quantity)),
        ("closingValue", money_to_value(&result.closing_value)),
        ("costOfSales", money_to_value(&result.cost_of_sales)),
        ("issueCosts", Value::Arr(result.issue_costs.iter().map(money_to_value).collect())),
        (
            "layers",
            Value::Arr(
                result
                    .layers
                    .iter()
                    .map(|l| {
                        Value::obj(vec![
                            ("date", Value::str(&l.date)),
                            ("quantity", Value::Int(l.quantity)),
                            ("unitCost", money_to_value(&l.unit_cost)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let movements: Vec<StockMovement> = args[0]
        .as_arr()
        .iter()
        .map(|v| {
            if let Value::Float(f) = v.get("quantity") {
                panic!("quantity must be a non-zero whole number, received {} on {}", f, v.get("date").as_str());
            }
            stock_movement_from_value(v)
        })
        .collect();
    fifo_valuation_to_value(&fifo_valuation(&movements, args[1].as_str()))
}