tool/forecast/injectortaxcharges.js

/* eslint-disable */

import { Injector } from './injector';

/**
 * Creates a tax charges injector
 * @class
 */
class InjectorTaxCharges extends Injector {
    /**
     * @param {struct} skv_config Struct passed in when initializing the injector
     * @param {array} arr_tax_years Array of basic tax years that we will manipulate
     * and pass back
     * @param {array} arr_tax_charges Array of tax charges data from pluto
     * @param {array} arr_tax_company_vehicle Array of company vehicle tax data from pluto
     */
    constructor(
        skv_config = {},
        arr_tax_years = [],
        arr_tax_charges = [],
        arr_tax_company_vehicle = [],
    ) {
        super();
        this.skv_config = skv_config;
        this.arr_tax_years = arr_tax_years;
        this.arr_tax_charges = arr_tax_charges;
        this.arr_tax_company_vehicle = arr_tax_company_vehicle;

        // expose functions
        this.removeUnrequiredRegions = InjectorTaxCharges.removeUnrequiredRegions;
        this.removeChargeYears = InjectorTaxCharges.removeChargeYears;
        this.duplicateChargeYears = InjectorTaxCharges.duplicateChargeYears;
        this.addTaxChargesToTaxYears = InjectorTaxCharges.addTaxChargesToTaxYears;

        // remove unrequired regions
        this.arr_tax_charges = InjectorTaxCharges.removeUnrequiredRegions(this.skv_config.region_code, this.arr_tax_charges);

        // if the first year of tax charges doesn't match up with the first year
        // of tax years we need to remove it (and potentially others).
        // This may happen in the case where a start date of April 20xx is passed in)
        if (this.arr_tax_years[0].year_slash !== this.arr_tax_charges[0].year) {
            this.arr_tax_charges = InjectorTaxCharges.removeChargeYears(this.arr_tax_years, this.arr_tax_charges);
        }

        // check whether we need to add extra years to tax charges
        if (this.arr_tax_years.length > this.arr_tax_charges.length) {
            this.arr_tax_charges = InjectorTaxCharges.duplicateChargeYears(this.arr_tax_years, this.arr_tax_charges);
        } else if(this.arr_tax_years.length < this.arr_tax_charges.length) {
            this.arr_tax_charges = InjectorTaxCharges.removeChargeYears(this.arr_tax_years, this.arr_tax_charges);
        }

        // update the limits and thresholds to be based on months occupied
        this.arr_tax_charges = this.updateToUseMonthsOccupied(this.arr_tax_years, this.arr_tax_charges);

        // check whether we need to add extra years to tax company vehicle
        if (
            this.arr_tax_company_vehicle.length > 0
            && this.arr_tax_years.length > this.arr_tax_company_vehicle.length
        ) {
            this.arr_tax_company_vehicle = InjectorTaxCharges.duplicateChargeYears(this.arr_tax_years, this.arr_tax_company_vehicle);
        }
        
        this.arr_tax_years = InjectorTaxCharges.addTaxChargesToTaxYears(this.arr_tax_years, this.arr_tax_charges);
        
        const cnt_years = this.arr_tax_years.length;

        // merge tax company vehicle in to tax years if we have it
        if (this.arr_tax_company_vehicle.length > 0) {
            for (let i = 0; i < cnt_years; i += 1) {
                this.arr_tax_years[i].skv_tax_company_vehicle = this.arr_tax_company_vehicle[i];
            }
        }
    }

    /**
     * We don't want redundant region data in tax charges, so remove it
     * @param {string} region_code_to_keep
     * @returns {array} Tax charges with regions removed from income tax
     */
    static removeUnrequiredRegions(region_code_to_keep, arr_tax_charges) {
        const cnt_years = arr_tax_charges.length;

        // loop through each tax charge year
        for (let i = 0; i < cnt_years; i += 1) {
            // currently only income tax has different regions
            const arr_income_tax = arr_tax_charges[i].income_tax;

            // remove any structs that aren't equal to the array
            arr_income_tax.regions = arr_income_tax.regions.filter((region) => region.name === region_code_to_keep);

            if (arr_income_tax.regions.length === 0) {
                throw new Error(`A region code of ${region_code_to_keep} was not found in income tax regions of provided tax charges`);
            }

            // eslint-disable-next-line no-param-reassign
            arr_tax_charges[i].income_tax = arr_income_tax;
        }

        return arr_tax_charges;
    }

    /**
     * See if we need to duplicate tax charge years to match the tax years
     * @param {array} arr_tax_years Basic tax years
     * @param {array} arr_tax_rates Tax rates, bands etc could be charges or company vehicle from pluto
     * @returns {array} Amended tax charges array
     */
    static duplicateChargeYears(arr_tax_years, arr_tax_rates) {
        const cnt_tax_years = arr_tax_years.length;
        const cnt_tax_rates = arr_tax_rates.length;
        // start with the original array and add any years we don't have to it
        const arr_return = JSON.parse(JSON.stringify(arr_tax_rates));

        if (cnt_tax_years > cnt_tax_rates) {
            // how many years we need to add on to the end of the tax charges array
            const num_to_duplicate = cnt_tax_years - cnt_tax_rates;

            // get the last year we have data for
            const skv_duplicate = JSON.parse(JSON.stringify(arr_return[cnt_tax_rates - 1]));

            for (let i = 0; i < num_to_duplicate; i += 1) {
                // delete skv_duplicate.year;
                const skv_new_rate_struct = JSON.parse(JSON.stringify(skv_duplicate));

                // add 1 to the year
                const arr_year = skv_new_rate_struct.year.split('/');

                arr_year[0] = Number(arr_year[0]) + (i + 1);
                arr_year[1] = Number(arr_year[1]) + (i + 1);

                skv_new_rate_struct.year = `${arr_year[0]}/${arr_year[1]}`;

                arr_return.push(skv_new_rate_struct);
            }
        }

        return arr_return;
    }

