tool/salarysacrifice/calculate.js

import { formatStruct } from '../../format';
import { averageFinanceCost } from '../../calculations/finance';
import * as utils from './utils';
import * as configuration from './configuration';

/**
 * Extra salary sacrifice calculations to the ones done in tool/forecast
 * @module
 * @since 0.1.0
 */

/**
     * Calculates what the salary sacrifice needs to be in order for the employer to have no difference
     * @param {struct} skv_config
     * @param {array} arr_tax_charges income tax, nic rates for the period retrieved from pluto
     * @param {array} arr_tax_company_vehicle vehicle benefit, co2 percentage charge for the period retrieved from pluto
     * @returns {array} Tax years with extra salary sacrifice info
     */
const calculateEmployerBreakEvenPoint = (
    skv_config,
    arr_tax_charges,
    arr_tax_company_vehicle,
) => {
    const arr_return = [];
    const config = JSON.parse(JSON.stringify(skv_config));
    const { skv_price } = config;

    // check whether employer or employee pays for insurance
    if (!config.employer_pays_insurance) {
        config.insurance_cost_pa = 0;
    }

    if (!config.num_total_options_net) {
        config.num_total_options_net = 0;
    }

    // run the forecast injectors
    const arr_tax_years = utils.getTaxYears(
        config,
        arr_tax_charges,
        arr_tax_company_vehicle,
    );

    let employer_break_even = 0;

    const num_years = skv_price.months / 12;
    const cnt_tax_years = arr_tax_years.length;

    for (let i = 0; i < cnt_tax_years; i += 1) {
        const skv_tax_year = arr_tax_years[i];

        // get vat rate
        const vat_percentage = skv_tax_year.skv_tax_charges.vat[0].standard;

        // calculations to include in a new struct on array tax years
        // essentially the impact on the employer
        const skv_return = {};

        // ratio for months in taxed year
        const coverage_ratio = skv_tax_year.months_occupied / 12;

        // work out the bch cost and irrecoverable vat
        const skv_bch_result = utils.calculateBCHcost(
            {
                finance_cost: skv_price.finance_cost,
                service_fee: skv_price.service_fee,
                // get options in to months so its the same as finance cost and service fee
                num_options_net: config.num_total_options_net / (num_years * 12),
                vat_percentage,
                employer_can_reclaim_vat_on_bch: config.employer_can_reclaim_vat_on_bch,
            },
        );

        // cost of bch for this tax year (includes options)
        const bch_avg_pm = averageFinanceCost(
            {
                price: skv_bch_result.bch_cost,
                initial_payment: skv_price.initial_payment,
                months: skv_price.months,
            },
        ).per_month;

        skv_return.bch_cost_total = bch_avg_pm * skv_tax_year.months_occupied;

        // irrecoverable vat for the entire term
        const non_recoverable_vat_avg_pm = averageFinanceCost(
            {
                price: skv_bch_result.non_recoverable_vat,
                initial_payment: skv_price.initial_payment,
                months: skv_price.months,
            },
        ).per_month;

        // irrecoverable vat for this tax year
        skv_return.non_recoverable_vat = non_recoverable_vat_avg_pm * skv_tax_year.months_occupied;

        // add the vat to the avg bch cost to get avg pch
        // const pch_cost = avg_bch_cost_pa * (1 + vat_dec);
        // const pch_cost_pm = pch_cost / 12;
        // const pch_cost_total = pch_cost * coverage_ratio;

        skv_return.insurance_cost = config.insurance_cost_pa * coverage_ratio;
        // skv_return.bch_contingency = skv_return.bch_cost_total * (config.bch_contingency_percentage / 100);
        skv_return.early_termination_ins = skv_return.bch_cost_total
                                        + skv_return.insurance_cost
                                        + skv_return.non_recoverable_vat;

        skv_return.early_termination_ins *= (config.early_termination_ins_percent / 100);
        skv_return.insurance_premium_tax = skv_return.early_termination_ins * (config.insurance_premium_tax_percentage / 100);

        // The employer may choose to keep the saving in Employers National Insurance on the salary sacrifice,
        // or it might choose to include the saving in calculating a neutral impact,
        // Or there is the Net approach where the Employer keeps the NI saving on the sacrifice less the cost of the additional Class 1A NI on the car.
        skv_return.company_vehicle_ben_nic = config.employer_keeps_ni === 'net' ? 0 : skv_tax_year.arr_employer_nic[1].tax_total;
        skv_return.employer_net_vehicle_ben_nic_deduction = config.employer_keeps_ni !== 'net' ? 0 : skv_tax_year.arr_employer_nic[1].tax_total;
        skv_return.fuel_ben_nic = 0;

        if (config.with_private_fuel) {
            skv_return.fuel_ben_nic = skv_tax_year.arr_employer_nic[2].tax_total;
        }

        // Charger installation cost should only be available for electrics if override set
        let { charger_installation_cost } = config;
        if (!config.charger_installation_available_for_phev) {
            charger_installation_cost = arr_tax_company_vehicle[0].fuel_type === 'electric' ? charger_installation_cost : 0;
        }

        // only include charger installaition cost in the first year
        skv_return.charger_installation_cost = i === 0
            ? charger_installation_cost
            : 0;

        skv_return.total = skv_return.bch_cost_total
                                    + skv_return.non_recoverable_vat
                                    + skv_return.insurance_cost
                                    + skv_return.charger_installation_cost
                                    + skv_return.early_termination_ins
                                    + skv_return.insurance_premium_tax
                                    + skv_return.company_vehicle_ben_nic
                                    + skv_return.fuel_ben_nic;

        const er1anic_perc_dec = skv_tax_year.skv_tax_charges.national_insurance.employer.class1A / 100;
        const er1anic_perc_int = 1 + er1anic_perc_dec;

        skv_return.employer_break_even = skv_return.total / er1anic_perc_int;

        employer_break_even += skv_return.employer_break_even;

        skv_tax_year.skv_salary_sacrifice = {
            skv_impact_on_er: skv_return,
            // pch_cost_pm,
            // pch_cost_total,
        };

        arr_return.push(skv_tax_year);
    }

    const employer_break_even_avg_pm = employer_break_even / config.months;
    const employer_break_even_avg_yr = employer_break_even_avg_pm * 12;

    return {
        arr_tax_years: arr_return,
        skv_salary_sacrifice_totals: {
            employer_break_even,
            employer_break_even_avg_pm,
            employer_break_even_avg_yr,
            employer_ni_savings: 0,
        },
    };
};

