Functional Weave
Code in Rust

finance.tax.vat-return@1.0.1

impl/rust.rs

5,679 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_days_between::days_between;  ← from dates.days-between ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const ZERO_VAT_SALES: [&str; 5] = ["exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "ni-eu-goods"];
const ZERO_VAT_PURCHASES: [&str; 2] = ["exempt", "outside-scope"];
const TREATMENTS: [&str; 7] = [
    "taxable",
    "exempt",
    "outside-scope",
    "domestic-reverse-charge",
    "services-reverse-charge",
    "postponed-import-vat",
    "ni-eu-goods",
];

/// Drop the pence, towards zero: boxes 6 to 9 are whole pounds.
fn whole_pounds(amount: &Money) -> Money {
    money(round_div(amount.minor, 100, "down") * 100, &amount.currency)
}

/// The nine boxes of a UK VAT return for a period.
///
/// Each transaction feeds the boxes its direction and treatment call for
/// (VAT Notice 700/12 sections 3 and 4.6); boxes 6 to 9 drop the pence after
/// totalling.
///
/// # Panics
/// Panics on a period that ends before it starts, an unknown direction or
/// treatment, non-GBP amounts, or VAT on a treatment that carries none.
pub fn vat_return(transactions: &[VatTransaction], period_start: &str, period_end: &str) -> VatReturn {
    if days_between(period_start, period_end) < 0 {
        panic!("VAT return period ends {}, before it starts {}", period_end, period_start);
    }
    let mut boxes: Vec<Money> = (0..10).map(|_| money(0, "GBP")).collect();
    let mut count = 0;
    for (index, t) in transactions.iter().enumerate() {
        let n = index + 1;
        if t.direction != "sale" && t.direction != "purchase" {
            panic!("transaction {}: unknown direction \"{}\"", n, t.direction);
        }
        if !TREATMENTS.contains(&t.treatment.as_str()) {
            panic!("transaction {}: unknown VAT treatment \"{}\"", n, t.treatment);
        }
        if t.net.currency != "GBP" || t.vat.currency != "GBP" {
            panic!("transaction {}: amounts must be in GBP", n);
        }
        let sale = t.direction == "sale";
        if sale && t.treatment == "postponed-import-vat" {
            panic!("transaction {}: postponed-import-vat applies to purchases only", n);
        }
        let zero_vat = if sale {
            ZERO_VAT_SALES.contains(&t.treatment.as_str())
        } else {
            ZERO_VAT_PURCHASES.contains(&t.treatment.as_str())
        };
        if t.vat.minor != 0 && zero_vat {
            panic!("transaction {}: {} {}s must have zero VAT", n, t.treatment, t.direction);
        }
        if days_between(period_start, &t.date) < 0 || days_between(&t.date, period_end) < 0 {
            continue;
        }
        count += 1;

        let mut add = |b: usize, amount: &Money| {
            boxes[b] = add_money(&boxes[b], amount);
        };
        let treatment = t.treatment.as_str();
        if sale {
            if treatment == "taxable" {
                add(1, &t.vat);
            }
            if treatment != "outside-scope" {
                add(6, &t.net);
            }
            if treatment == "ni-eu-goods" {
                add(8, &t.net);
            }
        } else {
            match treatment {
                "taxable" => {
                    add(4, &t.vat);
                    add(7, &t.net);
                }
                "exempt" => add(7, &t.net),
                "domestic-reverse-charge" | "services-reverse-charge" | "postponed-import-vat" => {
                    if treatment == "services-reverse-charge" {
                        add(6, &t.net);
                    }
                    add(1, &t.vat);
                    add(4, &t.vat);
                    add(7, &t.net);
                }
                "ni-eu-goods" => {
                    add(2, &t.vat);
                    add(4, &t.vat);
                    add(7, &t.net);
                    add(9, &t.net);
                }
                _ => {}
            }
        }
    }

    let box3 = add_money(&boxes[1], &boxes[2]);
    let difference = box3.minor - boxes[4].minor;
    VatReturn {
        box1: boxes[1].clone(),
        box2: boxes[2].clone(),
        box3,
        box4: boxes[4].clone(),
        box5: money(difference.abs(), "GBP"),
        box6: whole_pounds(&boxes[6]),
        box7: whole_pounds(&boxes[7]),
        box8: whole_pounds(&boxes[8]),
        box9: whole_pounds(&boxes[9]),
        repayment: difference < 0,
        transactions: count,
    }
}

pub fn vat_transaction_from_value(v: &Value) -> VatTransaction {
    VatTransaction {
        date: v.get("date").as_str().to_string(),
        direction: v.get("direction").as_str().to_string(),
        treatment: v.get("treatment").as_str().to_string(),
        net: money_from_value(v.get("net")),
        vat: money_from_value(v.get("vat")),
    }
}

pub fn vat_return_to_value(r: &VatReturn) -> Value {
    Value::obj(vec![
        ("box1", money_to_value(&r.box1)),
        ("box2", money_to_value(&r.box2)),
        ("box3", money_to_value(&r.box3)),
        ("box4", money_to_value(&r.box4)),
        ("box5", money_to_value(&r.box5)),
        ("box6", money_to_value(&r.box6)),
        ("box7", money_to_value(&r.box7)),
        ("box8", money_to_value(&r.box8)),
        ("box9", money_to_value(&r.box9)),
        ("repayment", Value::Bool(r.repayment)),
        ("transactions", Value::Int(r.transactions)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let transactions: Vec<VatTransaction> = args[0].as_arr().iter().map(vat_transaction_from_value).collect();
    vat_return_to_value(&vat_return(&transactions, args[1].as_str(), args[2].as_str()))
}