    /**
    * See if we need to remove tax charge years to match the tax years
    * @param {array} arr_tax_years Basic tax years
    * @param {array} arr_tax_rates Tax rates, bands etc could be charges or company vehicle from pluto
    * @returns {array} Amended tax charges array
    */
    static removeChargeYears(arr_tax_years, arr_tax_rates) {

        const cnt_tax_years = arr_tax_years.length;
        const cnt_tax_rates = arr_tax_rates.length;
        // start with the original array and add any years we don't have to it
        let arr_return = JSON.parse(JSON.stringify(arr_tax_rates));

        // remove any extra charge years
        if (cnt_tax_years < cnt_tax_rates) {
            const num_to_remove = cnt_tax_rates - cnt_tax_years;
            const end_idx = (cnt_tax_rates) - num_to_remove;
            arr_return = arr_return.slice(0, end_idx);
        }

        let allow_removal = true;
                
        // remove any non-matching charge years, starting with the first in the
        // array
        for(let i = 0; i < cnt_tax_years; i++) {

            // if we have found a matching year already then don't allow any 
            // subsequent removals
            if (
                arr_return.length > i
                && arr_tax_years[i].year_slash === arr_return[i].year
            ) {
                allow_removal = false;
            }

            // if no mismatched tax charges have been removed then check to do
            // so here 
            if (
                allow_removal
                && arr_return.length > i 
                && arr_tax_years[i].year_slash !== arr_return[i].year
            ) {
                // remove first element of tax charges
                arr_return.shift();
            }
        } 

        // if no tax charges left throw warning
        if (arr_return.length === 0) {
            throw new Error('No matching tax charges for tax years');
        }

        return arr_return;
    }

    /**
     * Updated tax years with the tax charges
     * @param {array} arr_tax_years
     * @param {array} arr_tax_charges
     * @returns {array}
     */
    static addTaxChargesToTaxYears(arr_tax_years, arr_tax_charges) {
        const arr_return = JSON.parse(JSON.stringify(arr_tax_years));
        const cnt_years = arr_return.length;

        // merge tax charges in to tax years
        for (let i = 0; i < cnt_years; i += 1) {
            arr_return[i].skv_tax_charges = arr_tax_charges[i];

            // check tax charge year matches tax year year and throw error if it doesn't
            if (arr_return[i].year_slash !== arr_tax_charges[i].year) {
                throw new Error(`Tax year ${arr_return[i].year_slash} does not match tax charge ${arr_tax_charges[i].year}`);
            }
        }

        return arr_return;
    }

    /**
     * Description
     * @param {data_type} param_name Description
     * @returns {data_type} Description
     */
    updateToUseMonthsOccupied(arr_tax_years, arr_tax_rates) {
        // at this point tax years and tax rates length will match
        const cnt_tax_rates = arr_tax_rates.length;
        const arr_return = [];

        for (let i = 0; i < cnt_tax_rates; i += 1) {
            const skv_yr_tax_rates = JSON.parse(JSON.stringify(arr_tax_rates[i]));
            const skv_tax_year = arr_tax_years[i];

            const { months_occupied } = skv_tax_year;
            const coverage_ratio = months_occupied / 12;

            const skv_updated_rates = this.formatData(skv_yr_tax_rates, coverage_ratio);

            arr_return.push(skv_updated_rates);
        }

        return arr_return;
    }

    /**
      * Recurse through a deeply nested object and update the desired key values
      * with the coverage ratio
      * @param {anything} data
      * @param {number} coverage_ratio
      * @param {string} curr_key We need current key to check whether its a value
      * to multiply by coverage ratio
      * @returns {anything}
      */
    formatData(data, coverage_ratio, curr_key = '') {
        // if one of these keys is found then apply the coverage ratio
        const arr_keys_to_update = ['limit', 'restriction_threshold'];

        if (typeof data === 'object') {
            const arr_keys = Object.keys(data);
            const cnt_keys = arr_keys.length;
            const data_return = JSON.parse(JSON.stringify(data));

            for (let i = 0; i < cnt_keys; i += 1) {
                const data_key = arr_keys[i];
                data_return[data_key] = this.formatData(data_return[data_key], coverage_ratio, data_key);
            }
            return data_return;
        } if (
            typeof data === 'number'
            // check whether its one of the designated keys to update
            && arr_keys_to_update.includes(curr_key)
        ) {
            // we'll just have a value at this point
            return data * coverage_ratio;
        }

        return data;
    }
}

export {
    InjectorTaxCharges,
};