tool/forecast/injectorcompanyvehicle.js

import { Injector } from './injector';

/**
 * Creates a company vehicle injector that covers company vehicle benefit and fuel benefit
 * @class
 */
class InjectorCompanyVehicle extends Injector {
    /**
     * @param {struct} skv_config Struct passed in when initializing the injector
     * @param {array} arr_tax_years Array of tax years to add basic salary to
     */
    constructor(skv_config = {}, arr_tax_years = []) {
        super();

        // they keys we definitely need
        this.required_keys = [
            'co2_after_options',
            'fuel_type',
        ];

        const rst_required_keys = InjectorCompanyVehicle.requiredKeysCheck(skv_config, this.required_keys);

        // check to see if any keys missing
        if (
            !rst_required_keys.success
            || rst_required_keys.data.length > 0
        ) {
            throw new Error(`Missing required keys: ${rst_required_keys.data} for InjectorCompanyVehicle`);
        }

        this.skv_default_config = {
            // required key, don't set a proper default
            // co2gpkm emissions after options have been added
            co2_after_options: null,
            // required key, don't set a proper default
            // For determining the correct benefit field. Currently default/electric
            fuel_type: '',
            // required for cars not for vans
            p11d_inc_taxable_options: null,
            // for whether to include fuel benefit
            with_fuel: false,
            // taxed as company car or private van?
            income_tax_group: 'company car',
            // does vehicle meets euro 6d standards
            is_euro_6d: false,
            // Is the private mileage large enough to warrent being taxed (vans only)
            is_significant_priv_mileage: false,
            // An amount that the employee can pay to reduce the BIK value
            annual_contribution: 0,
            // An amount that the employee can pay to reduce the BIK value
            capital_contribution: 0,
            // Either the car allowance or the amount of salary sacrificed. Leave 0 if neither apply
            allowance_sacrifice_total: 0,
            // Amount of allowance/salsac used towards fuel benefit
            allowance_sacrifice_fuel: 0,
            // Setting to true will make sure fuel benefit structure always included
            explicit_zero_fuel: false,
            // market value for private use assets
            market_value: null,
        };

        this.skv_config = this.skv_default_config;

        const keys = Object.keys(skv_config);

        // overwrite the default keys with the config passed in
        keys.forEach((key) => {
            this.skv_config[key] = skv_config[key];
        });

        this.arr_tax_years = arr_tax_years;

        // apply the basic initial structs to income section of arr_tax_years
        // vehicle always present
        this.arr_tax_years = InjectorCompanyVehicle.setup(this.skv_config, this.arr_tax_years, 'vehicle');

        // work out whether to include fuel benefit
        if (
            this.skv_config.with_fuel
            || this.skv_config.explicit_zero_fuel
        ) {
            this.arr_tax_years = InjectorCompanyVehicle.setup(this.skv_config, this.arr_tax_years, 'fuel');
        }

        this.arr_tax_years = InjectorCompanyVehicle.calculate(this.skv_config, this.arr_tax_years);

        // expose functions
        this.adjustBIKbySalarySacrifice = InjectorCompanyVehicle.adjustBIKbySalarySacrifice;
        this.calculateStandardBIK = InjectorCompanyVehicle.calculateStandardBIK;
    }

    /**
     * Calculate benefit in kind
     * @param {struct} skv_config
     * @param {array} arr_tax_years Basic tax years
     * @returns {array} Amended tax years with benefit calculated
     */
    static calculate(skv_config, arr_tax_years) {
        const cnt_arr_tax_years = arr_tax_years.length;
        const arr_return = [];

        for (let i = 0; i < cnt_arr_tax_years; i += 1) {
            const skv_curr_year = arr_tax_years[i];
            const { arr_income } = skv_curr_year;
            const cnt_arr_income = arr_income.length;

            for (let j = 0; j < cnt_arr_income; j += 1) {
                const skv_curr_income = arr_income[j];

                if (
                    skv_curr_income.name === 'Company Vehicle Benefit'
                    || skv_curr_income.name === 'Private Fuel Benefit'
                ) {
                    const skv_new_income = this.calculateSingleYear(
                        skv_config,
                        skv_curr_income,
                        skv_curr_year.skv_tax_company_vehicle,
                        skv_curr_year.months_occupied,
                    );
                    // over write the original income struct
                    skv_curr_year.arr_income[j] = skv_new_income;
                }
            }

            arr_return.push(skv_curr_year);
        }

        return arr_return;
    }

