Skip to content

Commit

Permalink
[Maintenence] Refactor Goals Schedule file (actualbudget#2102)
Browse files Browse the repository at this point in the history
* refactor pass 1

* refactor pass 2

* refactor pass 3

* commented out startDate

* remove console logging

* release note

* non-repeating error

* add daily

* move else

* Fix compounding to_budget

* lint

* reapply 2125
  • Loading branch information
shall0pass authored Jan 7, 2024
1 parent bb36b20 commit 7f37304
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 135 deletions.
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.

0 comments on commit 7f37304

Please sign in to comment.