/**
 * Description
 * @param {data_type} param_name Description
 * @returns {data_type} Description
 */
const getDifference = (
    config,
    arr_tax_years_no_salsac,
    arr_tax_years_with_salsac,
) => {
    const arr_return = [];
    const cnt_tax_years = arr_tax_years_no_salsac.length;

    const totals = {
        employee: {
            impact: 0,
        },
        employer: {
            impact: 0,
        },
    };

    for (let i = 0; i < cnt_tax_years; i += 1) {
        const skv_yr_no_salsac = arr_tax_years_no_salsac[i];
        const skv_yr_with_salsac = arr_tax_years_with_salsac[i];
        // start with the standard tax year date info
        const skv_return = {
            tax_year: skv_yr_no_salsac.tax_year,
            year_short: skv_yr_no_salsac.year_short,
            year_slash: skv_yr_no_salsac.year_slash,
            year_text: skv_yr_no_salsac.year_text,
            date_end: skv_yr_no_salsac.date_end,
            months_occupied: skv_yr_no_salsac.months_occupied,
            description: skv_yr_no_salsac.description,
        };

        const coverage_ratio = arr_tax_years_with_salsac[i].months_occupied / 12;

        /*
         EMPLOYEE
        */

        // changes to employee
        skv_return.employee = {
            salary: (skv_yr_no_salsac.arr_income[0].value - skv_yr_with_salsac.arr_income[0].value) * -1,
            income_tax_on_salary: (skv_yr_no_salsac.arr_income_tax[0].tax_total - skv_yr_with_salsac.arr_income_tax[0].tax_total),
            company_car_tax: skv_yr_with_salsac.arr_income_tax[1].tax_total * -1,
            income_tax_on_fuel_ben: skv_yr_with_salsac.arr_income_tax[2].tax_total * -1,
            insurance_cost: config.employer_pays_insurance ? 0 : (config.insurance_cost_pa * coverage_ratio) * -1,
            amaps: (config.amaps * coverage_ratio) * -1,
            nic_on_salary: (skv_yr_no_salsac.arr_employee_nic[0].tax_total - skv_yr_with_salsac.arr_employee_nic[0].tax_total),
            net_pay: (skv_yr_no_salsac.totals.income.net_pay - skv_yr_with_salsac.totals.income.net_pay) * -1,
        };

        // separate subtotal as insurance isn't a part of the tax forecasters
        skv_return.employee.total_impact = skv_return.employee.net_pay
                                            + skv_return.employee.insurance_cost
                                            + skv_return.employee.amaps;

        // format entire struct to a currency type
        skv_return.employee = formatStruct(skv_return.employee, 'currency', false);

        /*
         EMPLOYEE - TOTAL for entire period
        */

        // convert object to key's array
        let keys = Object.keys(skv_return.employee);

        // iterate over object
        keys.forEach((key) => {
            // if we haven't added the key yet, default to 0
            if (totals.employee[key] === undefined) {
                totals.employee[key] = 0;
            }

            totals.employee[key] += skv_return.employee[key];
        });

        /*
         EMPLOYER
        */

        const { skv_impact_on_er } = skv_yr_with_salsac.skv_salary_sacrifice;

        // Charger installation cost should only be available for electrics if override set
        let { charger_installation_cost } = skv_impact_on_er;
        if (!config.charger_installation_available_for_phev) {
            charger_installation_cost = config.fuel_type === 'electric' ? skv_impact_on_er.charger_installation_cost : 0;
        }

        // changes to employer
        skv_return.employer = {
            salary: skv_yr_no_salsac.arr_income[0].value - skv_impact_on_er.salary,
            nic_on_salary: skv_yr_no_salsac.arr_employer_nic[0].tax_total - skv_yr_with_salsac.arr_employer_nic[0].tax_total,
            bch_cost_total: skv_impact_on_er.bch_cost_total * -1,
            non_recoverable_vat: skv_impact_on_er.non_recoverable_vat * -1,
            insurance_cost: skv_impact_on_er.insurance_cost * -1,
            charger_installation_cost: charger_installation_cost * -1,
            early_termination_ins: skv_impact_on_er.early_termination_ins * -1,
            ins_premium_tax: skv_impact_on_er.insurance_premium_tax * -1,
            company_vehicle_ben_nic: skv_impact_on_er.company_vehicle_ben_nic * -1,
            fuel_ben_nic: skv_impact_on_er.fuel_ben_nic * -1,
            cost_of_benefits: skv_impact_on_er.total * -1,
            vehicle_ben_nic_savings_deduction: skv_impact_on_er.employer_net_vehicle_ben_nic_deduction,
        };

        skv_return.employer.savings_pool = (skv_return.employer.salary
                                            + skv_return.employer.nic_on_salary)
                                            - skv_impact_on_er.employer_net_vehicle_ben_nic_deduction;

        skv_return.employer.total_impact = skv_return.employer.savings_pool
                                            + skv_return.employer.cost_of_benefits;

        // format entire struct to a currency type
        skv_return.employer = formatStruct(skv_return.employer, 'currency', false);

        /*
         EMPLOYER - TOTAL for entire period
        */

        // convert object to key's array
        keys = Object.keys(skv_return.employer);

        // iterate over object
        keys.forEach((key) => {
            // if we haven't added the key yet, default to 0
            if (totals.employer[key] === undefined) {
                totals.employer[key] = 0;
            }

            totals.employer[key] += skv_return.employer[key];
        });

        arr_return.push(skv_return);
    }

    return {
        totals,
        arr_differences: arr_return,
    };
};