    /**
     * Description
     * @param {string} str_type fuel / vehicle
     * @param {struct} skv_tax_charges
     * @returns {number}
     */
    static getBenefit(
        str_type,
        skv_tax_charges,
    ) {
        // get vehicle or fuel or private use asset benefit struct
        if (str_type === 'fuel') {
            return skv_tax_charges.fuel_benefit;
        } if (skv_tax_charges.vehicle_benefit) {
            return skv_tax_charges.vehicle_benefit;
        }

        return skv_tax_charges.private_use_asset_benefit;
    }

    /**
     * work out the vehicle or fuel bik (no allowance sacrifice or anything
     * complicated like that)
     * @param {string} str_type fuel or vehicle
     * @param {struct} skv_tax_company_vehicle
     * @returns {struct} bik, bik_capcon_diff, bik_anncon_diff
     */
    static calculateStandardBIK(
        str_type,
        skv_config,
        skv_tax_company_vehicle,
    ) {
        const bik = InjectorCompanyVehicle.getBenefit(
            str_type,
            skv_tax_company_vehicle,
        );

        // bik diff will be a reduction to the salary sacrifice/foregone cash
        // benefit, based on capital contributions
        let bik_capcon_diff = 0;
        let bik_anncon_diff = 0;

        if (
            skv_config.income_tax_group === 'company car'
            && str_type === 'vehicle'
        ) {
            const co2_percentage = skv_tax_company_vehicle.co2_charge_percentage;
            // bik = skv_config.p11d_inc_taxable_options * (co2_percentage / 100);

            let bik_less_capcon = (skv_config.p11d_inc_taxable_options - skv_config.capital_contribution) * (co2_percentage / 100);

            // ensure bik is not negative
            bik_less_capcon = Math.max(0, bik_less_capcon);

            // work out the difference in the bik before and after contributions
            bik_capcon_diff = bik - bik_less_capcon;

            // car benefit will need the ann_con reduction
            bik_anncon_diff = skv_config.annual_contribution;
        }

        return {
            bik,
            bik_capcon_diff,
            bik_anncon_diff,
        };
    }

    /**
     * Description
     * @param {string} str_type fuel / vehicle
     * @param {struct} skv_config
     * @param {struct} skv_bik
     * @param {number} ulev_salsac_co2_threshold
     * @returns {struct} bik and allowance sacrificed
     */
    static adjustBIKbySalarySacrifice(
        str_type,
        skv_config,
        skv_bik,
        ulev_salsac_co2_threshold,
    ) {
        const original_bik = skv_bik.bik;
        const { bik_capcon_diff } = skv_bik;

        const skv_return = {
            bik: original_bik,
            is_allowance_sacrifice_taxed: false,
        };

        // get the salsac/allowance value, which may replace the bik
        let bik_salsac = skv_config.allowance_sacrifice_total;

        // if it's fuel, use fuel salsac, otherwise remove fuel amount from total
        if (str_type === 'fuel') {
            bik_salsac = skv_config.allowance_sacrifice_fuel;
        } else {
            bik_salsac -= skv_config.allowance_sacrifice_fuel;
        }

        // if an allowance has been offered but not taken,
        // or salary has been sacrificed, that must be taxed instead of
        // the vehicle BIK as long as the car meets certain conditions:
        // In Jan 2017: car must be taken after April 2017, 75g/km+
        // (OPRA rules)
        // check the salsac has been entered and if it's larger
        if (
            bik_salsac
            && skv_config.co2_after_options > ulev_salsac_co2_threshold
        ) {
            if (bik_salsac > original_bik) {
                skv_return.is_allowance_sacrifice_taxed = true;
                // if the salsac is higher, use that instead of bik
                // we still need to take off any potential savings
                // a capcon would have made to the bik
                skv_return.bik = bik_salsac - bik_capcon_diff;
            }
        }

        // if sal sac has not been used, return the normal bik
        // less any contributions
        if (
            !skv_return.is_allowance_sacrifice_taxed
            && str_type === 'vehicle'
        ) {
            // after the comparison has been made, we can revert to
            // using the bik with the capitalContribution deducted
            skv_return.bik = original_bik - bik_capcon_diff;
        }

        return skv_return;
    }

