From 832fd1e5d82dd7d36359d793332bc589038c4317 Mon Sep 17 00:00:00 2001 From: Julian Dominguez-Schatz Date: Thu, 2 Jan 2025 18:01:19 -0500 Subject: [PATCH] Fix schedule split template amounts (#4077) * Fix incorrect argument to goals schedule function * Add argument types to prevent similar issues * Add release notes * Fix test types --- .../src/server/budget/categoryTemplate.ts | 29 ++++++++-------- .../src/server/budget/goalsSchedule.test.ts | 20 ++++++++--- .../src/server/budget/goalsSchedule.ts | 34 ++++++++++++------- .../src/server/budget/goaltemplates.ts | 12 ++++--- upcoming-release-notes/4077.md | 6 ++++ 5 files changed, 66 insertions(+), 35 deletions(-) create mode 100644 upcoming-release-notes/4077.md diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 3106bf1e348..2b6e467f9c4 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -2,6 +2,7 @@ import * as monthUtils from '../../shared/months'; import { amountToInteger } from '../../shared/util'; +import { CategoryEntity } from '../../types/models'; import * as db from '../db'; import { getSheetValue, getSheetBoolean } from './actions'; @@ -29,18 +30,18 @@ export class CategoryTemplate { // Class interface // set up the class and check all templates - static async init(templates: Template[], categoryID: string, month) { + static async init(templates: Template[], category: CategoryEntity, month) { // get all the needed setup values const lastMonthSheet = monthUtils.sheetForMonth( monthUtils.subMonths(month, 1), ); const lastMonthBalance = await getSheetValue( lastMonthSheet, - `leftover-${categoryID}`, + `leftover-${category.id}`, ); const carryover = await getSheetBoolean( lastMonthSheet, - `carryover-${categoryID}`, + `carryover-${category.id}`, ); let fromLastMonth; if (lastMonthBalance < 0 && !carryover) { @@ -52,7 +53,7 @@ export class CategoryTemplate { await CategoryTemplate.checkByAndScheduleAndSpend(templates, month); await CategoryTemplate.checkPercentage(templates); // call the private constructor - return new CategoryTemplate(templates, categoryID, month, fromLastMonth); + return new CategoryTemplate(templates, category, month, fromLastMonth); } getPriorities(): number[] { @@ -126,7 +127,7 @@ export class CategoryTemplate { case 'schedule': { const budgeted = await getSheetValue( monthUtils.sheetForMonth(this.month), - `leftover-${this.categoryID}`, + `leftover-${this.category.id}`, ); const ret = await goalsSchedule( scheduleFlag, @@ -137,7 +138,7 @@ export class CategoryTemplate { this.fromLastMonth, toBudget, [], - this.categoryID, + this.category, ); toBudget = ret.to_budget; remainder = ret.remainder; @@ -203,7 +204,7 @@ export class CategoryTemplate { //----------------------------------------------------------------------------- // Implementation - readonly categoryID: string; //readonly so we can double check the category this is using + readonly category: CategoryEntity; //readonly so we can double check the category this is using private month: string; private templates = []; private remainder = []; @@ -223,11 +224,11 @@ export class CategoryTemplate { private constructor( templates: Template[], - categoryID: string, + category: CategoryEntity, month: string, fromLastMonth: number, ) { - this.categoryID = categoryID; + this.category = category; this.month = month; this.fromLastMonth = fromLastMonth; // sort the template lines into regular template, goals, and remainder templates @@ -424,7 +425,7 @@ export class CategoryTemplate { const sheetName = monthUtils.sheetForMonth( monthUtils.subMonths(this.month, template.lookBack), ); - return await getSheetValue(sheetName, `budget-${this.categoryID}`); + return await getSheetValue(sheetName, `budget-${this.category.id}`); } private runWeek(template): number { @@ -472,18 +473,18 @@ export class CategoryTemplate { //TODO figure out if I already found these values and can pass them in const spent = await getSheetValue( sheetName, - `sum-amount-${this.categoryID}`, + `sum-amount-${this.category.id}`, ); const balance = await getSheetValue( sheetName, - `leftover-${this.categoryID}`, + `leftover-${this.category.id}`, ); alreadyBudgeted = balance - spent; firstMonth = false; } else { alreadyBudgeted += await getSheetValue( sheetName, - `budget-${this.categoryID}`, + `budget-${this.category.id}`, ); } } @@ -536,7 +537,7 @@ export class CategoryTemplate { const sheetName = monthUtils.sheetForMonth( monthUtils.subMonths(this.month, i), ); - sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); + sum += await getSheetValue(sheetName, `sum-amount-${this.category.id}`); } return -Math.round(sum / template.numMonths); } diff --git a/packages/loot-core/src/server/budget/goalsSchedule.test.ts b/packages/loot-core/src/server/budget/goalsSchedule.test.ts index 6fa59205e8a..8e8d48b22ec 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.test.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.test.ts @@ -22,14 +22,20 @@ describe('goalsSchedule', () => { it('should return correct budget when recurring schedule set', async () => { // Given const scheduleFlag = false; - const template_lines = [{ type: 'schedule', name: 'Test Schedule' }]; + const template_lines = [ + { + type: 'schedule', + name: 'Test Schedule', + directive: '#template schedule Test Schedule', + } as const, + ]; const current_month = '2024-08-01'; const balance = 0; const remainder = 0; const last_month_balance = 0; const to_budget = 0; const errors: string[] = []; - const category = { id: 1, name: 'Test Category' }; + const category = { id: '1', name: 'Test Category' }; (db.first as jest.Mock).mockResolvedValue({ id: 1, completed: 0 }); (getRuleForSchedule as jest.Mock).mockResolvedValue({ @@ -86,14 +92,20 @@ describe('goalsSchedule', () => { it('should return correct budget when yearly recurring schedule set and balance is greater than target', async () => { // Given const scheduleFlag = false; - const template_lines = [{ type: 'schedule', name: 'Test Schedule' }]; + const template_lines = [ + { + type: 'schedule', + name: 'Test Schedule', + directive: '#template schedule Test Schedule', + } as const, + ]; const current_month = '2024-09-01'; const balance = 12000; const remainder = 0; const last_month_balance = 12000; const to_budget = 0; const errors: string[] = []; - const category = { id: 1, name: 'Test Category' }; + const category = { id: '1', name: 'Test Category' }; (db.first as jest.Mock).mockResolvedValue({ id: 1, completed: 0 }); (getRuleForSchedule as jest.Mock).mockResolvedValue({ diff --git a/packages/loot-core/src/server/budget/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts index 87a84485c7e..e2b74666c9b 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.ts @@ -1,6 +1,7 @@ // @ts-strict-ignore import * as monthUtils from '../../shared/months'; import { extractScheduleConds } from '../../shared/schedules'; +import { CategoryEntity } from '../../types/models'; import * as db from '../db'; import { getRuleForSchedule, @@ -9,8 +10,13 @@ import { } from '../schedules/app'; import { isReflectBudget } from './actions'; +import { ScheduleTemplate, Template } from './types/templates'; -async function createScheduleList(template, current_month, category) { +async function createScheduleList( + template: ScheduleTemplate[], + current_month: string, + category: CategoryEntity, +) { const t = []; const errors = []; @@ -186,22 +192,26 @@ async function getSinkingTotal(t) { } export async function goalsSchedule( - scheduleFlag, - template_lines, - current_month, - balance, - remainder, - last_month_balance, - to_budget, - errors, - category, + scheduleFlag: boolean, + template_lines: Template[], + current_month: string, + balance: number, + remainder: number, + last_month_balance: number, + to_budget: number, + errors: string[], + category: CategoryEntity, ) { if (!scheduleFlag) { scheduleFlag = true; - const template = template_lines.filter(t => t.type === 'schedule'); + const scheduleTemplates = 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); + const t = await createScheduleList( + scheduleTemplates, + current_month, + category, + ); errors = errors.concat(t.errors); const isPayMonthOf = c => diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 9e4ba9e9251..7a4b00a30c1 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -1,6 +1,7 @@ // @ts-strict-ignore import { Notification } from '../../client/state-types/notifications'; import * as monthUtils from '../../shared/months'; +import { CategoryEntity } from '../../types/models'; import * as db from '../db'; import { batchMessages } from '../sync'; @@ -46,7 +47,7 @@ export function runCheckTemplates() { return checkTemplates(); } -async function getCategories() { +async function getCategories(): Promise { return await db.all( ` SELECT categories.* FROM categories @@ -150,7 +151,8 @@ async function processTemplate( const budgetList = []; const goalList = []; for (let i = 0; i < categories.length; i++) { - const id = categories[i].id; + const category = categories[i]; + const { id } = category; const sheetName = monthUtils.sheetForMonth(month); const templates = categoryTemplates[id]; const budgeted = await getSheetValue(sheetName, `budget-${id}`); @@ -162,7 +164,7 @@ async function processTemplate( // gather needed priorities // gather remainder weights try { - const obj = await CategoryTemplate.init(templates, id, month); + const obj = await CategoryTemplate.init(templates, category, month); availBudget += budgeted; availBudget += obj.getLimitExcess(); const p = obj.getPriorities(); @@ -230,9 +232,9 @@ async function processTemplate( // finish catObjects.forEach(o => { const ret = o.getValues(); - budgetList.push({ category: o.categoryID, budgeted: ret.budgeted }); + budgetList.push({ category: o.category.id, budgeted: ret.budgeted }); goalList.push({ - category: o.categoryID, + category: o.category.id, goal: ret.goal, longGoal: ret.longGoal ? 1 : null, }); diff --git a/upcoming-release-notes/4077.md b/upcoming-release-notes/4077.md new file mode 100644 index 00000000000..bdc6024ca14 --- /dev/null +++ b/upcoming-release-notes/4077.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [jfdoming] +--- + +Fix schedule split template amounts