Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type MilkContract, type MilkPrice, type MilkSupply } from "./agri_milk_price_types.ts";
const MAX_PRICE = 1000000;
const MAX_LITRES = 1000000000;
function whole(value: number, name: string, min: number, max: number): number {
if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
throw new RangeError(`${name} must be a whole number from ${min} to ${max}, received ${value}`);
}
return value;
}
/**
* Milk price for one period under a buyer's contract.
*
* Prices are thousandths of a penny (or cent) per litre, because contracts
* quote to 0.01 or 0.001 ppl. Constituent adjustments are pro rata per 0.1
* percentage point and are each rounded to a thousandth, half away from
* zero; the payment is litres x price, rounded once to the penny.
*/
export function milkPrice(contract: MilkContract, supply: MilkSupply): MilkPrice {
money(0, contract.currency);
const base = whole(contract.basePrice, "basePrice", 0, MAX_PRICE);
const stdFat = whole(contract.standardButterfatBasisPoints, "standardButterfatBasisPoints", 0, 10000);
const fatValue = whole(contract.butterfatValue, "butterfatValue", -MAX_PRICE, MAX_PRICE);
const stdProtein = whole(contract.standardProteinBasisPoints, "standardProteinBasisPoints", 0, 10000);
const proteinValue = whole(contract.proteinValue, "proteinValue", -MAX_PRICE, MAX_PRICE);
const litres = whole(supply.litres, "litres", 0, MAX_LITRES);
const fat = whole(supply.butterfatBasisPoints, "butterfatBasisPoints", 0, 10000);
const protein = whole(supply.proteinBasisPoints, "proteinBasisPoints", 0, 10000);
const month = whole(supply.month, "month", 1, 12);
// 0.1 percentage point is 10 basis points.
const fatAdjustment = roundDiv((fat - stdFat) * fatValue, 10, "half-up");
const proteinAdjustment = roundDiv((protein - stdProtein) * proteinValue, 10, "half-up");
let volumeBonus = 0;
let previous = -1;
contract.volumeBonuses.forEach((tier, i) => {
const minimum = whole(tier.minimumLitres, `volumeBonuses[${i}].minimumLitres`, 0, MAX_LITRES);
const bonus = whole(tier.bonus, `volumeBonuses[${i}].bonus`, -MAX_PRICE, MAX_PRICE);
if (minimum <= previous) throw new RangeError("volumeBonuses must be in strictly ascending order of minimumLitres");
previous = minimum;
if (litres >= minimum) volumeBonus = bonus;
});
let seasonal = 0;
if (contract.seasonality.length === 12) {
contract.seasonality.forEach((v, i) => whole(v, `seasonality[${i}]`, -MAX_PRICE, MAX_PRICE));
seasonal = contract.seasonality[month - 1];
} else if (contract.seasonality.length !== 0) {
throw new RangeError(`seasonality must have 12 monthly values or none, received ${contract.seasonality.length}`);
}
const price = base + fatAdjustment + proteinAdjustment + volumeBonus + seasonal;
if (price < 0) throw new RangeError(`price per litre must not be negative, received ${price}`);
if (price > MAX_PRICE) throw new RangeError(`price per litre must be at most ${MAX_PRICE}, received ${price}`);
return {
basePrice: base,
butterfatAdjustment: fatAdjustment,
proteinAdjustment,
volumeBonus,
seasonalAdjustment: seasonal,
pricePerLitre: price,
total: money(roundDiv(price * litres, 1000, "half-up"), contract.currency),
};
}