tool/forecast/injectortaxes.js

import { Injector } from './injector';

/**
 * Creates a taxes injector that all injectors require in order to calculate
 * @class
 */
class InjectorTaxes extends Injector {
    /**
     * @param {struct} skv_config Struct passed in when initializing the injector
     * @param {array} arr_tax_years Array of tax years
     */
    constructor(skv_config = {}, arr_tax_years = []) {
        super();
        this.valid_keys = [];

        if (!this.validate(this.valid_keys)) {
            throw new Error('Invalid key passed in to InjectorTaxes');
        }

        this.skv_config = skv_config;
        this.arr_tax_years = arr_tax_years;

        // expose functions
        this.calculate = InjectorTaxes.calculate;
        this.calcEmployerNICSingleYear = InjectorTaxes.calcEmployerNICSingleYear;
        this.calcEmployeeNICSingleYear = InjectorTaxes.calcEmployeeNICSingleYear;
        this.calculateIncomeTaxTaxable = InjectorTaxes.calculateIncomeTaxTaxable;
        this.calcExtraTaxFromLostPA = InjectorTaxes.calcExtraTaxFromLostPA;
        this.calculateIncomeTaxRelief = InjectorTaxes.calculateIncomeTaxRelief;
        this.totalIncome = InjectorTaxes.totalIncome;
    }

    /**
     * Calculate the taxes for each year including totals
     * @param {array} arr_tax_years
     * @returns {array} tax years with calculated taxes and totals
     */
    static calculate(arr_tax_years) {
        const arr_return = [];
        const cnt_tax_years = arr_tax_years.length;

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

            // regions will be filtered down to 1 already at this point
            const arr_income_tax_bands = skv_tax_year.skv_tax_charges.income_tax.regions[0].bands;

            // Work out how much extra tax is paid due to lost personal allowance,
            // split by the sources of income (including benefits as income)
            const skv_extra_tax_pa = InjectorTaxes.calcExtraTaxFromLostPA(
                skv_tax_year.arr_income,
                arr_income_tax_bands,
            );

            skv_tax_year.arr_tax_on_lost_pa = skv_extra_tax_pa.arr_return;

            // calculate the taxable income and relief income
            skv_tax_year.arr_income_tax = InjectorTaxes.calcIncomeTax(
                skv_tax_year.arr_income,
                arr_income_tax_bands,
            );

            skv_tax_year.arr_employer_nic = InjectorTaxes.calcEmployerNICSingleYear(
                skv_tax_year.arr_income,
                skv_tax_year.skv_tax_charges.national_insurance.employer.class1A,
            );

            skv_tax_year.arr_employee_nic = InjectorTaxes.calcEmployeeNICSingleYear(
                skv_tax_year.arr_income,
                skv_tax_year.skv_tax_charges.national_insurance.employee.bands,
            );

            skv_tax_year.totals = InjectorTaxes.calculateTotals(skv_tax_year);
            skv_tax_year.totals.tax.on_lost_pa_reduction = skv_extra_tax_pa.totals.tax_on_lost_pa_reduction;
            skv_tax_year.totals.pa_reduction = skv_extra_tax_pa.totals.lost_pa_reduction;

            // add to the array we'll return
            arr_return.push(skv_tax_year);
        }

