Functional Weave
Code in Rust

payroll.gross-to-net@1.0.1

impl/rust.rs

5,882 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::money_amount::{money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune
use super::payroll_income_tax::income_tax;  ← from payroll.income-tax ^1.0.0 · built alongside by fune
use super::payroll_national_insurance::national_insurance;  ← from payroll.national-insurance ^1.0.0 · built alongside by fune
use super::payroll_pension_auto_enrolment::pension_contributions;  ← from payroll.pension-auto-enrolment ^1.0.0 · built alongside by fune
use super::payroll_pension_auto_enrolment_types::PensionScheme;
use super::payroll_student_loan::student_loan;  ← from payroll.student-loan ^1.0.0 · built alongside by fune
use super::payroll_tax_period::tax_period;  ← from payroll.tax-period ^1.0.0 · built alongside by fune

/// One period's payslip, in the order a UK payroll works it out.
///
/// The order matters because each deduction is taken from a different figure:
/// a net pay arrangement pension comes off pay before tax but not before
/// National Insurance; tax is on taxable pay to date; NI and student loans are
/// on this period's gross pay alone; relief at source pension comes off net pay.
///
/// # Panics
/// Panics on anything the capabilities it calls refuse, a non-GBP gross, or a
/// student loan plan listed twice.
pub fn gross_to_net(input: &PayslipInput) -> Payslip {
    let gross = &input.gross;
    if gross.currency != "GBP" {
        panic!(
            "payroll amounts must be in GBP, received {} for gross",
            gross.currency
        );
    }
    let when = tax_period(&input.pay_date, &input.frequency);

    let mut pension_employee = 0i64;
    let mut pension_employer = 0i64;
    let mut before_tax = 0i64;
    if let Some(scheme) = &input.pension {
        let pension = pension_contributions(gross, &input.frequency, scheme, &input.pay_date);
        pension_employee = pension.employee_deduction.minor;
        pension_employer = pension.employer.minor;
        if pension.deduct_before_tax {
            before_tax = pension.employee_deduction.minor;
        }
    }
    let taxable_pay = money(gross.minor - before_tax, "GBP");

    let tax = income_tax(
        &input.tax_code,
        &input.frequency,
        when.period,
        &taxable_pay,
        &input.previous_taxable_pay_to_date,
        &input.previous_tax_to_date,
        &input.pay_date,
    );
    let ni = national_insurance(gross, &input.ni_category, &input.frequency, &input.pay_date, None);

    // An employee with more than one undergraduate plan repays once, against
    // the lowest threshold, which is the largest of the per-plan deductions; a
    // postgraduate loan is repaid alongside it.
    let mut undergraduate = 0i64;
    let mut postgraduate = 0i64;
    let mut seen: Vec<&str> = Vec::new();
    for plan in &input.student_loan_plans {
        if seen.contains(&plan.as_str()) {
            panic!("student loan plan {} is listed twice", plan);
        }
        seen.push(plan);
        let deduction = student_loan(gross, plan, &input.frequency, &input.pay_date).minor;
        if plan == "postgraduate" {
            postgraduate = deduction;
        } else if deduction > undergraduate {
            undergraduate = deduction;
        }
    }

    let net = gross.minor - pension_employee - tax.tax.minor - ni.employee.minor - undergraduate - postgraduate;
    Payslip {
        gross: gross.clone(),
        pension_employee: money(pension_employee, "GBP"),
        pension_employer: money(pension_employer, "GBP"),
        taxable_pay,
        tax: tax.tax,
        employee_ni: ni.employee,
        employer_ni: ni.employer,
        student_loan: money(undergraduate, "GBP"),
        postgraduate_loan: money(postgraduate, "GBP"),
        net: money(net, "GBP"),
        taxable_pay_to_date: tax.pay_to_date,
        tax_to_date: tax.tax_to_date,
        period: when.period,
        tax_year: when.tax_year,
    }
}

fn optional_int(v: &Value) -> Option<i64> {
    if v.is_null() {
        None
    } else {
        Some(v.as_i64())
    }
}

pub fn payslip_input_from_value(v: &Value) -> PayslipInput {
    let pension = v.get("pension");
    PayslipInput {
        gross: money_from_value(v.get("gross")),
        frequency: v.get("frequency").as_str().to_string(),
        pay_date: v.get("payDate").as_str().to_string(),
        tax_code: v.get("taxCode").as_str().to_string(),
        previous_taxable_pay_to_date: money_from_value(v.get("previousTaxablePayToDate")),
        previous_tax_to_date: money_from_value(v.get("previousTaxToDate")),
        ni_category: v.get("niCategory").as_str().to_string(),
        student_loan_plans: v
            .get("studentLoanPlans")
            .as_arr()
            .iter()
            .map(|p| p.as_str().to_string())
            .collect(),
        pension: if pension.is_null() {
            None
        } else {
            Some(PensionScheme {
                employee_basis_points: optional_int(pension.get("employeeBasisPoints")),
                employer_basis_points: optional_int(pension.get("employerBasisPoints")),
                earnings_basis: pension.get("earningsBasis").as_str().to_string(),
                arrangement: pension.get("arrangement").as_str().to_string(),
            })
        },
    }
}

pub fn payslip_to_value(p: &Payslip) -> Value {
    Value::obj(vec![
        ("gross", money_to_value(&p.gross)),
        ("pensionEmployee", money_to_value(&p.pension_employee)),
        ("pensionEmployer", money_to_value(&p.pension_employer)),
        ("taxablePay", money_to_value(&p.taxable_pay)),
        ("tax", money_to_value(&p.tax)),
        ("employeeNi", money_to_value(&p.employee_ni)),
        ("employerNi", money_to_value(&p.employer_ni)),
        ("studentLoan", money_to_value(&p.student_loan)),
        ("postgraduateLoan", money_to_value(&p.postgraduate_loan)),
        ("net", money_to_value(&p.net)),
        ("taxablePayToDate", money_to_value(&p.taxable_pay_to_date)),
        ("taxToDate", money_to_value(&p.tax_to_date)),
        ("period", Value::Int(p.period)),
        ("taxYear", Value::str(&p.tax_year)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    payslip_to_value(&gross_to_net(&payslip_input_from_value(&args[0])))
}