/**
* This component provides a tax forecast (as in, what's coming up)
* from the provided start month/year for the specified time
* e.g. see 36 months worth of tax starting from 4/19
* you need to "inject" income (Salary or Benefits), in the order
* you want the tax to be worked out
* (usually Salary, Other Incomes, Vehicle and Fuel benefit)
* The injectors are provided as an array of structs, and a corresponding
* injector class needs to exist (they need to extend injector.cfc)
* e.g. injectorSalary is for calculating tax on salary
*
* To use create an instance of this forecast class and run forecast.calculateTaxes()
* @class
*/
import { dateDifference } from '../../date/util';
import { createTaxYears, describeTaxYear } from './taxyears';
import { InjectorTaxCharges } from './injectortaxcharges';
import { InjectorSalary } from './injectorsalary';
import { InjectorCompanyVehicle } from './injectorcompanyvehicle';
import { InjectorTaxes } from './injectortaxes';
class Forecast {
/**
* @param {struct} skv_config To include start_month, start_year, period_months, arr_injectors, region_code
* @param {array} arr_tax_charges Tax bands and rates across the tax years (income tax and nic)
* @param {array} arr_tax_company_vehicle Tax bands and rates across the tax years (vehicle ben, fuel ben)
*/
constructor(
skv_config,
arr_tax_charges,
arr_tax_company_vehicle = [],
) {
const required_keys = ['start_month', 'start_year', 'period_months', 'arr_injectors', 'region_code'];
const cnt_keys = required_keys.length;
for (let i = 0; i < cnt_keys; i += 1) {
const str_key = required_keys[i];
// throw error if the key isn't present in skv_config
if (!Object.prototype.hasOwnProperty.call(skv_config, str_key)) {
throw new Error(`Missing key required for tax forecast: ${str_key}`);
}
}
// start_month The month the period starts from
this.start_month = skv_config.start_month;
// start_year_short The year the period starts from in xx format. Will be worked out later
this.start_year_short = 0;
// start_year_short The year the period starts from in xxxx format. Will be worked out later
this.start_year_full = 0;
// period_months The period of time over which we must calculate
this.period_months = skv_config.period_months;
// arr_injectors Array of TaxYears that this forecast period spans
this.arr_injectors = skv_config.arr_injectors;
// where each tax year will eventually be included
this.arr_tax_years = [];
// work out start_year_short and start_year_long i.e xx and 20xx respectively
if (skv_config.start_year < 2000) {
this.start_year_short = skv_config.start_year;
this.start_year_full = 2000 + skv_config.start_year;
} else {
this.start_year_full = skv_config.start_year;
this.start_year_short = skv_config.start_year - 2000;
}
// build the years that the forecast spans
this.arr_tax_years = Forecast.buildYears(
this.start_month,
this.start_year_full,
this.period_months,
);
// apply the tax charges injector to get the rates and bands for that tax year
const obj_injector_tax_charges = new InjectorTaxCharges(
skv_config,
this.arr_tax_years,
arr_tax_charges,
arr_tax_company_vehicle,
);
// update tax years with tax charges added
this.arr_tax_years = obj_injector_tax_charges.taxYears;
// bring the various injectors together
this.arr_tax_years = Forecast.applyInjectors(this.arr_injectors, this.arr_tax_years);
// ensure we have skv_config available for calculateTaxes()
this.skv_config = skv_config;
// expose functions
this.buildYears = Forecast.buildYears;
}
/**
* Calculate all the taxes based on injectors setup previously
* @returns {array} calculated tax years
*/
calculateTaxes() {
// Call the taxes on renumeration injector, pretty manditory for everything so not dependent on the config
const obj_injector_taxes = new InjectorTaxes(
this.skv_config,
this.arr_tax_years,
);
return obj_injector_taxes.calculate(this.arr_tax_years);
}
/**
* Bring the various injectors together
* @param {array} arr_injectors
* @param {array} arr_tax_years
* @returns {array} Calculated array of tax years
*/
static applyInjectors(arr_injectors, arr_tax_years) {
const cnt_injectors = arr_injectors.length;
let arr_return = [];
// walk over the arr_injectors array calling the injectors
for (let i = 0; i < cnt_injectors; i += 1) {
const curr_injector = arr_injectors[i];
switch (curr_injector.type) {
case 'salary': {
const obj_injector_salary = new InjectorSalary(curr_injector.args, arr_tax_years);
arr_return = obj_injector_salary.taxYears;
break;
}
case 'company_vehicle': {
const obj_injector_company_vehicle = new InjectorCompanyVehicle(curr_injector.args, arr_tax_years);
arr_return = obj_injector_company_vehicle.taxYears;
break;
}
default: {
throw new Error(`Injector type of ${curr_injector.type} is not recognised`);
}
}
}
return arr_return;
}
/**
* Create the array of years that span the forecast
* @param {number} start_month Month number e.g 5 for May
* @param {number} start_year_full Year number e.g 2021
* @param {number} period_months Number of months the forecast will cover
* @returns {array} of years
*/
static buildYears(
start_month,
start_year_full,
period_months,
) {
// to be returned once years constructed
const arr_tax_years = [];
// work out the difference from the start/end of the tax year
const obj_start_date = new Date(start_year_full, start_month);
let obj_tmp_date_end = new Date(start_year_full, 4);
let months_diff = dateDifference('months', obj_start_date, obj_tmp_date_end);
// Calculate the number of taxyears that the period spans
// Ceiling this will round up the answer, so if it takes
// 1.2 years, we need to get 2 tax years
let cnt_years = Math.ceil(period_months / 12);
// If the period does not start right at the beginning of a tax year
// (april) then we'll need to check if the period specified goes into
// another tax year (period > diff to end of year)
// if it does, we need to add an extra tax year.
// this is because we'll only be using part of the first year,
// we'll need part of an extra year at the end
// e.g 1/4 of year 1, 2 full years, 3/4 of year 4 adds up to 4 years
if (
start_month !== 4
&& period_months > months_diff
) {
cnt_years += 1;
}
// if the start date is after april, we need to use the
// tax year, which is start_year + 1
let start_tax_year = start_year_full.toString();
// work out the short tax year version
start_tax_year = Number(start_tax_year.substring(2, 4));
if (start_month >= 4) {
start_tax_year += 1;
}
// make a basic tax year array to suit out start date and period
// the start date needs to be XX format, e.g. 19 not 2019
// will return with just date information
const arr_basic_years = createTaxYears(
start_tax_year,
cnt_years,
);
// for each tax year, make a new struct with start/end dates,
// actual months occupied (might not be the full 12 months)
// and create basic income array (we always need in come in the forecast)
for (let i = 0; i < cnt_years; i += 1) {
const skv_year = arr_basic_years[i];
// get the month/year date of the current year in the loop
skv_year.date_end = `04/20${skv_year.tax_year}`;
obj_tmp_date_end = new Date(`20${skv_year.tax_year}`, 4);
// work out how far the end of the current year in the loop
// is from the start date of the forecast
months_diff = dateDifference('months', obj_start_date, obj_tmp_date_end);
if (months_diff < 12) {
// if less than a year has passed,
// from the start of the forecase to the end of the current
// tax year then we just store that date difference
skv_year.months_occupied = Math.min(months_diff, period_months);
} else if (i === cnt_years - 1) {
// if we're in the final year, then we need the remaining
// months, which is the difference between the total months
// and the sum of those already occupied
let cnt_months_occupied = 0;
arr_basic_years.forEach((year) => {
if (Object.prototype.hasOwnProperty.call(year, 'months_occupied')) {
cnt_months_occupied += year.months_occupied;
}
});
skv_year.months_occupied = period_months - cnt_months_occupied;
} else {
// anything else will be a full year
skv_year.months_occupied = 12;
}
// build a description for the year
skv_year.description = describeTaxYear(
skv_year.months_occupied,
skv_year.year_slash,
);
arr_tax_years.push(skv_year);
}
return arr_tax_years;
}
get taxYears() {
return this.arr_tax_years;
}
}
export {
Forecast,
};