/**
 * Determine what the tax years calcutions will be with and without the salary sacrifice
 * @param {struct} skv_config
 * @param {array} arr_tax_charges
 * @param {array} arr_tax_company_vehicle
 * @returns {array}
 */
const calculate = (
    skv_config,
    arr_tax_charges,
    arr_tax_company_vehicle,
) => {
    const init_config = configuration.create(skv_config, 'init');
    const local_arr_tax_charges = JSON.parse(JSON.stringify(arr_tax_charges));

    /*

    CALCULATE WITH NO SALARY SACRIFICE

    */
    const no_salsac_config = configuration.create(skv_config, 'without_salsac');

    // calculate the tax years with no salary sacrifice for the employee
    const arr_tax_years_no_salsac = utils.getTaxYears(
        no_salsac_config,
        local_arr_tax_charges,
        arr_tax_company_vehicle,
    );

    /*

    CALCULATE THE EMPLOYER'S BREAK EVEN POINT

    */

    // calculate the employer break even point
    const rst_tax_years_and_break_point = calculateEmployerBreakEvenPoint(
        init_config,
        local_arr_tax_charges,
        arr_tax_company_vehicle,
    );

    const arr_tax_years_breakpoint = rst_tax_years_and_break_point.arr_tax_years;

    /*

    WORK OUT EFFECT OF SACRIFICE ON SALARY (inc whether employer is keeping ni savings)

    */

    let { skv_salary_sacrifice_totals } = rst_tax_years_and_break_point;

    // save the original employer break even point to compare to updated break even
    // later
    const original_employer_break_even_avg_yr = skv_salary_sacrifice_totals.employer_break_even_avg_yr;

    // single number for break even point average across the years
    const employer_break_even_avg_yr = utils.applyEmployerKeepsNIpercentage(
        'updated',
        init_config.employer_keeps_ni,
        init_config.employer_keeps_ni_percentage,
        skv_salary_sacrifice_totals.employer_break_even_avg_yr,
        arr_tax_years_no_salsac,
    );

    // start a new config to calculate tax years with salsac
    const config_with_salsac = configuration.create(
        init_config,
        'with_salsac',
        {
            employer_break_even_avg_yr,
        },
    );
    /*

    CALCULATE WITH SALARY SACRIFICE

    */

    // calculate the tax years again with the salary sacrifice for the employee
    const arr_tax_years_with_salsac = utils.getTaxYears(
        config_with_salsac,
        local_arr_tax_charges,
        arr_tax_company_vehicle,
    );

    const cnt_years = arr_tax_years_breakpoint.length;

    // update salsac tax years with amended salary with sacrifice from earlier
    for (let i = 0; i < cnt_years; i += 1) {
        const coverage_ratio = arr_tax_years_with_salsac[i].months_occupied / 12;

        arr_tax_years_with_salsac[i].skv_salary_sacrifice = arr_tax_years_breakpoint[i].skv_salary_sacrifice;

        const employer_ni_savings = (employer_break_even_avg_yr - original_employer_break_even_avg_yr) * coverage_ratio;

        arr_tax_years_with_salsac[i].skv_salary_sacrifice.skv_impact_on_er.employer_ni_savings = employer_ni_savings;

        // include the salary with employer ni savings included
        arr_tax_years_with_salsac[i].skv_salary_sacrifice.skv_impact_on_er.employer_break_even = employer_break_even_avg_yr * coverage_ratio;
        arr_tax_years_with_salsac[i].skv_salary_sacrifice.skv_impact_on_er.salary = (init_config.salary_pa - employer_break_even_avg_yr) * coverage_ratio;

        // update totals with new salary sacrifice and ni savings
        skv_salary_sacrifice_totals.employer_ni_savings += employer_ni_savings;
        skv_salary_sacrifice_totals.employer_break_even_avg_yr = employer_break_even_avg_yr;
        skv_salary_sacrifice_totals.employer_break_even_avg_pm = employer_break_even_avg_yr / 12;
    }

    /*

    WORK OUT THE DIFFERENCE BETWEEN WITH AND WITHOUT SALARY SACRIFICE

    */

    // find the differences between having salary sacrifice and put in an easy
    // to use form
    const skv_tax_years_difference = getDifference(
        init_config,
        arr_tax_years_no_salsac,
        arr_tax_years_with_salsac,
    );

    /*

    SOME ADDIIONAL TOTALS

    */
    skv_salary_sacrifice_totals.employee = skv_tax_years_difference.totals.employee;
    skv_salary_sacrifice_totals.employee.total_impact_avg_pm = skv_tax_years_difference.totals.employee.total_impact / init_config.months;
    skv_salary_sacrifice_totals.employee.total_impact_avg_pa = skv_salary_sacrifice_totals.employee.total_impact_avg_pm * 12;

    skv_salary_sacrifice_totals.employee.net_pay_pm = skv_tax_years_difference.totals.employee.total_impact / init_config.months;
    skv_salary_sacrifice_totals.employee.net_pay_pa = skv_salary_sacrifice_totals.employee.net_pay_pm * 12;

    skv_salary_sacrifice_totals.employer = skv_tax_years_difference.totals.employer;
    skv_salary_sacrifice_totals.employer.total_impact_avg_pm = skv_tax_years_difference.totals.employer.total_impact / init_config.months;
    skv_salary_sacrifice_totals.employer.total_impact_avg_pa = skv_salary_sacrifice_totals.employer.total_impact_avg_pm * 12;

    // format the totals
    skv_salary_sacrifice_totals = formatStruct(skv_salary_sacrifice_totals, 'currency', false);

    return {
        arr_no_salsac: arr_tax_years_no_salsac,
        arr_with_salsac: arr_tax_years_with_salsac,
        arr_difference: skv_tax_years_difference.arr_differences,
        skv_salary_sacrifice_totals,
    };
};

export {
    calculateEmployerBreakEvenPoint,
    calculate,
};