use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value}; use super::payroll_income_tax::income_tax; use super::payroll_national_insurance::national_insurance; use super::payroll_pension_auto_enrolment::pension_contributions; use super::payroll_pension_auto_enrolment_types::PensionScheme; use super::payroll_student_loan::student_loan; use super::payroll_tax_period::tax_period; /// 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 { 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]))) }