        return arr_return;
    }

    /**
     * Do the final totalling
     * @param {struct} skv_tax_year
     * @returns {struct} Totals of income and tax
     */
    static calculateTotals(skv_tax_year) {
        const skv_return = {
            income: {},
            tax: {},
            pa_reduction: 0,
        };

        // incomes are always for the whole year
        skv_return.income.gross_value = this.totalIncome(skv_tax_year.arr_income);

        /*
        * TOTALS - INCOME
        */

        // employer provided non benefits
        skv_return.income.employer_provided_non_benefits = this.totalIncome(
            skv_tax_year.arr_income,
            'only_employer_provided_non_benefits',
        );

        /*
        * TOTALS - P11D
        */
        const p11d = this.totalIncome(
            skv_tax_year.arr_income,
            'p11d',
        );

        skv_return.income.p11d = p11d;

        /*
        * TOTALS - EMPLOYEE NIC
        */
        skv_return.tax.employee_nic = this.totalEmployeeNIC(
            skv_tax_year.arr_employee_nic,
        );

        /*
        * TOTALS - EMPLOYER NIC with benefits
        */
        skv_return.tax.employer_nic_benefits = this.totalEmployerNIC(
            skv_tax_year.arr_employer_nic,
            'benefits_only',
        );

        /*
        * TOTALS - EMPLOYER NIC without benefits
        */
        skv_return.tax.employer_nic_exc_benefits = this.totalEmployerNIC(
            skv_tax_year.arr_employer_nic,
            'no_benefits',
        );

        /*
        * TOTALS - INCOME TAX
        */
        skv_return.tax.income_tax_on_employer_provided = this.totalIncomeTaxes(
            skv_tax_year.arr_income_tax,
            'only_employer_provided',
        );

        /*
        * TOTALS - INCOME TAX
        */
        skv_return.tax.income_tax_on_employer_provided_benefits = this.totalIncomeTaxes(
            skv_tax_year.arr_income_tax,
            'only_employer_provided_benefits',
        );

        /*
        * TOTALS - INCOME TAX RELIEF
        */
        let income_tax_relief = this.totalTaxRelief(
            skv_tax_year.arr_income_tax,
        );

        // we want to be removing the relief so change to a negative for clarity
        if (income_tax_relief > 0) {
            income_tax_relief *= -1;
        }

        skv_return.tax.relief = income_tax_relief;

        /*
        * TOTALS - NET PAY
        */
        const net_pay = skv_return.income.employer_provided_non_benefits
                            - skv_return.tax.income_tax_on_employer_provided
                            - skv_return.tax.employee_nic
                            // relief is a negative number so add it
                            + skv_return.tax.relief;

        // format the value and put in to totals from the year
        skv_return.income.net_pay = net_pay;

        return skv_return;
    }

    /**
     * Loops through the income tax looking for any taxed relief
     * @param {array} arr_income_tax
     * @returns {number} num_total
     */
    static totalTaxRelief(
        arr_income_tax,
    ) {
        const cnt_income_tax = arr_income_tax.length;
        let num_total = 0;

        for (let i = 0; i < cnt_income_tax; i += 1) {
            const skv_income_tax = arr_income_tax[i];

            if (skv_income_tax.is_relief) {
                num_total += skv_income_tax.tax_relief_total;
            }
        }

        return num_total;
    }

    /**
     * Loops through the employee NIC array
     * @param {array} arr_employee_nic
     * @returns {number} num_total
     */
    static totalEmployeeNIC(
        arr_employee_nic,
    ) {
        const cnt_employee_nic = arr_employee_nic.length;
        let num_total = 0;

        for (let i = 0; i < cnt_employee_nic; i += 1) {
            const skv_employee_nic = arr_employee_nic[i];
            num_total += skv_employee_nic.tax_total;
        }

        return num_total;
    }

    /**
     * Loops through the employer NIC array
     * @param {array} arr_employer_nic
     * @returns {number} num_total
     */
    static totalEmployerNIC(
        arr_employer_nic,
        str_mode = '',
    ) {
        const cnt_employee_nic = arr_employer_nic.length;
        let num_total = 0;

        for (let i = 0; i < cnt_employee_nic; i += 1) {
            const skv_employee_nic = arr_employer_nic[i];

            if (str_mode === '') {
                num_total += skv_employee_nic.tax_total;
            } else if (
                str_mode === 'benefits_only'
                && skv_employee_nic.is_benefit
            ) {
                num_total += skv_employee_nic.tax_total;
            } else if (
                str_mode === 'no_benefits'
                && !skv_employee_nic.is_benefit
            ) {
                num_total += skv_employee_nic.tax_total;
            }
        }

        return num_total;
    }

    /**
     * Loops through the array totalling up the valuePounds
     * @param {array} arr_income_tax Array of structures each containing an element with key: tax_total
     * @param {string} str_mode what elements are we counting
     * @returns {data_type} Description
     */
    static totalIncomeTaxes(
        arr_income_tax,
        str_mode = '',
    ) {
        const cnt_income_tax = arr_income_tax.length;
        let num_total = 0;

        for (let i = 0; i < cnt_income_tax; i += 1) {
            const skv_income_tax = arr_income_tax[i];

            if (str_mode === '') {
                num_total += skv_income_tax.tax_total;
            } else if (
                str_mode === 'only_employer_provided'
                && skv_income_tax.is_provided_by_employer
            ) {
                num_total += skv_income_tax.tax_total;
            } else if (
                str_mode === 'only_employer_provided_benefits'
                && skv_income_tax.is_provided_by_employer
                && skv_income_tax.is_benefit
            ) {
                num_total += skv_income_tax.tax_total;
            }
        }

        return num_total;
    }

    /**
     * Loops through the income array totalling up the value
     * @param {array} arr_income Array of income structures each containing an element with key 'value'
     * @param {string} str_mode what elements are we counting
     * @returns {number}
     */
    static totalIncome(arr_income, str_mode = '') {
        const cnt_income = arr_income.length;
        let num_total = 0;

        for (let i = 0; i < cnt_income; i += 1) {
            // this particular type of income e.g salary, company car benefit etc
            const skv_income = arr_income[i];

            if (str_mode === '') {
                num_total += skv_income.value;
            } else if (
                str_mode === 'only_employer_provided_non_benefits'
                && skv_income.is_provided_by_employer
                && !skv_income.is_benefit
            ) {
                num_total += skv_income.value;
            } else if (
                str_mode === 'P11D'
                && skv_income.is_provided_by_employer
                && skv_income.is_benefit
            ) {
                num_total += skv_income.value;
            }
        }

        return num_total;
    }

    /**
        * Work out how much extra tax is paid due to lost personal allowance,
        * split by the sources of income (including benefits as income)
        * @param {array} arr_income
        * @param {array} arr_tax_bands
        * @returns {struct}
    */
    static calcExtraTaxFromLostPA(
        arr_income,
        arr_tax_bands,
    ) {
        const skv_return = {
            totals: {
                tax_on_lost_pa_reduction: 0,
                lost_pa_reduction: 0,
            },
            arr_return: [],
        };

        // the first band is the allowance
        const skv_pa = arr_tax_bands[0];

        // variables for original, remaining and lost pa
        const original_pa = skv_pa.limit;
        let remaining_pa = original_pa;

        // get the reduction threshold and rate (the first tax band is PA)
        const pa_restrict = skv_pa.restriction_threshold;
        const pa_rate = skv_pa.restriction_rate;

        // we need to get the highest tax bracket before the PA is lost,
        // as the other bands will shift down, it's effectively the highest
        // tax bracket that the lost allowance gets charged at
        let lost_pa_tax_perc = 0;
        const cnt_bands = arr_tax_bands.length;

        for (let i = 0; i < cnt_bands; i += 1) {
            const skv_band = arr_tax_bands[i];
            lost_pa_tax_perc = skv_band.percentage;

            if (
                skv_band.limit === ''
                || skv_band.limit >= pa_restrict
            ) {
                break;
            }
        }

        // convert the lost_pa_tax_perc to a decimal
        lost_pa_tax_perc /= 100;

        // total income so far
        let total_income = 0;

        // total over limit and max over limit
        let total_over_limit = 0;

        const cnt_income = arr_income.length;

        // Loop through each of the items in the arr_income array
        for (let i = 0; i < cnt_income; i += 1) {
            const skv_income_source = arr_income[i];

            const num_income = skv_income_source.value;

            total_income += num_income;

            // amount the PA is to be reduced by
            let reduction = 0;

            // amount of extra tax due to lost pa
            let extra_tax = 0;

            // check if the total income has taken us over the
            // allowance restriction, and if we have any allowance
            // left to reduce
            if (
                total_income > pa_restrict
                && remaining_pa > 0
            ) {
                // how much are we over
                const over_limit = total_income
                                    - pa_restrict
                                    - total_over_limit;

                total_over_limit += over_limit;

                // amount to reduce by, limited to remaining PA
                reduction = Math.min(
                    over_limit * pa_rate,
                    remaining_pa,
                );

                extra_tax = reduction * lost_pa_tax_perc;

                remaining_pa -= reduction;
            }

            const skv_temp = {
                description: `Income Tax on lost allowance due to ${skv_income_source.name}`,
                name: skv_income_source.name,
                // the tax is always at the basic rate
                pa_reduction: reduction,
                tax_total: extra_tax,
            };

            skv_return.arr_return.push(skv_temp);

            skv_return.totals.lost_pa_reduction += skv_temp.pa_reduction;
            skv_return.totals.tax_on_lost_pa_reduction += skv_temp.tax_total;
        }

        return skv_return;
    }

    /**
     * Creates the structure for an income tax struct
     * @param {struct} skv_income The structure representing a single year
     * @returns {struct}
     */
    static getTaxStartingStruct(type, skv_income) {
        const skv_return = {};

        skv_return.arr_tax_bands = [];

        let str_name = '';

        if (type === 'income_tax') {
            str_name = 'Income Tax';
        } else if (type === 'employee_nic') {
            str_name = 'NIC';
        }

        skv_return.name = `${str_name} on ${skv_income.name}`;
        skv_return.short_name = skv_income.name;
        skv_return.gross_value = skv_income.value;

        if (type === 'income_tax') {
            skv_return.is_provided_by_employer = skv_income.is_provided_by_employer;
            skv_return.is_taxed_at_source = skv_income.is_taxed_at_source;
            skv_return.is_benefit = skv_income.is_benefit;
            skv_return.is_relief = skv_income.is_relief;
        }

        return skv_return;
    }

    /**
     * build a struct with the calculated income tax
     * @param {array} skv_band basic tax band
     * @param {number} amount_to_tax
     * @param {number} tax_applied
     * @returns {struct} tax band with calculated tax
     */
    static addCalculatedTaxBand(
        skv_band,
        amount_to_tax,
        tax_applied,
    ) {
        return {
            band: skv_band,
            tax: tax_applied,
            tax_relief: 0,
            income: amount_to_tax,
        };
    }

    /**
     * Work out how much of the tax is in each band
     * @param {array} arr_tax_bands
     * @param {number} tax_total all the tax across the tax bands added up
     * @returns {array} tax bands with % proportions included
     */
    static addProportionsToBands(arr_tax_bands, tax_total) {
        const arr_return = [];
        const cnt_bands = arr_tax_bands.length;

        for (let i = 0; i < cnt_bands; i += 1) {
            const skv_band = arr_tax_bands[i];
            let perc_proportion = 0;

            if (skv_band.tax !== 0) {
                perc_proportion = (skv_band.tax / tax_total) * 100;
            }

            skv_band.percentage_of_total = perc_proportion;
            arr_return.push(skv_band);
        }

        return arr_return;
    }

    /**
     * Get the highest tax band with tax due to be paid
     * @param {array} arr_tax_bands Description
     * @returns {number} index of highest tax band
     */
    static getTopBandWithTaxIndex(arr_tax_bands) {
        const cnt_tax_bands = arr_tax_bands.length;
        let num_highest_idx = 0;

        for (let i = 0; i < cnt_tax_bands; i += 1) {
            const curr_tax_band = arr_tax_bands[i];

            if (curr_tax_band.tax > 0) {
                num_highest_idx = i;
            }
        }

        return num_highest_idx;
    }

    /**
     * Filter income by whether its income taxable
     * Note: Relief such as Mileage Allowance Relief (MAR) is
     * @param {array} arr_income
     * @returns {array}
     */
    static getTaxableIncomes(arr_income) {
        return arr_income.filter((skv_curr_income) => {
            let should_include_income = true;

            // if there is an income taxable property use its value
            if (
                Object.prototype.hasOwnProperty.call(skv_curr_income, 'is_income_taxable')
            ) {
                should_include_income = skv_curr_income.is_income_taxable;
            }

            return should_include_income;
        });
    }

    /**
     * Filter income by whether its consider as relief
     * @param {array} arr_income
     * @returns {array}
     */
    static getReliefIncomes(arr_income) {
        return arr_income.filter((skv_curr_income) => {
            let should_include_income = false;

            // if there is an income taxable property use its value
            if (Object.prototype.hasOwnProperty.call(skv_curr_income, 'is_relief')) {
                should_include_income = skv_curr_income.is_relief;
            }

            return should_include_income;
        });
    }

    /**
     * Filter income by whether its provided by employer
     * @param {array} arr_income
     * @returns {array}
     */
    static getEmployerProvidedIncomes(arr_income) {
        return arr_income.filter((skv_curr_income) => skv_curr_income.is_provided_by_employer);
    }

    /**
     * Filter income by whether national insurance contributions are due on it
     * @param {array} arr_income
     * @returns {array}
     */
    static getEmployeeNICTaxableIncomes(arr_income) {
        return arr_income.filter((skv_curr_income) => skv_curr_income.is_employee_nic_taxable);
    }

    /**
     * Creates and populates the arr_income_tax array for a single year
     * @param {array} arr_income
     * @param {array} arr_tax_bands
     * @returns {struct}
     */
    static calcIncomeTax(
        arr_income,
        arr_tax_bands,
    ) {
        let arr_income_tax_return = [];

        // calculated income tax
        const arr_income_tax_taxable = InjectorTaxes.calculateIncomeTaxTaxable(
            arr_income,
            arr_tax_bands,
        );

        // combine arrays
        arr_income_tax_return = arr_income_tax_return.concat(arr_income_tax_taxable);

        // work out any tax relief
        const arr_income_tax_relief = InjectorTaxes.calculateIncomeTaxRelief(
            arr_income,
            // pass in the tax calculated salary tax bands
            arr_income_tax_return[0].arr_tax_bands,
        );

        // combine arrays
        arr_income_tax_return = arr_income_tax_return.concat(arr_income_tax_relief);

        return arr_income_tax_return;
    }

    /**
     * calculate income tax that is taxable
     * @param {array} arr_income
     * @param {array} arr_tax_bands
     * @returns {array} array of income tax
     */
    static calculateIncomeTaxTaxable(
        arr_income,
        arr_tax_bands,
    ) {
        const arr_income_tax_return = [];

        // the first band is the allowance
        const skv_pa = arr_tax_bands[0];

        const cnt_bands = arr_tax_bands.length;

        // variables for original and remaining
        const original_pa = skv_pa.limit;
        let remaining_pa = original_pa;

        // ratio for months in taxed year

        // get the amount of money earnt this year
        // this will be the full salary plus the vehicle/fuel benefits
        // reduced by the coverage ratio
        const gross_annual_income = InjectorTaxes.totalIncome(arr_income);

        // If the total of all remuneration is greater than the
        // Personal Allowance Restriction (£100,000 at time of writing)
        // then reduce the personal allowance by the amount over the threshold,
        // multiplied by the restriciton rate (0.5 at the time of writing)
        // so originally for every £2 over the limit you lose £1 of PA
        if (
            gross_annual_income > skv_pa.restriction_threshold
        ) {
            const over_limit = (
                gross_annual_income - skv_pa.restriction_threshold
            ) * skv_pa.restriction_rate;

            // remove amount over limit, but max sure it doesn't go negative
            remaining_pa = Math.max(remaining_pa - over_limit, 0);
        }

        // // the reduction in personal allowance
        const pa_diff = original_pa - remaining_pa;

        // If this income is NOT incomeTaxable, don't include in taxable income
        // Relief such as Mileage Allowance Relief (MAR) is
        const arr_taxable_incomes = InjectorTaxes.getTaxableIncomes(arr_income);
        const cnt_income = arr_taxable_incomes.length;

        // total of income and the amount that has been
        // taxed so far
        let total_income = 0;
        let total_income_taxed = 0;

        // Loop through each of the items in the taxable income array
        for (let i = 0; i < cnt_income; i += 1) {
            const skv_curr_income = arr_taxable_incomes[i];
            const skv_new_income_tax = InjectorTaxes.getTaxStartingStruct('income_tax', skv_curr_income);

            skv_new_income_tax.tax_total = 0;

            if (Object.prototype.hasOwnProperty.call(skv_curr_income, 'is_allowance_sacrifice_taxed')) {
                skv_new_income_tax.is_allowance_sacrifice_taxed = skv_curr_income.is_allowance_sacrifice_taxed;
                skv_new_income_tax.ulev_salsac_co2_threshold = skv_curr_income.ulev_salsac_co2_threshold;
            }

            const income_amount = skv_curr_income.value;
            total_income += income_amount;

            let income_left_to_tax = income_amount;

            // we need to know the limit at the end of the previous band,
            // which at the beginning/bottom bracket, is nothing.
            let prev_limit = 0;
            let tax_applied = 0;
            // loop over the tax bands and work out tax
            // note that the brackets may shift down as the personal allowance
            // is lost (bottom bracket)
            // the top backet does not shift, nor does it have a limit
            // so we will process this outside of the loop at the end
            for (let j = 0; j < cnt_bands - 1; j += 1) {
                const band = arr_tax_bands[j];
                tax_applied = 0;

                // if we're not in the penultimate band, we need to reduce the
                // band limit by the lost_pa (which is capped at total pa)
                const band_shift = j !== (cnt_bands - 2) ? pa_diff : 0;

                const shifted_limit = band.limit - band_shift;

                let amount_to_tax = 0;

                if (
                    total_income_taxed < shifted_limit
                    && total_income_taxed < total_income
                ) {
                    // work out the maximum that can be taxed
                    const max_taxable_amount = shifted_limit
                                                - Math.max(
                                                    prev_limit,
                                                    total_income_taxed,
                                                );

                    // we tax the smaller of:
                    // the remainder of the salary
                    // or the space left in the bracket
                    amount_to_tax = Math.min(
                        income_left_to_tax,
                        max_taxable_amount,
                    );

                    // reduce the income left to tax
                    income_left_to_tax -= amount_to_tax;
                    // increase the total_income_taxed
                    total_income_taxed += amount_to_tax;
                    tax_applied = amount_to_tax * (band.percentage / 100);

                    skv_new_income_tax.tax_total += tax_applied;
                }

                // store this limit
                // we need to store it even if we didn't tax it
                // so we can skip up to the next bracket
                prev_limit = shifted_limit;

                skv_new_income_tax.arr_tax_bands.push(InjectorTaxes.addCalculatedTaxBand(
                    band,
                    amount_to_tax,
                    tax_applied,
                ));
            } // END: looping bands

            // if there is anything left to tax, it goes on the top rate
            const top_band = arr_tax_bands[cnt_bands - 1];
            // var top_label = capitaliseWord( top_band.label );
            let top_tax_applied = 0;

            // work out the amount to tax
            if (income_left_to_tax > 0) {
                top_tax_applied = income_left_to_tax * (top_band.percentage / 100);
            }

            skv_new_income_tax.arr_tax_bands.push(InjectorTaxes.addCalculatedTaxBand(
                top_band,
                income_left_to_tax,
                top_tax_applied,
            ));

            // add top tax to the total
            skv_new_income_tax.tax_total += top_tax_applied;

            // in order to work out average % rates loop back over the bands and
            // work out the proportion of tax per band in relation to the total tax for the period
            skv_new_income_tax.arr_tax_bands = InjectorTaxes.addProportionsToBands(skv_new_income_tax.arr_tax_bands, skv_new_income_tax.tax_total);

            // tax_total is proportional to the year so the gross_value should be adjusted too when creating the net
            const net_value = (skv_new_income_tax.gross_value) - skv_new_income_tax.tax_total;
            skv_new_income_tax.net_value = net_value;

            arr_income_tax_return.push(skv_new_income_tax);
        } // END: looping incomes for tax

        return arr_income_tax_return;
    }

    /**
     * calculate income tax that is classed as relief
     * @param {array} arr_income
     * @param {array} arr_calculated_salary_tax_bands We need tax that has already
     * been calculated on income tax to ensure the relief is deducted from the
     * highest band with tax in it
     * @returns {array} array of income tax
     */
    static calculateIncomeTaxRelief(
        arr_income,
        arr_calculated_salary_tax_bands,
    ) {
        const arr_income_tax_return = [];

        // get only sources of income that is considered relief
        const arr_relief_incomes = InjectorTaxes.getReliefIncomes(arr_income);
        const cnt_relief_income = arr_relief_incomes.length;

        // include any tax relief
        // Loop through each of the items in the arr_income array
        for (let i = 0; i < cnt_relief_income; i += 1) {
            const skv_curr_income = arr_relief_incomes[i];

            const skv_income_tax_relief = InjectorTaxes.getTaxStartingStruct('income_tax', skv_curr_income);
            // skv_income_tax_relief['tax_total'] = 0;
            skv_income_tax_relief.tax_relief_total = 0;

            // find the last band being taxed upon
            const top_tax_band_idx = InjectorTaxes.getTopBandWithTaxIndex(arr_calculated_salary_tax_bands);
            const cnt_bands = arr_calculated_salary_tax_bands.length;

            for (let j = 0; j < cnt_bands; j += 1) {
                // passed in to the function as an argument
                const { band } = arr_calculated_salary_tax_bands[j];
                let gross_amount = 0;

                // we're not worried about tax going across bands or tax being
                // pushed in to other bands
                if (j === top_tax_band_idx) {
                    gross_amount = skv_curr_income.value;
                }

                skv_income_tax_relief.arr_tax_bands.push({
                    band,
                    tax: 0,
                    tax_relief: gross_amount * (band.percentage / 100),
                    income: gross_amount,
                    percentage_of_total: (gross_amount < 0) ? 100 : 0,
                });

                // skv_income_tax_relief['tax_total'] -= skv_income_tax_relief.arr_tax_bands[j].tax_relief;
                skv_income_tax_relief.tax_relief_total += skv_income_tax_relief.arr_tax_bands[j].tax_relief;
            } // END: looping bands

            arr_income_tax_return.push(skv_income_tax_relief);
        } // END: looping incomes for relief

        return arr_income_tax_return;
    }

    /**
     * Creates and populates an array with EMPLOYER class1a national insurance contributions
     * for a single year
     * @param {array} arr_income
     * @param {number} class_1A_rate
     * @returns {array} array of employer national insurance contributions
     */
    static calcEmployerNICSingleYear(
        arr_income,
        class_1A_rate,
    ) {
        const arr_incomes = InjectorTaxes.getEmployerProvidedIncomes(arr_income);
        const cnt_income = arr_incomes.length;
        const arr_employer_nic_return = [];

        // Loop through each of the items in the arrClass1ANICTaxable array
        for (let i = 0; i < cnt_income; i += 1) {
            const skv_income = arr_income[i];
            const skv_new_nic = {};

            let str_A = '';

            if (skv_income.is_benefit) {
                str_A = 'A';
            }

            skv_new_nic.name = `Class 1${str_A} NIC on ${skv_income.name}`;
            skv_new_nic.gross_value = skv_income.value;
            skv_new_nic.is_benefit = skv_income.is_benefit;

            // Now calculate the actual tax
            const tax_total = skv_new_nic.gross_value * (class_1A_rate / 100);

            skv_new_nic.tax_total = tax_total;

            arr_employer_nic_return.push(skv_new_nic);
        }

        return arr_employer_nic_return;
    }

    /**
     * Creates and populates an array with EMPLOYEE national insurance contributions
     * for a single year
     * @param {array} arr_income
     * @param {number} employee_nic_bands
     * @returns {array} array of employer national insurance contributions
    */
    static calcEmployeeNICSingleYear(
        arr_income,
        employee_nic_bands,
    ) {
        const arr_incomes = InjectorTaxes.getEmployeeNICTaxableIncomes(arr_income);
        const cnt_income = arr_incomes.length;
        const arr_employee_nic_return = [];

        // As we loop through the taxable items this value will store what has been taxed so far
        let tax_total = 0;

        // Loop through each of the items in the arrIncome array
        for (let i = 0; i < cnt_income; i += 1) {
            const skv_income = arr_incomes[i];

            // build the base structure for nic struct
            const skv_new_nic = InjectorTaxes.getTaxStartingStruct('employee_nic', skv_income);

            const cnt_bands = employee_nic_bands.length;
            // where we're going to put the updated bands with tax calculated
            const arr_new_bands = [];

            let running_total_income = skv_new_nic.gross_value;
            let prev_limit = 0;

            // loop through the bands except the top band which doesn't have a limit
            // and anything else will be stuck in there
            for (let j = 0; j < cnt_bands - 1; j += 1) {
                const skv_band = employee_nic_bands[j];

                // if we've got income that falls with the bounds of this band's limit - the previous limit
                if (running_total_income > (skv_band.limit - prev_limit)) {
                    // we put the income up to the limit of the band less the
                    // income we've already accounted for
                    skv_band.income = skv_band.limit - prev_limit;
                    // and take it off the running total income for the next band
                    running_total_income -= skv_band.income;
                } else {
                    // if the income is below the limit it means this is the
                    // the last band to add income to to be taxed
                    skv_band.income = running_total_income;
                    running_total_income = 0;
                }

                prev_limit = skv_band.limit;
                skv_band.tax = skv_band.income * (skv_band.percentage / 100);
                tax_total += skv_band.tax;

                arr_new_bands.push(skv_band);
            }

            // get the penultimate band
            const skv_prev_band = arr_new_bands[cnt_bands - 2];
            prev_limit = skv_prev_band.limit;
            // work out what income is left to tax
            const amount_left_to_tax = skv_income.value - prev_limit;
            const skv_band = employee_nic_bands[cnt_bands - 1];

            // default to nothing
            skv_band.income = 0;
            skv_band.tax = 0;

            // anything left to tax?
            if (amount_left_to_tax > 0) {
                skv_band.income = amount_left_to_tax - skv_band.limit;
                skv_band.tax = skv_band.income * (skv_band.percentage / 100);
            }

            tax_total += skv_band.tax;
            arr_new_bands.push(skv_band);

            // include the calculated tax bands
            skv_new_nic.arr_tax_bands = arr_new_bands;
            skv_new_nic.tax_total = tax_total;
            skv_new_nic.net_value = skv_new_nic.gross_value - tax_total;

            arr_employee_nic_return.push(skv_new_nic);
        }

        return arr_employee_nic_return;
    }
}

export {
    InjectorTaxes,
};