    /**
     * Calculate the benefit for a single year - work out elec/van/salsac stuff
     * @param {struct} skv_config
     * @param {struct} skv_income
     * @param {struct} skv_tax_charges
     * @returns {struct} Completed income struct
     */
    static calculateSingleYear(skv_config, skv_income, skv_tax_company_vehicle, months_occupied) {
        // the struct to return starting with the basic of the income struct
        // passed in
        const skv_return = skv_income;
        const str_type = skv_income.name === 'Company Vehicle Benefit'
            ? 'vehicle'
            : 'fuel';

        let bik = 0;

        // if we don't need fuel ben, but do need a 0 fuel value
        // or
        // Vehicle benefit on a van is only taxable
        // if there is significant private mileage
        if (
            (
                str_type === 'fuel'
                && !skv_config.with_fuel
                && skv_config.explicit_zero_fuel
            ) || (
                skv_config.is_van
                && !skv_config.is_significant_priv_mileage
            )
        ) {
            skv_return.value = bik;
            return skv_return;
        }

        // work out the benefit in kind before any allowance sacrifice rules
        // are taken in to account later on
        const skv_result_bik = InjectorCompanyVehicle.calculateStandardBIK(
            str_type,
            skv_config,
            skv_tax_company_vehicle,
        );

        bik = skv_result_bik.bik;

        // bik diff will be a reduction to the salary sacrifice/foregone cash
        // benefit, based on capital contributions
        const { bik_anncon_diff } = skv_result_bik;

        skv_return.original_bik = bik;

        // work out the adjustment to the benefit in kind based on vehicle or fuel
        // allowance, whether its a ulev
        const skv_adjusted_bik = InjectorCompanyVehicle.adjustBIKbySalarySacrifice(
            str_type,
            skv_config,
            skv_result_bik,
            skv_tax_company_vehicle.ulev_salsac_co2_threshold,
        );

        // use the adjusted bik
        bik = skv_adjusted_bik.bik;

        // whatever has happened, salsac or not, we need to remove
        // the annual annualContribution for car benefit (not fuel, not vans)
        bik -= bik_anncon_diff;

        // make sure benefit is not negative
        bik = Math.max(bik, 0);

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

        // Cars are always considered in the Employees tax and Employers 1A NIC
        skv_return.value = bik * coverage_ratio;
        // update the income struct
        skv_return.is_allowance_sacrifice_taxed = skv_adjusted_bik.is_allowance_sacrifice_taxed;

        return skv_return;
    }

    /**
     * Do the setup to get basic structs for income
     * @param {struct} skv_config Struct passed in when initializing the injector
     * @param {array} arr_tax_years Array of tax years to add basic salary to
     * @returns {array} Amended tax years
     */
    static setup(skv_config, arr_tax_years, type) {
        const arr_temp = arr_tax_years;
        const cnt_arr_temp = arr_temp.length;

        for (let i = 0; i < cnt_arr_temp; i += 1) {
            const skv_return = {};
            skv_return.name = type === 'vehicle' ? 'Company Vehicle Benefit' : 'Private Fuel Benefit';
            skv_return.is_benefit = true;
            skv_return.is_employee_nic_taxable = false;
            skv_return.is_provided_by_employer = true;
            skv_return.is_taxed_at_source = false;
            skv_return.is_relief = false;
            skv_return.is_allowance_sacrifice_taxed = false;

            if (!Object.prototype.hasOwnProperty.call(arr_temp[i], 'arr_income')) {
                arr_temp[i].arr_income = [];
            }

            arr_temp[i].arr_income.push(skv_return);
        }

        return arr_temp;
    }
}

export {
    InjectorCompanyVehicle,
};