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
This commit is contained in:
Julian Dominguez-Schatz
2025-01-02 18:01:19 -05:00
committed by GitHub
parent 928260ca3a
commit 832fd1e5d8
5 changed files with 66 additions and 35 deletions

View File

@@ -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);
}

View File

@@ -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({

View File

@@ -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 =>

View File

@@ -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<CategoryEntity[]> {
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,
});

View File

@@ -0,0 +1,6 @@
---
category: Bugfix
authors: [jfdoming]
---
Fix schedule split template amounts