Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Maintenence] Refactor Goals Schedule file #2102

Merged
merged 16 commits into from
Jan 7, 2024
295 changes: 160 additions & 135 deletions packages/loot-core/src/server/budget/goals/goalsSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,82 +8,71 @@ import {
} from '../../schedules/app';
import { isReflectBudget } from '../actions';

export async function goalsSchedule(
scheduleFlag,
template_lines,
current_month,
balance,
remainder,
last_month_balance,
to_budget,
errors,
category,
) {
if (!scheduleFlag) {
scheduleFlag = true;
const template = template_lines.filter(t => t.type === 'schedule');
//in the case of multiple templates per category, schedules may have wrong priority level
let t = [];
let totalScheduledGoal = 0;
async function createScheduleList(template, current_month, category) {
const t = [];
const errors = [];

for (let ll = 0; ll < template.length; ll++) {
const { id: sid, completed: complete } = await db.first(
'SELECT * FROM schedules WHERE name = ?',
[template[ll].name],
);
const rule = await getRuleForSchedule(sid);
const conditions = rule.serialize().conditions;
const { date: dateConditions, amount: amountCondition } =
extractScheduleConds(conditions);
const sign = category.is_income ? 1 : -1;
const target =
amountCondition.op === 'isbetween'
? (sign *
Math.round(
amountCondition.value.num1 + amountCondition.value.num2,
)) /
2
: sign * amountCondition.value;
const next_date_string = getNextDate(
dateConditions,
monthUtils._parse(current_month),
);
const target_interval = dateConditions.value.interval
? dateConditions.value.interval
: 1;
const target_frequency = dateConditions.value.frequency;
const isRepeating =
Object(dateConditions.value) === dateConditions.value &&
'frequency' in dateConditions.value;
const num_months = monthUtils.differenceInCalendarMonths(
next_date_string,
current_month,
);
const startDate = dateConditions.value.start ?? dateConditions.value;
const started = startDate <= monthUtils.addMonths(current_month, 1);
for (let ll = 0; ll < template.length; ll++) {
const { id: sid, completed: complete } = await db.first(
'SELECT * FROM schedules WHERE name = ? AND tombstone = 0',
[template[ll].name],
);
const rule = await getRuleForSchedule(sid);
const conditions = rule.serialize().conditions;
const { date: dateConditions, amount: amountCondition } =
extractScheduleConds(conditions);
const sign = category.is_income ? 1 : -1;
const target =
amountCondition.op === 'isbetween'
? (sign *
Math.round(
amountCondition.value.num1 + amountCondition.value.num2,
)) /
2
: sign * amountCondition.value;
const next_date_string = getNextDate(
dateConditions,
monthUtils._parse(current_month),
);
const target_interval = dateConditions.value.interval
? dateConditions.value.interval
: 1;
const target_frequency = dateConditions.value.frequency;
const isRepeating =
Object(dateConditions.value) === dateConditions.value &&
'frequency' in dateConditions.value;
const num_months = monthUtils.differenceInCalendarMonths(
next_date_string,
current_month,
);
if (num_months < 0) {
//non-repeating schedules could be negative
errors.push(`Schedule ${template[ll].name} is in the Past.`);
} else {
t.push({
template: template[ll],
target,
next_date_string,
target_interval,
target_frequency,
num_months,
completed: complete,
started,
//started,
full: template[ll].full === null ? false : template[ll].full,
repeat: isRepeating,
name: template[ll].name,
});
if (!complete && started) {
if (!complete) {
if (isRepeating) {
let monthlyTarget = 0;
const nextMonth = monthUtils.addMonths(
current_month,
t[ll].num_months + 1,
t[t.length - 1].num_months + 1,
);
let nextBaseDate = getNextDate(
dateConditions,
monthUtils._parse(current_month),
true,
);

let nextDate = dateConditions.value.skipWeekend
? monthUtils.dayFromDate(
getDateWithSkippedWeekend(
Expand All @@ -92,7 +81,6 @@ export async function goalsSchedule(
),
)
: nextBaseDate;

while (nextDate < nextMonth) {
monthlyTarget += -target;
const currentDate = nextBaseDate;
Expand All @@ -119,92 +107,129 @@ export async function goalsSchedule(
break;
}
}
t[ll].target = -monthlyTarget;
totalScheduledGoal += target;
t[t.length - 1].target = -monthlyTarget;
}
} else {
errors.push(
`Schedule ${t[ll].template.name} is not active during the month in question.`,
`Schedule ${t[ll].name} is not active during the month in question.`,
);
}
}
}
return { t: t.filter(c => c.completed === 0), errors };
}

t = t.filter(t => t.completed === 0 && t.started);
t = t.sort((a, b) => b.target - a.target);
async function getPayMonthOfTotal(t) {
//return the contribution amounts of full or every month type schedules
let total = 0;
const schedules = t.filter(c => c.num_months === 0);
for (let ll = 0; ll < schedules.length; ll++) {
total += schedules[ll].target;
}
return total;
}

let increment = 0;
if (balance >= totalScheduledGoal) {
for (let ll = 0; ll < t.length; ll++) {
if (t[ll].num_months < 0) {
errors.push(
`Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`,
);
break;
}
if (
(t[ll].template.full && t[ll].num_months === 0) ||
t[ll].target_frequency === 'weekly' ||
t[ll].target_frequency === 'daily'
) {
increment += t[ll].target;
} else if (t[ll].template.full && t[ll].num_months > 0) {
increment += 0;
} else {
increment += t[ll].target / t[ll].target_interval;
}
}
} else if (balance < totalScheduledGoal) {
for (let ll = 0; ll < t.length; ll++) {
if (isReflectBudget()) {
if (!t[ll].template.full) {
errors.push(
`Report budgets require the full option for Schedules.`,
);
break;
}
if (t[ll].template.full && t[ll].num_months === 0) {
to_budget += t[ll].target;
}
}
if (!isReflectBudget()) {
if (t[ll].num_months < 0) {
errors.push(
`Non-repeating schedule ${t[ll].template.name} was due on ${t[ll].next_date_string}, which is in the past.`,
);
break;
}
if (t[ll].template.full && t[ll].num_months > 0) {
remainder = 0;
} else if (ll === 0 && !t[ll].template.full) {
remainder = t[ll].target - last_month_balance;
} else {
remainder = t[ll].target - remainder;
}
let tg = 0;
if (remainder >= 0) {
tg = remainder;
remainder = 0;
} else {
tg = 0;
remainder = Math.abs(remainder);
}
if (
t[ll].template.full ||
t[ll].num_months === 0 ||
t[ll].target_frequency === 'weekly' ||
t[ll].target_frequency === 'daily'
) {
increment += tg;
} else if (t[ll].template.full && t[ll].num_months > 0) {
increment += 0;
} else {
increment += tg / (t[ll].num_months + 1);
}
}
}
async function getSinkingContributionTotal(t, remainder, last_month_balance) {
//return the contribution amount if there is a balance carried in the category
let total = 0;
for (let ll = 0; ll < t.length; ll++) {
remainder =
ll === 0 ? t[ll].target - last_month_balance : t[ll].target - remainder;
let tg = 0;
if (remainder >= 0) {
tg = remainder;
remainder = 0;
} else {
tg = 0;
remainder = Math.abs(remainder);
}
total += tg / (t[ll].num_months + 1);
}
return total;
}

async function getSinkingBaseContributionTotal(t) {
//return only the base contribution of each schedule
let total = 0;
for (let ll = 0; ll < t.length; ll++) {
total += t[ll].target / t[ll].target_interval;
}
return total;
}

async function getSinkingTotal(t) {
//sum the total of all upcoming schedules
let total = 0;
for (let ll = 0; ll < t.length; ll++) {
total += t[ll].target;
}
return total;
}

export async function goalsSchedule(
scheduleFlag,
template_lines,
current_month,
balance,
remainder,
last_month_balance,
to_budget,
errors,
category,
) {
if (!scheduleFlag) {
scheduleFlag = true;
const template = template_lines.filter(t => t.type === 'schedule');
//in the case of multiple templates per category, schedules may have wrong priority level

const t = await createScheduleList(template, current_month, category);
errors = errors.concat(t.errors);

const t_payMonthOf = t.t.filter(
c =>
c.full ||
(c.target_frequency === 'monthly' &&
c.target_interval === 1 &&
c.num_months === 0) ||
(c.target_frequency === 'weekly' &&
c.target_interval >= 0 &&
c.num_months === 0) ||
c.target_frequency === 'daily' ||
isReflectBudget(),
);

const t_sinking = t.t
.filter(
c =>
(!c.full &&
c.target_frequency === 'monthly' &&
c.target_interval > 1) ||
(!c.full &&
c.target_frequency === 'monthly' &&
c.num_months > 0 &&
c.target_interval === 1) ||
(!c.full && c.target_frequency === 'yearly') ||
(!c.full && c.target_frequency === undefined),
)
.sort((a, b) => a.next_date_string.localeCompare(b.next_date_string));

const totalPayMonthOf = await getPayMonthOfTotal(t_payMonthOf);

const totalSinking = await getSinkingTotal(t_sinking);
const totalSinkingBaseContribution = await getSinkingBaseContributionTotal(
t_sinking,
);

if (balance >= totalSinking + totalPayMonthOf) {
to_budget += Math.round(totalPayMonthOf + totalSinkingBaseContribution);
} else {
const totalSinkingContribution = await getSinkingContributionTotal(
t_sinking,
remainder,
last_month_balance,
);
to_budget += Math.round(totalPayMonthOf + totalSinkingContribution);
}
increment = Math.round(increment);
to_budget += increment;
}
return { to_budget, errors, remainder, scheduleFlag };
}
6 changes: 6 additions & 0 deletions upcoming-release-notes/2102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Maintenance
authors: [shall0pass]
---

Goals: Refactor schedules file into functions and improve the readability of the code.
Loading