Functional Weave
Code in Python

subscriptions.invoice@1.0.0

impl/rust.rs

6,163 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::finance_invoice_calculate::{calculate_invoice, invoice_line_from_value, invoice_to_value, Invoice, InvoiceLine};  ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
use super::finance_invoice_line_total::line_total;  ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
use super::money_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate;  ← from money.apply-rate ^1.0.0 · built alongside by fune

fn check_discount(d: &Discount, currency: &str) {
    if d.basis_points.is_none() == d.amount_off.is_none() {
        panic!(
            "a discount needs exactly one of basisPoints and amountOff: \"{}\"",
            d.description
        );
    }
    if let Some(bp) = d.basis_points {
        if !(0..=10000).contains(&bp) {
            panic!("discount basis points must be between 0 and 10000, received {}", bp);
        }
    }
    if let Some(off) = &d.amount_off {
        assert_same_currency(&money(0, currency), off);
        if off.minor < 0 {
            panic!("discount amount must not be negative, received {}", off.minor);
        }
    }
    if d.start_period < 1 {
        panic!("discount start period must be 1 or more, received {}", d.start_period);
    }
    match d.duration.as_str() {
        "repeating" => {
            if d.duration_periods.map_or(true, |n| n < 1) {
                panic!(
                    "a repeating discount needs durationPeriods of 1 or more, received {:?}",
                    d.duration_periods
                );
            }
        }
        "once" | "forever" => {
            if d.duration_periods.is_some() {
                panic!(
                    "durationPeriods applies only to a repeating discount: \"{}\"",
                    d.description
                );
            }
        }
        other => panic!(
            "unknown discount duration \"{}\": expected once, repeating or forever",
            other
        ),
    }
}

fn applies(d: &Discount, period: i64) -> bool {
    match d.duration.as_str() {
        "once" => period == d.start_period,
        "repeating" => period >= d.start_period && period < d.start_period + d.duration_periods.unwrap_or(0),
        _ => period >= d.start_period,
    }
}

/// The renewal invoice for one billing period: plan, add-ons and usage, less
/// the coupons in force in that period, taxed per line by
/// finance::invoice::calculate. Each coupon becomes negative lines, one per
/// VAT category, so the reduction is taxed at the rate of what it reduces.
///
/// # Panics
/// Panics on a period below 1, a malformed coupon, or anything
/// `calculate_invoice` refuses.
pub fn renewal_invoice(
    plan: &InvoiceLine,
    add_ons: &[InvoiceLine],
    usage: &[InvoiceLine],
    discounts: &[Discount],
    period_number: i64,
    jurisdiction: &str,
    invoice_date: &str,
) -> Invoice {
    if period_number < 1 {
        panic!("period number must be 1 or more, received {}", period_number);
    }
    let currency = plan.unit_price.currency.clone();
    let mut items: Vec<InvoiceLine> = vec![plan.clone()];
    items.extend(add_ons.iter().cloned());
    items.extend(usage.iter().cloned());

    let mut categories: Vec<String> = Vec::new();
    let mut category_net: Vec<i64> = Vec::new();
    let mut running: i64 = 0;
    for item in &items {
        let net = line_total(&item.unit_price, item.quantity, item.discount_basis_points);
        assert_same_currency(&money(0, &currency), &net);
        let k = match categories.iter().position(|c| *c == item.tax_category) {
            Some(k) => k,
            None => {
                categories.push(item.tax_category.clone());
                category_net.push(0);
                categories.len() - 1
            }
        };
        category_net[k] += net.minor;
        running += net.minor;
    }
    // Credit-only categories take no share of a discount.
    let weights: Vec<i64> = category_net.iter().map(|&n| n.max(0)).collect();

    let mut discount_lines: Vec<InvoiceLine> = Vec::new();
    for d in discounts {
        check_discount(d, &currency);
        if !applies(d, period_number) || running <= 0 {
            continue;
        }
        let amount = match (d.basis_points, &d.amount_off) {
            (Some(bp), _) => apply_rate(&money(running, &currency), bp, "half-up").minor,
            (None, Some(off)) => off.minor.min(running),
            (None, None) => 0,
        };
        if amount == 0 {
            continue;
        }
        running -= amount;
        for (k, share) in allocate(&money(amount, &currency), &weights).iter().enumerate() {
            if share.minor != 0 {
                discount_lines.push(InvoiceLine {
                    description: d.description.clone(),
                    unit_price: money(-share.minor, &currency),
                    quantity: 1,
                    discount_basis_points: 0,
                    tax_category: categories[k].clone(),
                });
            }
        }
    }
    items.extend(discount_lines);
    calculate_invoice(&items, jurisdiction, invoice_date)
}

pub fn discount_from_value(v: &Value) -> Discount {
    Discount {
        description: v.get("description").as_str().to_string(),
        basis_points: if v.get("basisPoints").is_null() { None } else { Some(v.get("basisPoints").as_i64()) },
        amount_off: if v.get("amountOff").is_null() { None } else { Some(money_from_value(v.get("amountOff"))) },
        duration: v.get("duration").as_str().to_string(),
        duration_periods: if v.get("durationPeriods").is_null() { None } else { Some(v.get("durationPeriods").as_i64()) },
        start_period: v.get("startPeriod").as_i64(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let add_ons: Vec<InvoiceLine> = args[1].as_arr().iter().map(invoice_line_from_value).collect();
    let usage: Vec<InvoiceLine> = args[2].as_arr().iter().map(invoice_line_from_value).collect();
    let discounts: Vec<Discount> = args[3].as_arr().iter().map(discount_from_value).collect();
    invoice_to_value(&renewal_invoice(
        &invoice_line_from_value(&args[0]),
        &add_ons,
        &usage,
        &discounts,
        args[4].as_i64(),
        args[5].as_str(),
        args[6].as_str(),
    ))
}