From f0d10db0602a3c3d5d6dd4be3977780e646ee547 Mon Sep 17 00:00:00 2001 From: youngcw Date: Fri, 25 Oct 2024 13:50:28 -0700 Subject: [PATCH 01/31] by check --- .../src/server/budget/categoryTemplate.ts | 419 ++++++++++++++++++ .../src/server/budget/goal-template.pegjs | 2 +- .../src/server/budget/goals/goalsSimple.ts | 19 +- 3 files changed, 422 insertions(+), 18 deletions(-) create mode 100644 packages/loot-core/src/server/budget/categoryTemplate.ts diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts new file mode 100644 index 00000000000..a408f1a4fd4 --- /dev/null +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -0,0 +1,419 @@ +// @ts-strict-ignore + +import * as monthUtils from '../../shared/months'; +import * as db from '../../db'; +import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; +import { amountToInteger, integerToAmount } from '../../shared/util'; +import {getActiveSchedules} from './statements'; + +class categoryTemplate { +/*---------------------------------------------------------------------------- + * Using This Class: + * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; + * categoryID: the ID of the category that this Class will be for + * templates: all templates for this category (including templates and goals) + * month: the month string of the month for templates being applied + * 2. gather needed data for external use. ex: remainder weights, priorities, startingBalance + * 3. run each priority level that is needed via runTemplatesForPriority + * 4. run applyLimits to apply any existing limit to the category + * 5. run the remainder templates via runRemainder (limits get applied at the start of this) + * 6. finish processing by running runFinish() + * Alternate: + * If the situation calls for it you can run all templates in a catagory in one go using the + * method runAll which will run all templates and goals and save the amounts. + */ + +//----------------------------------------------------------------------------- +// Class interface + + // returns the categoryID of the category for this object + get categoryID(){ return this.#categoryID; } + + // returns the total remainder weight of remainder templates in this category + get remainderWeight(){ return this.#remainderWeight; } + + // return the current budgeted amount in the category + get startingBalance(){ return this.#startingBalance; } + + // returns a list of priority levels in this category + get priorities(){ return this.#priorities; } + + // what is the full requested amount this month + static runAll(){ + //TODO + } + + // run all templates in a given priority level + // return: amount budgeted in this priority level + static runTemplatesForPriority(priority: int, budgetAvail: int){ + const t = this.#templates.filter(t => t.priority === priority); + let available = budgetAvail; + let toBudget = 0; + // switch on template type and calculate the amount for the line + for(let i = 0; i < t.length; i++) { + switch (t[i].type) { + case 'simple': { + toBudget += #runSimple(t,this.#limitAmount); + break; + } + case 'copy' : { + toBudget += #runCopy(t); + break; + } + case 'week': { + toBudget += #runWeek(t); + break; + } + case 'spend': { + toBudget +- #runSpend(t); + break; + } + case 'percentage': { + toBudget += #runPercentage(t, budgetAvail); + break; + } + case 'by': { + toBudget += #runBy(t); + break; + } + case 'schedule': { + toBudget += #runSchedule(t); + break; + } + case 'average': { + toBudget += #runAverage(t); + break; + } + } + + // don't overbudget when using a priority + if(priority > 0 && toBudget>available){ + toBudget=available; + } + available = available - toBudget; + + if(available <= 0) { break; } + } + + this.#toBudgetAmount += toBudget; + return toBudget; + } + + static applyLimit(){ + if(this.#limitCheck === false){ + this.#toBudgetAmount = this.#targetAmount; + return + } + if(this.#limitHold && this.#fromLastMonth >= this.#limitAmount){ + this.#toBudgetAmount = 0; + return + } + if(this.#targetAmount>this.#limitAmount){ + this.#toBudget = this.#limitAmount - this.#fromLastMonth; + } + } + + // run all of the 'remainder' type templates + static runRemainder(budgetAvail: int, perWeight: int){ + const toBudget = Math.round(this.#remainderWeight*perWeight); + //check possible overbudget from rounding, 1cent leftover + if(toBudget > budgetAvail){ + this.#toBudgetAmount += budgetAvail; + } else if (budgetAvail - toBudget === 1){ + this.#toBudgetAmount+=toBudget + 1; + } else { + this.#toBudgetAmount+=toBudget; + } + } + + static runFinish(){ + #runGoal() + #setBudget(); + #setGoal(); + } + + +//----------------------------------------------------------------------------- +// Implimentation + + #categoryID; + #month; + #templates = []; + #remainder = []; + #goals = []; + #priorities = []; + #remainderWeight = 0; // sum of all remainder weights in category + #startingBalance = 0; // budgeted at the start; + #toBudgetAmount = 0; // amount that will be budgeted by the templates + #isLongGoal = false; // is the goal type a long goal + #goalAmount = 0; + #spentThisMonth = 0; // amount already spend this month + #fromLastMonth = 0; // leftover from last month + #limitAmount = 0, + #limitCheck = false, + #limitHold = false; + + constructor(templates, categoryID, month) { + this.#categoryID = categoryID; + this.#month = month; + // sort the template lines into regular template, goals, and remainder templates + this.#templates = templates.filter(t => {t.directive === 'template' && t.type != 'remainder'}); + this.#remainder = templates.filter(t =>{t.directive === 'template' && t.type === 'remainder'}); + this.#goals = templates.filter(t => {t.directive === 'goal'}); + //check templates and throw exception if there is something wrong + #checkTemplates(); + //find priorities + #findPriorities(); + //find remainder weight + #findRemainderWeightSum(); + //get the starting budget amount + #readBudgeted(); + //get the from last month amount + #readSpent(); + #calcLeftover(); + } + + static #findPriorities(){ + let p = []; + this.#templates.forEach(t => { + if(t.priority != null){ + p.push(t.priority) + } + }); + //sort and reduce to unique items + this.#priorities = p.sort(function (a,b) { + return a-b; + }) + .filter((item, idx, curr) => curr.indexOf(item) === index); + } + + static #findRemainderWeightSum(){ + let weight = 0; + this.#remainder.forEach(r => {weight+=r.weight}); + this.remainderWeight = weight; + } + + static #checkTemplates(){ + //run all the individual checks + #checkByAndSchedule(); + #checkPercentage(); + #checkLimit(); + #checkGoal(); + } + + static #runGoal(){ + if(this.#goals.length>0) { + this.#isLongGoal = true; + this.#goalAmount = amountToInteger(this.#goals[0].amount); + return + } + this.#goalAmount = this.#toBudgetAmount; + } + + static #setBudget(){ + setBudget({ + category: this.#categoryID, + month: this.#month, + amount: this.#toBudgetAmount, + }); + } + + static #readBudgeted(){ + return await getSheetValue( + monthUtils.sheetForMonth(month), + `budget-${this.categoryID}`, + ); + } + + static #setGoal(){ + setGoal({ + category: this.categoryID, + goal: this.#goalAmount; + month: this.#month, + long_goal: this.#isLongGoal ? 1 : 0, + }); + } + +//----------------------------------------------------------------------------- +// Template Validation + static #checkByAndSchedule(){ + //check schedule names + const scheduleNames = (await getActiveSchedules()).map(({name}) => name); + this.#templates + .filter(t => t.type === 'schedule') + .forEach(t => { + if (!scheduleNames.includes(t.name)) { + throw new Error(`Schedule "${t.name}" does not exist`); + } + }); + //find lowest priority + let lowestPriority = null; + this.#templates + .filter(t=> t.type === 'schedule' || t.type === 'by' ) + .forEach(t => { + if(lowestPriority === null || t.priority < lowestPriority){ + lowestPriority = t.priority; + } + }); + //set priority to needed value + this.#templates + .filter(t => t.type === 'schedule' || t.type === 'by') + .forEach(t => t.priority = lowestPriority); + //TODO add a message that the priority has been changed + } + + static #checkLimit(){ + for(let i = 0; i < this.#templates.length; i++){ + if(this.#limitCheck){ + throw new Error('Only one `up to` allowed per category'); + break; + } else if (t.limit!=null){ + this.limitCheck = true; + this.#limitHold = t.limit.hold ? true : false; + this.#limitAmount = amountToInteger(t.limit.amount); + } + }); + } + + static #checkPercentage(){ + const pt = this.#templates.filter(t => t.type === 'percentage'); + let reqCategories = []; + pt.forEach(t => reqCategories.push(t.category)); + + const availCategories = await db.getCategories(); + let availNames = []; + availCategories.forEach(c => { + if(c.is_income){ + availNames.push(c.name.toLowerCase()); + } + }); + + reqCategories.forEach(n => { + if(n === 'availble funds' || n === 'all income'){ + //skip the name check since these are special + continue; + } else if (!availNames.includes(n)) { + throw new Error(`Category ${t.category} is not found in available income categories`); + } + }); + } + + static #checkGoal(){ + if(this.#goals.length>1){ + throw new Error('Can only have one #goal per category'); + } + } + +//----------------------------------------------------------------------------- +// Processor Functions + + static #runSimple(template, limit){ + let toBudget = 0; + if (template.monthly != null) { + toBudget= amountToInteger(template.monthly); + } else { + toBudget = limit; + } + return toBudget; + } + + static #runCopy(template){ + const sheetName = monthUtils.sheetForMonth( + monthUtils.subMonts(this.#month,template.lookback), + ); + return await getSheetValue(sheetName, `budget-${this.#categoryID}`); + } + + static #runWeek(template){ + let toBudget = 0; + const amount = amountToInteger(template.amount); + const weeks = template.weeks != null ? match.round(template.weeks) : 1; + let w = template.starting; + const nextMonth = monthUtils.addMonths(this.#month,1); + + while (w < nextMonth){ + if( w >= this.#month) { + toBudget += amount; + } + w = monthUtils.addWeeks(w, weeks); + } + return toBudget; + } + + static #runSpend(template){ + const fromMonth = `${template.from}`; + const toMonth = `${template.month}`; + let alreadyBudgeted = this.#fromLastMonth; + let firstMonth = true; + + for ( + let m = fromMonth; + monthUtils.differenceIncCalendarMonths(this.#month, m) > 0; + m = monthUtils.addMonths(m,1) + ) { + const sheetName = monthUtils.sheetForMonth(m); + if(firstMonth) { + //TODO figure out if I already found these values and can pass them in + const spent = await getSheetValue(sheetName, `sum-amount-${this.#categoryID}`); + const balance = await getSheetValue(sheetName, `leftover-${this.#categoryID}`); + alreadyBudgeted = balance - spent; + firstMonth = false; + } else { + alreadyBudgeted += await getSheetValue(sheetName, `budget-${this.#categoryID}`); + } + } + + const numMonths = monthUtils.differenceIncCalendarMonths(toMonth, this.#month); + const target = amountToInteger(template.amount); + if(numMonths < 0){ + return 0; + } else { + return Math.round((target-alreadyBudget) /(numMonths + 1)); + } + } + + static #runPercentage(template, availableFunds)){ + const percent = template.percent; + const cat = template.category.toLowerCase(); + const prev = template.previous; + let montlyIncome = 0; + + //choose the sheet to find income for + if(prev){ + const sheetName = monthUtils.sheetForMonth( + monthUtils.subMonths(this.#month, 1) + ); + } else { + const sheetName = monthUtils.sheetForMonth(this.#month); + } + if(cat === 'all income'){ + montlyIncome = await getSheetValue(sheetName,`total-income`); + } else if (cat === 'available funds') { + montlyIncome = availableFunds; + } else { + const incomeCat = (await db.getCategories()).find(c=> c.is_income && c.name.toLowerCase() === cat); + monthlyIncome = await getSheetValue(sheetName, `sum_amount-${incomeCat.id}`); + } + + return Math.max(0, Math.round(montlyIncome * (percent /100))); + } + + static #runBy(template_lines){ + //TODO + } + + static #runSchedule(){ + //TODO + } + + static #runAverage(template){ + let sum = 0; + for (let i = 1; i <= template.numMonths; i++) { + const sheetName = monthUtils.sheetForMonth( + monthUtils.subMonts(this.#month, i) + ); + sum += await getSheetValue(sheetName, `sum-amount-${this.#categoryID}`); + } + return -Math.round(sum / template.amount); + } +} diff --git a/packages/loot-core/src/server/budget/goal-template.pegjs b/packages/loot-core/src/server/budget/goal-template.pegjs index e8805c7c5b1..4da54588299 100644 --- a/packages/loot-core/src/server/budget/goal-template.pegjs +++ b/packages/loot-core/src/server/budget/goal-template.pegjs @@ -23,7 +23,7 @@ expr / template: template _ remainder: remainder limit: limit? { return { type: 'remainder', priority: null, directive: template.directive, weight: remainder, limit } } / template: template _ 'average'i _ amount: positive _ 'months'i? - { return { type: 'average', amount: +amount, priority: template.priority, directive: template.directive }} + { return { type: 'average', numMonths: +amount, priority: template.priority, directive: template.directive }} / template: template _ 'copy from'i _ lookBack: positive _ 'months ago'i limit:limit? { return { type: 'copy', priority: template.priority, directive: template.directive, lookBack: +lookBack, limit }} / goal: goal amount: amount { return {type: 'simple', amount: amount, priority: null, directive: 'goal' }} diff --git a/packages/loot-core/src/server/budget/goals/goalsSimple.ts b/packages/loot-core/src/server/budget/goals/goalsSimple.ts index eec6e962497..c0af20511f2 100644 --- a/packages/loot-core/src/server/budget/goals/goalsSimple.ts +++ b/packages/loot-core/src/server/budget/goals/goalsSimple.ts @@ -3,31 +3,16 @@ import { amountToInteger } from '../../../shared/util'; export async function goalsSimple( template, - limitCheck, - errors, limit, - hold, - to_budget, - last_month_balance, ) { // simple has 'monthly' and/or 'limit' params - if (template.limit != null) { - if (limitCheck) { - errors.push(`More than one “up to” limit found.`); - return { to_budget, errors, limit, limitCheck, hold }; - } else { - limitCheck = true; - limit = amountToInteger(template.limit.amount); - hold = template.limit.hold; - } - } let increment = 0; if (template.monthly != null) { const monthly = amountToInteger(template.monthly); increment = monthly; } else { - increment = limit - last_month_balance; + increment = limit; } to_budget += increment; - return { to_budget, errors, limit, limitCheck, hold }; + return { to_budget }; } From 5ee72573fa93682554f778e335ec593f3b784e1f Mon Sep 17 00:00:00 2001 From: youngcw Date: Tue, 29 Oct 2024 17:38:13 -0700 Subject: [PATCH 02/31] minor changes and TS class migration --- .../src/server/budget/categoryTemplate.ts | 295 +++++++++--------- 1 file changed, 154 insertions(+), 141 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a408f1a4fd4..b75b8d71761 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -1,9 +1,9 @@ // @ts-strict-ignore import * as monthUtils from '../../shared/months'; -import * as db from '../../db'; +import * as db from '../db'; import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; -import { amountToInteger, integerToAmount } from '../../shared/util'; +import { amountToInteger } from '../../shared/util'; import {getActiveSchedules} from './statements'; class categoryTemplate { @@ -26,220 +26,230 @@ class categoryTemplate { //----------------------------------------------------------------------------- // Class interface - // returns the categoryID of the category for this object - get categoryID(){ return this.#categoryID; } - // returns the total remainder weight of remainder templates in this category - get remainderWeight(){ return this.#remainderWeight; } + get remainderWeight(){ return this.remainderWeight; } // return the current budgeted amount in the category - get startingBalance(){ return this.#startingBalance; } + get startingBalance(){ return this.startingBalance; } // returns a list of priority levels in this category - get priorities(){ return this.#priorities; } + get priorities(){ return this.priorities; } // what is the full requested amount this month - static runAll(){ - //TODO + runAll(force: boolean, available: number){ + let toBudget; + this.priorities.forEach(p => { + toBudget += this.runTemplatesForPriority(p, available, force); + }); + return toBudget; } // run all templates in a given priority level // return: amount budgeted in this priority level - static runTemplatesForPriority(priority: int, budgetAvail: int){ - const t = this.#templates.filter(t => t.priority === priority); + async runTemplatesForPriority(priority: number, budgetAvail: number, force: boolean){ + const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail; let toBudget = 0; // switch on template type and calculate the amount for the line for(let i = 0; i < t.length; i++) { switch (t[i].type) { case 'simple': { - toBudget += #runSimple(t,this.#limitAmount); + toBudget += this.runSimple(t,this.limitAmount); break; } case 'copy' : { - toBudget += #runCopy(t); + toBudget += await this.runCopy(t); break; } case 'week': { - toBudget += #runWeek(t); + toBudget += this.runWeek(t); break; } case 'spend': { - toBudget +- #runSpend(t); + toBudget +-this.runSpend(t); break; } case 'percentage': { - toBudget += #runPercentage(t, budgetAvail); + toBudget += await this.runPercentage(t, budgetAvail); break; } case 'by': { - toBudget += #runBy(t); + toBudget += this.runBy(t); break; } case 'schedule': { - toBudget += #runSchedule(t); + toBudget += this.runSchedule(t); break; } case 'average': { - toBudget += #runAverage(t); + toBudget += await this.runAverage(t); break; } } // don't overbudget when using a priority - if(priority > 0 && toBudget>available){ + if(priority > 0 && force === false &&toBudget>available){ toBudget=available; } available = available - toBudget; - if(available <= 0) { break; } + if(available <= 0 && force === false) { break; } } - this.#toBudgetAmount += toBudget; + this.toBudgetAmount += toBudget; return toBudget; } - static applyLimit(){ - if(this.#limitCheck === false){ - this.#toBudgetAmount = this.#targetAmount; + applyLimit(){ + if(this.limitCheck === false){ + this.toBudgetAmount = this.targetAmount; return } - if(this.#limitHold && this.#fromLastMonth >= this.#limitAmount){ - this.#toBudgetAmount = 0; + if(this.limitHold && this.fromLastMonth >= this.limitAmount){ + this.toBudgetAmount = 0; return } - if(this.#targetAmount>this.#limitAmount){ - this.#toBudget = this.#limitAmount - this.#fromLastMonth; + if(this.targetAmount>this.limitAmount){ + this.toBudgetAmount = this.limitAmount - this.fromLastMonth; } } // run all of the 'remainder' type templates - static runRemainder(budgetAvail: int, perWeight: int){ - const toBudget = Math.round(this.#remainderWeight*perWeight); + runRemainder(budgetAvail: number, perWeight: number){ + const toBudget = Math.round(this.remainderWeight*perWeight); //check possible overbudget from rounding, 1cent leftover if(toBudget > budgetAvail){ - this.#toBudgetAmount += budgetAvail; + this.toBudgetAmount += budgetAvail; } else if (budgetAvail - toBudget === 1){ - this.#toBudgetAmount+=toBudget + 1; + this.toBudgetAmount+=toBudget + 1; } else { - this.#toBudgetAmount+=toBudget; + this.toBudgetAmount+=toBudget; } } - static runFinish(){ - #runGoal() - #setBudget(); - #setGoal(); + runFinish(){ + this.runGoal() + this.setBudget(); + this.setGoal(); } //----------------------------------------------------------------------------- // Implimentation - - #categoryID; - #month; - #templates = []; - #remainder = []; - #goals = []; - #priorities = []; - #remainderWeight = 0; // sum of all remainder weights in category - #startingBalance = 0; // budgeted at the start; - #toBudgetAmount = 0; // amount that will be budgeted by the templates - #isLongGoal = false; // is the goal type a long goal - #goalAmount = 0; - #spentThisMonth = 0; // amount already spend this month - #fromLastMonth = 0; // leftover from last month - #limitAmount = 0, - #limitCheck = false, - #limitHold = false; - - constructor(templates, categoryID, month) { - this.#categoryID = categoryID; - this.#month = month; + readonly categoryID: string; + private month; + private templates = []; + private remainder = []; + private goals = []; + private toBudgetAmount = 0; // amount that will be budgeted by the templates + private targetAmount = 0; + private isLongGoal = false; // is the goal type a long goal + private goalAmount = 0; + private spentThisMonth = 0; // amount already spend this month + private fromLastMonth = 0; // leftover from last month + private limitAmount = 0; + private limitCheck = false; + private limitHold = false; + + private set priorities(value){this.priorities=value} + private set remainderWeight(value){this.remainderWeight=value} + private set startingBalance(value){this.startingBalance=value} + + constructor(templates, categoryID: string, month) { + this.categoryID = categoryID; + this.month = month; // sort the template lines into regular template, goals, and remainder templates - this.#templates = templates.filter(t => {t.directive === 'template' && t.type != 'remainder'}); - this.#remainder = templates.filter(t =>{t.directive === 'template' && t.type === 'remainder'}); - this.#goals = templates.filter(t => {t.directive === 'goal'}); + this.templates = templates.filter(t => {t.directive === 'template' && t.type != 'remainder'}); + this.remainder = templates.filter(t =>{t.directive === 'template' && t.type === 'remainder'}); + this.goals = templates.filter(t => {t.directive === 'goal'}); //check templates and throw exception if there is something wrong - #checkTemplates(); + this.checkTemplates(); //find priorities - #findPriorities(); + this.findPriorities(); //find remainder weight - #findRemainderWeightSum(); + this.findRemainderWeightSum(); //get the starting budget amount - #readBudgeted(); + this.readBudgeted(); //get the from last month amount - #readSpent(); - #calcLeftover(); + this.readSpent(); + this.calcLeftover(); } - - static #findPriorities(){ + + private findPriorities(){ let p = []; - this.#templates.forEach(t => { + this.templates.forEach(t => { if(t.priority != null){ p.push(t.priority) } }); //sort and reduce to unique items - this.#priorities = p.sort(function (a,b) { + this.priorities = p.sort(function (a,b) { return a-b; }) - .filter((item, idx, curr) => curr.indexOf(item) === index); + .filter((item, idx, curr) => curr.indexOf(item) === idx); } - static #findRemainderWeightSum(){ + private findRemainderWeightSum(){ let weight = 0; - this.#remainder.forEach(r => {weight+=r.weight}); + this.remainder.forEach(r => {weight+=r.weight}); this.remainderWeight = weight; } - static #checkTemplates(){ + private checkTemplates(){ //run all the individual checks - #checkByAndSchedule(); - #checkPercentage(); - #checkLimit(); - #checkGoal(); + this.checkByAndSchedule(); + this.checkPercentage(); + this.checkLimit(); + this.checkGoal(); } - static #runGoal(){ - if(this.#goals.length>0) { - this.#isLongGoal = true; - this.#goalAmount = amountToInteger(this.#goals[0].amount); + private runGoal(){ + if(this.goals.length>0) { + this.isLongGoal = true; + this.goalAmount = amountToInteger(this.goals[0].amount); return } - this.#goalAmount = this.#toBudgetAmount; + this.goalAmount = this.toBudgetAmount; } - static #setBudget(){ + private setBudget(){ setBudget({ - category: this.#categoryID, - month: this.#month, - amount: this.#toBudgetAmount, + category: this.categoryID, + month: this.month, + amount: this.toBudgetAmount, }); } - static #readBudgeted(){ + private async readBudgeted(){ return await getSheetValue( - monthUtils.sheetForMonth(month), + monthUtils.sheetForMonth(this.month), `budget-${this.categoryID}`, ); } - static #setGoal(){ + private setGoal(){ setGoal({ category: this.categoryID, - goal: this.#goalAmount; - month: this.#month, - long_goal: this.#isLongGoal ? 1 : 0, + goal: this.goalAmount, + month: this.month, + long_goal: this.isLongGoal ? 1 : 0, }); } + private readSpent(){ + //TODO + } + + private calcLeftover(){ + //TODO + } + //----------------------------------------------------------------------------- // Template Validation - static #checkByAndSchedule(){ + private async checkByAndSchedule(){ //check schedule names const scheduleNames = (await getActiveSchedules()).map(({name}) => name); - this.#templates + this.templates .filter(t => t.type === 'schedule') .forEach(t => { if (!scheduleNames.includes(t.name)) { @@ -248,7 +258,7 @@ class categoryTemplate { }); //find lowest priority let lowestPriority = null; - this.#templates + this.templates .filter(t=> t.type === 'schedule' || t.type === 'by' ) .forEach(t => { if(lowestPriority === null || t.priority < lowestPriority){ @@ -256,27 +266,28 @@ class categoryTemplate { } }); //set priority to needed value - this.#templates + this.templates .filter(t => t.type === 'schedule' || t.type === 'by') .forEach(t => t.priority = lowestPriority); //TODO add a message that the priority has been changed } - static #checkLimit(){ - for(let i = 0; i < this.#templates.length; i++){ - if(this.#limitCheck){ + private checkLimit(){ + for(let i = 0; i < this.templates.length; i++){ + const t = this.templates[i]; + if(this.limitCheck){ throw new Error('Only one `up to` allowed per category'); break; } else if (t.limit!=null){ this.limitCheck = true; - this.#limitHold = t.limit.hold ? true : false; - this.#limitAmount = amountToInteger(t.limit.amount); + this.limitHold = t.limit.hold ? true : false; + this.limitAmount = amountToInteger(t.limit.amount); } - }); + }; } - static #checkPercentage(){ - const pt = this.#templates.filter(t => t.type === 'percentage'); + private async checkPercentage(){ + const pt = this.templates.filter(t => t.type === 'percentage'); let reqCategories = []; pt.forEach(t => reqCategories.push(t.category)); @@ -291,15 +302,14 @@ class categoryTemplate { reqCategories.forEach(n => { if(n === 'availble funds' || n === 'all income'){ //skip the name check since these are special - continue; } else if (!availNames.includes(n)) { - throw new Error(`Category ${t.category} is not found in available income categories`); + throw new Error(`Category ${n} is not found in available income categories`); } }); } - static #checkGoal(){ - if(this.#goals.length>1){ + private checkGoal(){ + if(this.goals.length>1){ throw new Error('Can only have one #goal per category'); } } @@ -307,7 +317,7 @@ class categoryTemplate { //----------------------------------------------------------------------------- // Processor Functions - static #runSimple(template, limit){ + private runSimple(template, limit){ let toBudget = 0; if (template.monthly != null) { toBudget= amountToInteger(template.monthly); @@ -317,22 +327,22 @@ class categoryTemplate { return toBudget; } - static #runCopy(template){ + private async runCopy(template){ const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonts(this.#month,template.lookback), + monthUtils.subMonths(this.month,template.lookback), ); - return await getSheetValue(sheetName, `budget-${this.#categoryID}`); + return await getSheetValue(sheetName, `budget-${this.categoryID}`); } - static #runWeek(template){ + private runWeek(template){ let toBudget = 0; const amount = amountToInteger(template.amount); - const weeks = template.weeks != null ? match.round(template.weeks) : 1; + const weeks = template.weeks != null ? Math.round(template.weeks) : 1; let w = template.starting; - const nextMonth = monthUtils.addMonths(this.#month,1); + const nextMonth = monthUtils.addMonths(this.month,1); while (w < nextMonth){ - if( w >= this.#month) { + if( w >= this.month) { toBudget += amount; } w = monthUtils.addWeeks(w, weeks); @@ -340,79 +350,82 @@ class categoryTemplate { return toBudget; } - static #runSpend(template){ + private async runSpend(template){ const fromMonth = `${template.from}`; const toMonth = `${template.month}`; - let alreadyBudgeted = this.#fromLastMonth; + let alreadyBudgeted = this.fromLastMonth; let firstMonth = true; for ( let m = fromMonth; - monthUtils.differenceIncCalendarMonths(this.#month, m) > 0; + monthUtils.differenceInCalendarMonths(this.month, m) > 0; m = monthUtils.addMonths(m,1) ) { const sheetName = monthUtils.sheetForMonth(m); if(firstMonth) { //TODO figure out if I already found these values and can pass them in - const spent = await getSheetValue(sheetName, `sum-amount-${this.#categoryID}`); - const balance = await getSheetValue(sheetName, `leftover-${this.#categoryID}`); + const spent = await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); + const balance = await getSheetValue(sheetName, `leftover-${this.categoryID}`); alreadyBudgeted = balance - spent; firstMonth = false; } else { - alreadyBudgeted += await getSheetValue(sheetName, `budget-${this.#categoryID}`); + alreadyBudgeted += await getSheetValue(sheetName, `budget-${this.categoryID}`); } } - const numMonths = monthUtils.differenceIncCalendarMonths(toMonth, this.#month); + const numMonths = monthUtils.differenceInCalendarMonths(toMonth, this.month); const target = amountToInteger(template.amount); if(numMonths < 0){ return 0; } else { - return Math.round((target-alreadyBudget) /(numMonths + 1)); + return Math.round((target-alreadyBudgeted) /(numMonths + 1)); } } - static #runPercentage(template, availableFunds)){ + private async runPercentage(template, availableFunds){ const percent = template.percent; const cat = template.category.toLowerCase(); const prev = template.previous; - let montlyIncome = 0; + let sheetName; + let monthlyIncome = 0; //choose the sheet to find income for if(prev){ - const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.#month, 1) + sheetName = monthUtils.sheetForMonth( + monthUtils.subMonths(this.month, 1) ); } else { - const sheetName = monthUtils.sheetForMonth(this.#month); + sheetName = monthUtils.sheetForMonth(this.month); } if(cat === 'all income'){ - montlyIncome = await getSheetValue(sheetName,`total-income`); + monthlyIncome = await getSheetValue(sheetName,`total-income`); } else if (cat === 'available funds') { - montlyIncome = availableFunds; + monthlyIncome = availableFunds; } else { const incomeCat = (await db.getCategories()).find(c=> c.is_income && c.name.toLowerCase() === cat); monthlyIncome = await getSheetValue(sheetName, `sum_amount-${incomeCat.id}`); } - return Math.max(0, Math.round(montlyIncome * (percent /100))); + return Math.max(0, Math.round(monthlyIncome * (percent /100))); } - static #runBy(template_lines){ + private runBy(template_lines){ //TODO + return 0; } - static #runSchedule(){ + private runSchedule(template_lines){ //TODO + return 0; } - static #runAverage(template){ + private async runAverage(template){ let sum = 0; for (let i = 1; i <= template.numMonths; i++) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonts(this.#month, i) + monthUtils.subMonths(this.month, i) ); - sum += await getSheetValue(sheetName, `sum-amount-${this.#categoryID}`); + sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); } return -Math.round(sum / template.amount); } From b8d19f2c87fb1bd8254242b55d986726b340c813 Mon Sep 17 00:00:00 2001 From: youngcw Date: Tue, 29 Oct 2024 19:04:16 -0700 Subject: [PATCH 03/31] good starting point --- .../src/server/budget/categoryTemplate.ts | 423 ++++++++++-------- .../src/server/budget/goals/goalsSimple.ts | 8 +- .../src/server/budget/types/templates.d.ts | 6 + 3 files changed, 255 insertions(+), 182 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index b75b8d71761..d995d2e0a16 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -1,63 +1,81 @@ // @ts-strict-ignore import * as monthUtils from '../../shared/months'; +import { amountToInteger } from '../../shared/util'; import * as db from '../db'; +import { Template } from './types/templates'; + import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; -import { amountToInteger } from '../../shared/util'; -import {getActiveSchedules} from './statements'; +import { getActiveSchedules } from './statements'; class categoryTemplate { -/*---------------------------------------------------------------------------- - * Using This Class: - * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; - * categoryID: the ID of the category that this Class will be for - * templates: all templates for this category (including templates and goals) - * month: the month string of the month for templates being applied - * 2. gather needed data for external use. ex: remainder weights, priorities, startingBalance - * 3. run each priority level that is needed via runTemplatesForPriority - * 4. run applyLimits to apply any existing limit to the category - * 5. run the remainder templates via runRemainder (limits get applied at the start of this) - * 6. finish processing by running runFinish() - * Alternate: - * If the situation calls for it you can run all templates in a catagory in one go using the - * method runAll which will run all templates and goals and save the amounts. - */ - -//----------------------------------------------------------------------------- -// Class interface - + /*---------------------------------------------------------------------------- + * Using This Class: + * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; + * categoryID: the ID of the category that this Class will be for + * templates: all templates for this category (including templates and goals) + * month: the month string of the month for templates being applied + * 2. gather needed data for external use. ex: remainder weights, priorities, originalBudget + * 3. run each priority level that is needed via runTemplatesForPriority + * 4. run applyLimits to apply any existing limit to the category + * 5. run the remainder templates via runRemainder (limits get applied at the start of this) + * 6. finish processing by running runFinish() + * Alternate: + * If the situation calls for it you can run all templates in a catagory in one go using the + * method runAll which will run all templates and goals and save the amounts. + */ + + //----------------------------------------------------------------------------- + // Class interface + // returns the total remainder weight of remainder templates in this category - get remainderWeight(){ return this.remainderWeight; } + get remainderWeight(): number { + return this.remainderWeight; + } // return the current budgeted amount in the category - get startingBalance(){ return this.startingBalance; } + get originalBudget(): number { + return this.originalBudget; + } // returns a list of priority levels in this category - get priorities(){ return this.priorities; } - + get priorities(): number[] { + return this.priorities; + } + + // get messages that were generated during construction + readMsgs() { + return this.msgs; + } + // what is the full requested amount this month - runAll(force: boolean, available: number){ - let toBudget; - this.priorities.forEach(p => { - toBudget += this.runTemplatesForPriority(p, available, force); + runAll(force: boolean, available: number) { + let toBudget: number = 0; + this.priorities.forEach(async p => { + toBudget += await this.runTemplatesForPriority(p, available, force); }); + //TODO does this need to run limits? return toBudget; - } + } // run all templates in a given priority level // return: amount budgeted in this priority level - async runTemplatesForPriority(priority: number, budgetAvail: number, force: boolean){ + async runTemplatesForPriority( + priority: number, + budgetAvail: number, + force: boolean, + ) { const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail; let toBudget = 0; // switch on template type and calculate the amount for the line - for(let i = 0; i < t.length; i++) { + for (let i = 0; i < t.length; i++) { switch (t[i].type) { case 'simple': { - toBudget += this.runSimple(t,this.limitAmount); + toBudget += this.runSimple(t, this.limitAmount); break; } - case 'copy' : { + case 'copy': { toBudget += await this.runCopy(t); break; } @@ -66,11 +84,11 @@ class categoryTemplate { break; } case 'spend': { - toBudget +-this.runSpend(t); + toBudget + -this.runSpend(t); break; } case 'percentage': { - toBudget += await this.runPercentage(t, budgetAvail); + toBudget += await this.runPercentage(t, budgetAvail); break; } case 'by': { @@ -88,80 +106,96 @@ class categoryTemplate { } // don't overbudget when using a priority - if(priority > 0 && force === false &&toBudget>available){ - toBudget=available; + if (priority > 0 && force === false && toBudget > available) { + toBudget = available; } available = available - toBudget; - if(available <= 0 && force === false) { break; } + if (available <= 0 && force === false) { + break; + } } this.toBudgetAmount += toBudget; return toBudget; } - applyLimit(){ - if(this.limitCheck === false){ + applyLimit() { + if (this.limitCheck === false) { this.toBudgetAmount = this.targetAmount; - return + return; } - if(this.limitHold && this.fromLastMonth >= this.limitAmount){ + if (this.limitHold && this.fromLastMonth >= this.limitAmount) { this.toBudgetAmount = 0; - return + return; } - if(this.targetAmount>this.limitAmount){ + if (this.targetAmount > this.limitAmount) { this.toBudgetAmount = this.limitAmount - this.fromLastMonth; } + //TODO return the value removed by the limits } // run all of the 'remainder' type templates - runRemainder(budgetAvail: number, perWeight: number){ - const toBudget = Math.round(this.remainderWeight*perWeight); + runRemainder(budgetAvail: number, perWeight: number) { + const toBudget = Math.round(this.remainderWeight * perWeight); //check possible overbudget from rounding, 1cent leftover - if(toBudget > budgetAvail){ + if (toBudget > budgetAvail) { this.toBudgetAmount += budgetAvail; - } else if (budgetAvail - toBudget === 1){ - this.toBudgetAmount+=toBudget + 1; + } else if (budgetAvail - toBudget === 1) { + this.toBudgetAmount += toBudget + 1; } else { - this.toBudgetAmount+=toBudget; + this.toBudgetAmount += toBudget; } + return toBudget; } - runFinish(){ - this.runGoal() + runFinish() { + this.runGoal(); this.setBudget(); this.setGoal(); } - -//----------------------------------------------------------------------------- -// Implimentation + //----------------------------------------------------------------------------- + // Implimentation readonly categoryID: string; - private month; + private month: string; private templates = []; private remainder = []; private goals = []; - private toBudgetAmount = 0; // amount that will be budgeted by the templates - private targetAmount = 0; - private isLongGoal = false; // is the goal type a long goal - private goalAmount = 0; + private toBudgetAmount:number = null; // amount that will be budgeted by the templates + private targetAmount:number = null; + private isLongGoal:boolean = null; //defaulting the goals to null so templates can be unset + private goalAmount:number = null; private spentThisMonth = 0; // amount already spend this month private fromLastMonth = 0; // leftover from last month private limitAmount = 0; private limitCheck = false; private limitHold = false; + private msgs: string[]; - private set priorities(value){this.priorities=value} - private set remainderWeight(value){this.remainderWeight=value} - private set startingBalance(value){this.startingBalance=value} + private set priorities(value) { + this.priorities = value; + } + private set remainderWeight(value) { + this.remainderWeight = value; + } + private set originalBudget(value) { + this.originalBudget = value; + } - constructor(templates, categoryID: string, month) { + constructor(templates, categoryID: string, month: string) { this.categoryID = categoryID; this.month = month; // sort the template lines into regular template, goals, and remainder templates - this.templates = templates.filter(t => {t.directive === 'template' && t.type != 'remainder'}); - this.remainder = templates.filter(t =>{t.directive === 'template' && t.type === 'remainder'}); - this.goals = templates.filter(t => {t.directive === 'goal'}); + this.templates = templates.filter(t => { + t.directive === 'template' && t.type != 'remainder'; + }); + this.remainder = templates.filter(t => { + t.directive === 'template' && t.type === 'remainder'; + }); + this.goals = templates.filter(t => { + t.directive === 'goal'; + }); //check templates and throw exception if there is something wrong this.checkTemplates(); //find priorities @@ -172,47 +206,54 @@ class categoryTemplate { this.readBudgeted(); //get the from last month amount this.readSpent(); - this.calcLeftover(); + this.getFromLastMonth(); } - - private findPriorities(){ - let p = []; + + private findPriorities() { + const p = []; this.templates.forEach(t => { - if(t.priority != null){ - p.push(t.priority) + if (t.priority != null) { + p.push(t.priority); } }); //sort and reduce to unique items - this.priorities = p.sort(function (a,b) { - return a-b; - }) - .filter((item, idx, curr) => curr.indexOf(item) === idx); + this.priorities = p + .sort(function (a, b) { + return a - b; + }) + .filter((item, idx, curr) => curr.indexOf(item) === idx); } - private findRemainderWeightSum(){ + private findRemainderWeightSum() { let weight = 0; - this.remainder.forEach(r => {weight+=r.weight}); + this.remainder.forEach(r => { + weight += r.weight; + }); this.remainderWeight = weight; } - - private checkTemplates(){ + + private checkTemplates() { //run all the individual checks this.checkByAndSchedule(); this.checkPercentage(); this.checkLimit(); - this.checkGoal(); } - private runGoal(){ - if(this.goals.length>0) { - this.isLongGoal = true; + private runGoal() { + if (this.goals.length > 0) { + this.isLongGoal = true; this.goalAmount = amountToInteger(this.goals[0].amount); - return + return; + } + if (this.goals.length > 1) { + this.msgs.push( + 'Can only have one #goal per category. Using the first found', + ); } this.goalAmount = this.toBudgetAmount; } - private setBudget(){ + private setBudget() { setBudget({ category: this.categoryID, month: this.month, @@ -220,14 +261,14 @@ class categoryTemplate { }); } - private async readBudgeted(){ - return await getSheetValue( + private async readBudgeted() { + this.originalBudget = await getSheetValue( monthUtils.sheetForMonth(this.month), `budget-${this.categoryID}`, ); } - private setGoal(){ + private setGoal() { setGoal({ category: this.categoryID, goal: this.goalAmount, @@ -236,113 +277,128 @@ class categoryTemplate { }); } - private readSpent(){ - //TODO + private async readSpent() { + const sheetName = monthUtils.sheetForMonth(this.month); + this.spentThisMonth = await getSheetValue( + sheetName, + `sum-amount-${this.categoryID}`, + ); } - private calcLeftover(){ - //TODO + private async getFromLastMonth() { + const sheetName = monthUtils.sheetForMonth( + monthUtils.subMonths(this.month, 1), + ); + //TODO see if this is accurate from the sheet for the balance last month + this.fromLastMonth = await getSheetValue( + sheetName, + `leftover-${this.categoryID}`, + ); } -//----------------------------------------------------------------------------- -// Template Validation - private async checkByAndSchedule(){ + //----------------------------------------------------------------------------- + // Template Validation + private async checkByAndSchedule() { //check schedule names - const scheduleNames = (await getActiveSchedules()).map(({name}) => name); + const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); this.templates - .filter(t => t.type === 'schedule') - .forEach(t => { - if (!scheduleNames.includes(t.name)) { - throw new Error(`Schedule "${t.name}" does not exist`); - } - }); + .filter(t => t.type === 'schedule') + .forEach(t => { + if (!scheduleNames.includes(t.name)) { + throw new Error(`Schedule "${t.name}" does not exist`); + } + }); //find lowest priority let lowestPriority = null; this.templates - .filter(t=> t.type === 'schedule' || t.type === 'by' ) - .forEach(t => { - if(lowestPriority === null || t.priority < lowestPriority){ - lowestPriority = t.priority; - } - }); + .filter(t => t.type === 'schedule' || t.type === 'by') + .forEach(t => { + if (lowestPriority === null || t.priority < lowestPriority) { + lowestPriority = t.priority; + } + }); //set priority to needed value this.templates - .filter(t => t.type === 'schedule' || t.type === 'by') - .forEach(t => t.priority = lowestPriority); + .filter(t => t.type === 'schedule' || t.type === 'by') + .forEach(t => { + if (t.priority != lowestPriority) { + t.priority = lowestPriority; + this.msgs.push( + `Changed the priority of BY and SCHEDULE templates to be ${lowestPriority}`, + ); + } + }); + //TODO add a message that the priority has been changed } - private checkLimit(){ - for(let i = 0; i < this.templates.length; i++){ + private checkLimit() { + for (let i = 0; i < this.templates.length; i++) { const t = this.templates[i]; - if(this.limitCheck){ + if (this.limitCheck) { throw new Error('Only one `up to` allowed per category'); break; - } else if (t.limit!=null){ + } else if (t.limit != null) { this.limitCheck = true; this.limitHold = t.limit.hold ? true : false; this.limitAmount = amountToInteger(t.limit.amount); } - }; + } } - - private async checkPercentage(){ - const pt = this.templates.filter(t => t.type === 'percentage'); - let reqCategories = []; - pt.forEach(t => reqCategories.push(t.category)); + + private async checkPercentage() { + const pt = this.templates.filter(t => t.type === 'percentage'); + const reqCategories = []; + pt.forEach(t => reqCategories.push(t.category.toLowerCase())); const availCategories = await db.getCategories(); - let availNames = []; + const availNames = []; availCategories.forEach(c => { - if(c.is_income){ + if (c.is_income) { availNames.push(c.name.toLowerCase()); } }); reqCategories.forEach(n => { - if(n === 'availble funds' || n === 'all income'){ + if (n === 'availble funds' || n === 'all income') { //skip the name check since these are special } else if (!availNames.includes(n)) { - throw new Error(`Category ${n} is not found in available income categories`); + throw new Error( + `Category ${n} is not found in available income categories`, + ); } }); } - private checkGoal(){ - if(this.goals.length>1){ - throw new Error('Can only have one #goal per category'); - } - } - -//----------------------------------------------------------------------------- -// Processor Functions + //----------------------------------------------------------------------------- + // Processor Functions - private runSimple(template, limit){ + private runSimple(template, limit) { let toBudget = 0; if (template.monthly != null) { - toBudget= amountToInteger(template.monthly); + toBudget = amountToInteger(template.monthly); } else { toBudget = limit; } return toBudget; } - private async runCopy(template){ + private async runCopy(template) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month,template.lookback), + monthUtils.subMonths(this.month, template.lookback), ); return await getSheetValue(sheetName, `budget-${this.categoryID}`); } - private runWeek(template){ + private runWeek(template) { let toBudget = 0; const amount = amountToInteger(template.amount); const weeks = template.weeks != null ? Math.round(template.weeks) : 1; let w = template.starting; - const nextMonth = monthUtils.addMonths(this.month,1); + const nextMonth = monthUtils.addMonths(this.month, 1); - while (w < nextMonth){ - if( w >= this.month) { + while (w < nextMonth) { + if (w >= this.month) { toBudget += amount; } w = monthUtils.addWeeks(w, weeks); @@ -350,7 +406,7 @@ class categoryTemplate { return toBudget; } - private async runSpend(template){ + private async runSpend(template) { const fromMonth = `${template.from}`; const toMonth = `${template.month}`; let alreadyBudgeted = this.fromLastMonth; @@ -359,74 +415,89 @@ class categoryTemplate { for ( let m = fromMonth; monthUtils.differenceInCalendarMonths(this.month, m) > 0; - m = monthUtils.addMonths(m,1) + m = monthUtils.addMonths(m, 1) ) { const sheetName = monthUtils.sheetForMonth(m); - if(firstMonth) { + if (firstMonth) { //TODO figure out if I already found these values and can pass them in - const spent = await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); - const balance = await getSheetValue(sheetName, `leftover-${this.categoryID}`); + const spent = await getSheetValue( + sheetName, + `sum-amount-${this.categoryID}`, + ); + const balance = await getSheetValue( + sheetName, + `leftover-${this.categoryID}`, + ); alreadyBudgeted = balance - spent; firstMonth = false; } else { - alreadyBudgeted += await getSheetValue(sheetName, `budget-${this.categoryID}`); + alreadyBudgeted += await getSheetValue( + sheetName, + `budget-${this.categoryID}`, + ); } } - const numMonths = monthUtils.differenceInCalendarMonths(toMonth, this.month); + const numMonths = monthUtils.differenceInCalendarMonths( + toMonth, + this.month, + ); const target = amountToInteger(template.amount); - if(numMonths < 0){ - return 0; + if (numMonths < 0) { + return 0; } else { - return Math.round((target-alreadyBudgeted) /(numMonths + 1)); + return Math.round((target - alreadyBudgeted) / (numMonths + 1)); } } - private async runPercentage(template, availableFunds){ + private async runPercentage(template, availableFunds) { const percent = template.percent; const cat = template.category.toLowerCase(); const prev = template.previous; let sheetName; let monthlyIncome = 0; - + //choose the sheet to find income for - if(prev){ - sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, 1) - ); + if (prev) { + sheetName = monthUtils.sheetForMonth(monthUtils.subMonths(this.month, 1)); } else { sheetName = monthUtils.sheetForMonth(this.month); } - if(cat === 'all income'){ - monthlyIncome = await getSheetValue(sheetName,`total-income`); + if (cat === 'all income') { + monthlyIncome = await getSheetValue(sheetName, `total-income`); } else if (cat === 'available funds') { monthlyIncome = availableFunds; } else { - const incomeCat = (await db.getCategories()).find(c=> c.is_income && c.name.toLowerCase() === cat); - monthlyIncome = await getSheetValue(sheetName, `sum_amount-${incomeCat.id}`); + const incomeCat = (await db.getCategories()).find( + c => c.is_income && c.name.toLowerCase() === cat, + ); + monthlyIncome = await getSheetValue( + sheetName, + `sum_amount-${incomeCat.id}`, + ); } - - return Math.max(0, Math.round(monthlyIncome * (percent /100))); - } - - private runBy(template_lines){ - //TODO - return 0; - } - private runSchedule(template_lines){ - //TODO - return 0; + return Math.max(0, Math.round(monthlyIncome * (percent / 100))); } - private async runAverage(template){ + private async runAverage(template) { let sum = 0; for (let i = 1; i <= template.numMonths; i++) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, i) + monthUtils.subMonths(this.month, i), ); sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); } return -Math.round(sum / template.amount); } + + private runBy(template_lines) { + //TODO + return 0; + } + + private runSchedule(template_lines) { + //TODO + return 0; + } } diff --git a/packages/loot-core/src/server/budget/goals/goalsSimple.ts b/packages/loot-core/src/server/budget/goals/goalsSimple.ts index c0af20511f2..05d3a112f21 100644 --- a/packages/loot-core/src/server/budget/goals/goalsSimple.ts +++ b/packages/loot-core/src/server/budget/goals/goalsSimple.ts @@ -1,10 +1,7 @@ // @ts-strict-ignore import { amountToInteger } from '../../../shared/util'; -export async function goalsSimple( - template, - limit, -) { +export async function goalsSimple(template, limit) { // simple has 'monthly' and/or 'limit' params let increment = 0; if (template.monthly != null) { @@ -13,6 +10,5 @@ export async function goalsSimple( } else { increment = limit; } - to_budget += increment; - return { to_budget }; + return { increment }; } diff --git a/packages/loot-core/src/server/budget/types/templates.d.ts b/packages/loot-core/src/server/budget/types/templates.d.ts index 8b6efda780d..b377108e0e0 100644 --- a/packages/loot-core/src/server/budget/types/templates.d.ts +++ b/packages/loot-core/src/server/budget/types/templates.d.ts @@ -63,6 +63,11 @@ interface GoalTemplate extends BaseTemplate { amount: number; } +interface CopyTemplate extends BaseTemplate { + type: 'copy'; + lookBack: number; +} + interface ErrorTemplate extends BaseTemplate { type: 'error'; line: string; @@ -79,4 +84,5 @@ export type Template = | RemainderTemplate | AverageTemplate | GoalTemplate + | CopyTemplate | ErrorTemplate; From 5a28390cb8e9230ef098793c261527fa83b5fa16 Mon Sep 17 00:00:00 2001 From: youngcw Date: Tue, 29 Oct 2024 19:04:16 -0700 Subject: [PATCH 04/31] very basic testing --- .../src/server/budget/categoryTemplate.ts | 470 +++++++----- .../src/server/budget/goals/goalsSimple.ts | 8 +- .../src/server/budget/goaltemplates.ts | 683 +++--------------- .../src/server/budget/types/templates.d.ts | 8 +- 4 files changed, 379 insertions(+), 790 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index b75b8d71761..949f17b2b41 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -1,167 +1,202 @@ // @ts-strict-ignore import * as monthUtils from '../../shared/months'; +import { amountToInteger } from '../../shared/util'; import * as db from '../db'; + import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; -import { amountToInteger } from '../../shared/util'; -import {getActiveSchedules} from './statements'; - -class categoryTemplate { -/*---------------------------------------------------------------------------- - * Using This Class: - * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; - * categoryID: the ID of the category that this Class will be for - * templates: all templates for this category (including templates and goals) - * month: the month string of the month for templates being applied - * 2. gather needed data for external use. ex: remainder weights, priorities, startingBalance - * 3. run each priority level that is needed via runTemplatesForPriority - * 4. run applyLimits to apply any existing limit to the category - * 5. run the remainder templates via runRemainder (limits get applied at the start of this) - * 6. finish processing by running runFinish() - * Alternate: - * If the situation calls for it you can run all templates in a catagory in one go using the - * method runAll which will run all templates and goals and save the amounts. - */ - -//----------------------------------------------------------------------------- -// Class interface - +import { getActiveSchedules } from './statements'; +import { Template } from './types/templates'; + +export class categoryTemplate { + /*---------------------------------------------------------------------------- + * Using This Class: + * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; + * categoryID: the ID of the category that this Class will be for + * templates: all templates for this category (including templates and goals) + * month: the month string of the month for templates being applied + * 2. gather needed data for external use. ex: remainder weights, priorities, originalBudget + * 3. run each priority level that is needed via runTemplatesForPriority + * 4. run applyLimits to apply any existing limit to the category + * 5. run the remainder templates via runRemainder (limits get applied at the start of this) + * 6. finish processing by running runFinish() + * Alternate: + * If the situation calls for it you can run all templates in a catagory in one go using the + * method runAll which will run all templates and goals for reference, and can optionally be saved + */ + + //----------------------------------------------------------------------------- + // Class interface + // returns the total remainder weight of remainder templates in this category - get remainderWeight(){ return this.remainderWeight; } + getRemainderWeight(): number { + return this.remainderWeight; + } // return the current budgeted amount in the category - get startingBalance(){ return this.startingBalance; } + getOriginalBudget(): number { + return this.originalBudget; + } // returns a list of priority levels in this category - get priorities(){ return this.priorities; } - + readPriorities(): number[] { + return this.priorities; + } + + // get messages that were generated during construction + readMsgs() { + return this.msgs; + } + // what is the full requested amount this month - runAll(force: boolean, available: number){ - let toBudget; - this.priorities.forEach(p => { - toBudget += this.runTemplatesForPriority(p, available, force); + runAll(force: boolean, available: number) { + let toBudget: number = 0; + this.priorities.forEach(async p => { + toBudget += await this.runTemplatesForPriority(p, available, force); }); + //TODO does this need to run limits? return toBudget; - } + } // run all templates in a given priority level // return: amount budgeted in this priority level - async runTemplatesForPriority(priority: number, budgetAvail: number, force: boolean){ + async runTemplatesForPriority( + priority: number, + budgetAvail: number, + force: boolean, + ) { const t = this.templates.filter(t => t.priority === priority); - let available = budgetAvail; + let available = budgetAvail || 0; let toBudget = 0; // switch on template type and calculate the amount for the line - for(let i = 0; i < t.length; i++) { + for (let i = 0; i < t.length; i++) { switch (t[i].type) { case 'simple': { - toBudget += this.runSimple(t,this.limitAmount); + toBudget += this.runSimple(t[i], this.limitAmount); break; } - case 'copy' : { - toBudget += await this.runCopy(t); + case 'copy': { + toBudget += await this.runCopy(t[i]); break; } case 'week': { - toBudget += this.runWeek(t); + toBudget += this.runWeek(t[i]); break; } case 'spend': { - toBudget +-this.runSpend(t); + toBudget + -this.runSpend(t[i]); break; } case 'percentage': { - toBudget += await this.runPercentage(t, budgetAvail); + toBudget += await this.runPercentage(t[i], budgetAvail); break; } case 'by': { - toBudget += this.runBy(t); + //TODO add the logic to run all of these at once + toBudget += this.runBy(t[i], this.templates, 0, budgetAvail); break; } case 'schedule': { - toBudget += this.runSchedule(t); + toBudget += this.runSchedule(t[i]); break; } case 'average': { - toBudget += await this.runAverage(t); + toBudget += await this.runAverage(t[i]); break; } } // don't overbudget when using a priority - if(priority > 0 && force === false &&toBudget>available){ - toBudget=available; + //TODO fix the usage of force here. Its wrong + if (priority > 0 && force === false && toBudget > available) { + toBudget = available; } available = available - toBudget; - if(available <= 0 && force === false) { break; } + if (available <= 0 && force === false) { + break; + } } this.toBudgetAmount += toBudget; return toBudget; } - applyLimit(){ - if(this.limitCheck === false){ + applyLimit() { + if (this.limitCheck === false) { this.toBudgetAmount = this.targetAmount; - return + return; } - if(this.limitHold && this.fromLastMonth >= this.limitAmount){ + if (this.limitHold && this.fromLastMonth >= this.limitAmount) { this.toBudgetAmount = 0; - return + return; } - if(this.targetAmount>this.limitAmount){ + if (this.targetAmount > this.limitAmount) { this.toBudgetAmount = this.limitAmount - this.fromLastMonth; } + //TODO return the value removed by the limits } // run all of the 'remainder' type templates - runRemainder(budgetAvail: number, perWeight: number){ - const toBudget = Math.round(this.remainderWeight*perWeight); + runRemainder(budgetAvail: number, perWeight: number) { + const toBudget = Math.round(this.remainderWeight * perWeight); //check possible overbudget from rounding, 1cent leftover - if(toBudget > budgetAvail){ + if (toBudget > budgetAvail) { this.toBudgetAmount += budgetAvail; - } else if (budgetAvail - toBudget === 1){ - this.toBudgetAmount+=toBudget + 1; + } else if (budgetAvail - toBudget === 1) { + this.toBudgetAmount += toBudget + 1; } else { - this.toBudgetAmount+=toBudget; + this.toBudgetAmount += toBudget; } + return toBudget; } - runFinish(){ - this.runGoal() + runFinish() { + this.runGoal(); this.setBudget(); this.setGoal(); } - -//----------------------------------------------------------------------------- -// Implimentation + //----------------------------------------------------------------------------- + // Implimentation readonly categoryID: string; - private month; + private month: string; private templates = []; private remainder = []; private goals = []; - private toBudgetAmount = 0; // amount that will be budgeted by the templates - private targetAmount = 0; - private isLongGoal = false; // is the goal type a long goal - private goalAmount = 0; + private priorities: number[] = []; + private remainderWeight: number = 0; + private originalBudget: number = 0; + private toBudgetAmount: number = null; // amount that will be budgeted by the templates + private targetAmount: number = null; + private isLongGoal: boolean = null; //defaulting the goals to null so templates can be unset + private goalAmount: number = null; private spentThisMonth = 0; // amount already spend this month private fromLastMonth = 0; // leftover from last month - private limitAmount = 0; + private limitAmount = null; private limitCheck = false; private limitHold = false; + private msgs: string[]; - private set priorities(value){this.priorities=value} - private set remainderWeight(value){this.remainderWeight=value} - private set startingBalance(value){this.startingBalance=value} - - constructor(templates, categoryID: string, month) { + constructor(templates, categoryID: string, month: string) { this.categoryID = categoryID; this.month = month; // sort the template lines into regular template, goals, and remainder templates - this.templates = templates.filter(t => {t.directive === 'template' && t.type != 'remainder'}); - this.remainder = templates.filter(t =>{t.directive === 'template' && t.type === 'remainder'}); - this.goals = templates.filter(t => {t.directive === 'goal'}); + if (templates) { + templates.forEach(t => { + if (t.directive === 'template' && t.type != 'remainder') { + this.templates.push(t); + } + }); + templates.forEach(t => { + if (t.directive === 'template' && t.type === 'remainder') { + this.remainder.push(t); + } + }); + templates.forEach(t => { + if (t.directive === 'goal') this.goals.push(t); + }); + } //check templates and throw exception if there is something wrong this.checkTemplates(); //find priorities @@ -172,47 +207,54 @@ class categoryTemplate { this.readBudgeted(); //get the from last month amount this.readSpent(); - this.calcLeftover(); + this.getFromLastMonth(); } - - private findPriorities(){ - let p = []; + + private findPriorities() { + const p = []; this.templates.forEach(t => { - if(t.priority != null){ - p.push(t.priority) + if (t.priority != null) { + p.push(t.priority); } }); //sort and reduce to unique items - this.priorities = p.sort(function (a,b) { - return a-b; - }) - .filter((item, idx, curr) => curr.indexOf(item) === idx); + this.priorities = p + .sort(function (a, b) { + return a - b; + }) + .filter((item, idx, curr) => curr.indexOf(item) === idx); } - private findRemainderWeightSum(){ + private findRemainderWeightSum() { let weight = 0; - this.remainder.forEach(r => {weight+=r.weight}); + this.remainder.forEach(r => { + weight += r.weight; + }); this.remainderWeight = weight; } - - private checkTemplates(){ + + private checkTemplates() { //run all the individual checks this.checkByAndSchedule(); this.checkPercentage(); this.checkLimit(); - this.checkGoal(); } - private runGoal(){ - if(this.goals.length>0) { - this.isLongGoal = true; + private runGoal() { + if (this.goals.length > 0) { + this.isLongGoal = true; this.goalAmount = amountToInteger(this.goals[0].amount); - return + return; + } + if (this.goals.length > 1) { + this.msgs.push( + 'Can only have one #goal per category. Using the first found', + ); } this.goalAmount = this.toBudgetAmount; } - private setBudget(){ + private setBudget() { setBudget({ category: this.categoryID, month: this.month, @@ -220,14 +262,14 @@ class categoryTemplate { }); } - private async readBudgeted(){ - return await getSheetValue( + private async readBudgeted() { + this.originalBudget = await getSheetValue( monthUtils.sheetForMonth(this.month), `budget-${this.categoryID}`, ); } - private setGoal(){ + private setGoal() { setGoal({ category: this.categoryID, goal: this.goalAmount, @@ -236,113 +278,128 @@ class categoryTemplate { }); } - private readSpent(){ - //TODO + private async readSpent() { + const sheetName = monthUtils.sheetForMonth(this.month); + this.spentThisMonth = await getSheetValue( + sheetName, + `sum-amount-${this.categoryID}`, + ); } - private calcLeftover(){ - //TODO + private async getFromLastMonth() { + const sheetName = monthUtils.sheetForMonth( + monthUtils.subMonths(this.month, 1), + ); + //TODO see if this is accurate from the sheet for the balance last month + this.fromLastMonth = await getSheetValue( + sheetName, + `leftover-${this.categoryID}`, + ); } -//----------------------------------------------------------------------------- -// Template Validation - private async checkByAndSchedule(){ + //----------------------------------------------------------------------------- + // Template Validation + private async checkByAndSchedule() { //check schedule names - const scheduleNames = (await getActiveSchedules()).map(({name}) => name); + const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); this.templates - .filter(t => t.type === 'schedule') - .forEach(t => { - if (!scheduleNames.includes(t.name)) { - throw new Error(`Schedule "${t.name}" does not exist`); - } - }); + .filter(t => t.type === 'schedule') + .forEach(t => { + if (!scheduleNames.includes(t.name.trim())) { + throw new Error(`Schedule "${t.name.trim()}" does not exist`); + } + }); //find lowest priority let lowestPriority = null; this.templates - .filter(t=> t.type === 'schedule' || t.type === 'by' ) - .forEach(t => { - if(lowestPriority === null || t.priority < lowestPriority){ - lowestPriority = t.priority; - } - }); + .filter(t => t.type === 'schedule' || t.type === 'by') + .forEach(t => { + if (lowestPriority === null || t.priority < lowestPriority) { + lowestPriority = t.priority; + } + }); //set priority to needed value this.templates - .filter(t => t.type === 'schedule' || t.type === 'by') - .forEach(t => t.priority = lowestPriority); + .filter(t => t.type === 'schedule' || t.type === 'by') + .forEach(t => { + if (t.priority != lowestPriority) { + t.priority = lowestPriority; + this.msgs.push( + `Changed the priority of BY and SCHEDULE templates to be ${lowestPriority}`, + ); + } + }); + //TODO add a message that the priority has been changed } - private checkLimit(){ - for(let i = 0; i < this.templates.length; i++){ + private checkLimit() { + for (let i = 0; i < this.templates.length; i++) { const t = this.templates[i]; - if(this.limitCheck){ + if (this.limitCheck) { throw new Error('Only one `up to` allowed per category'); break; - } else if (t.limit!=null){ + } else if (t.limit != null) { this.limitCheck = true; this.limitHold = t.limit.hold ? true : false; this.limitAmount = amountToInteger(t.limit.amount); } - }; + } } - - private async checkPercentage(){ - const pt = this.templates.filter(t => t.type === 'percentage'); - let reqCategories = []; - pt.forEach(t => reqCategories.push(t.category)); + + private async checkPercentage() { + const pt = this.templates.filter(t => t.type === 'percentage'); + const reqCategories = []; + pt.forEach(t => reqCategories.push(t.category.toLowerCase())); const availCategories = await db.getCategories(); - let availNames = []; + const availNames = []; availCategories.forEach(c => { - if(c.is_income){ + if (c.is_income) { availNames.push(c.name.toLowerCase()); } }); reqCategories.forEach(n => { - if(n === 'availble funds' || n === 'all income'){ + if (n === 'availble funds' || n === 'all income') { //skip the name check since these are special } else if (!availNames.includes(n)) { - throw new Error(`Category ${n} is not found in available income categories`); + throw new Error( + `Category ${n} is not found in available income categories`, + ); } }); } - private checkGoal(){ - if(this.goals.length>1){ - throw new Error('Can only have one #goal per category'); - } - } - -//----------------------------------------------------------------------------- -// Processor Functions + //----------------------------------------------------------------------------- + // Processor Functions - private runSimple(template, limit){ + private runSimple(template, limit) { let toBudget = 0; if (template.monthly != null) { - toBudget= amountToInteger(template.monthly); + toBudget = amountToInteger(template.monthly); } else { - toBudget = limit; + toBudget = limit || 0; } return toBudget; } - private async runCopy(template){ + private async runCopy(template) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month,template.lookback), + monthUtils.subMonths(this.month, template.lookback), ); return await getSheetValue(sheetName, `budget-${this.categoryID}`); } - private runWeek(template){ + private runWeek(template) { let toBudget = 0; const amount = amountToInteger(template.amount); const weeks = template.weeks != null ? Math.round(template.weeks) : 1; let w = template.starting; - const nextMonth = monthUtils.addMonths(this.month,1); + const nextMonth = monthUtils.addMonths(this.month, 1); - while (w < nextMonth){ - if( w >= this.month) { + while (w < nextMonth) { + if (w >= this.month) { toBudget += amount; } w = monthUtils.addWeeks(w, weeks); @@ -350,7 +407,7 @@ class categoryTemplate { return toBudget; } - private async runSpend(template){ + private async runSpend(template) { const fromMonth = `${template.from}`; const toMonth = `${template.month}`; let alreadyBudgeted = this.fromLastMonth; @@ -359,74 +416,113 @@ class categoryTemplate { for ( let m = fromMonth; monthUtils.differenceInCalendarMonths(this.month, m) > 0; - m = monthUtils.addMonths(m,1) + m = monthUtils.addMonths(m, 1) ) { const sheetName = monthUtils.sheetForMonth(m); - if(firstMonth) { + if (firstMonth) { //TODO figure out if I already found these values and can pass them in - const spent = await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); - const balance = await getSheetValue(sheetName, `leftover-${this.categoryID}`); + const spent = await getSheetValue( + sheetName, + `sum-amount-${this.categoryID}`, + ); + const balance = await getSheetValue( + sheetName, + `leftover-${this.categoryID}`, + ); alreadyBudgeted = balance - spent; firstMonth = false; } else { - alreadyBudgeted += await getSheetValue(sheetName, `budget-${this.categoryID}`); + alreadyBudgeted += await getSheetValue( + sheetName, + `budget-${this.categoryID}`, + ); } } - const numMonths = monthUtils.differenceInCalendarMonths(toMonth, this.month); + const numMonths = monthUtils.differenceInCalendarMonths( + toMonth, + this.month, + ); const target = amountToInteger(template.amount); - if(numMonths < 0){ - return 0; + if (numMonths < 0) { + return 0; } else { - return Math.round((target-alreadyBudgeted) /(numMonths + 1)); + return Math.round((target - alreadyBudgeted) / (numMonths + 1)); } } - private async runPercentage(template, availableFunds){ + private async runPercentage(template, availableFunds) { const percent = template.percent; const cat = template.category.toLowerCase(); const prev = template.previous; let sheetName; let monthlyIncome = 0; - + //choose the sheet to find income for - if(prev){ - sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, 1) - ); + if (prev) { + sheetName = monthUtils.sheetForMonth(monthUtils.subMonths(this.month, 1)); } else { sheetName = monthUtils.sheetForMonth(this.month); } - if(cat === 'all income'){ - monthlyIncome = await getSheetValue(sheetName,`total-income`); + if (cat === 'all income') { + monthlyIncome = await getSheetValue(sheetName, `total-income`); } else if (cat === 'available funds') { monthlyIncome = availableFunds; } else { - const incomeCat = (await db.getCategories()).find(c=> c.is_income && c.name.toLowerCase() === cat); - monthlyIncome = await getSheetValue(sheetName, `sum_amount-${incomeCat.id}`); + const incomeCat = (await db.getCategories()).find( + c => c.is_income && c.name.toLowerCase() === cat, + ); + monthlyIncome = await getSheetValue( + sheetName, + `sum_amount-${incomeCat.id}`, + ); } - - return Math.max(0, Math.round(monthlyIncome * (percent /100))); - } - - private runBy(template_lines){ - //TODO - return 0; - } - private runSchedule(template_lines){ - //TODO - return 0; + return Math.max(0, Math.round(monthlyIncome * (percent / 100))); } - private async runAverage(template){ + private async runAverage(template) { let sum = 0; for (let i = 1; i <= template.numMonths; i++) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, i) + monthUtils.subMonths(this.month, i), ); sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); } return -Math.round(sum / template.amount); } + + private runBy(template, allTemplates, l: number, remainder: number) { + let target = 0; + let targetMonth = `${allTemplates[l].month}`; + let numMonths = monthUtils.differenceInCalendarMonths( + targetMonth, + this.month, + ); + const repeat = + template.type === 'by' ? template.repeat : (template.repeat || 1) * 12; + while (numMonths < 0 && repeat) { + targetMonth = monthUtils.addMonths(targetMonth, repeat); + numMonths = monthUtils.differenceInCalendarMonths( + allTemplates[l].month, + this.month, + ); + } + if (l === 0) remainder = this.fromLastMonth; + remainder = amountToInteger(allTemplates[l].amount) - remainder; + if (remainder >= 0) { + target = remainder; + remainder = 0; + } else { + target = 0; + remainder = Math.abs(remainder); + } + return numMonths >= 0 ? Math.round(target / (numMonths + 1)) : 0; + } + + private runSchedule(template_lines) { + //TODO add this...... + //TODO remember to trim the schedule name + return 0; + } } diff --git a/packages/loot-core/src/server/budget/goals/goalsSimple.ts b/packages/loot-core/src/server/budget/goals/goalsSimple.ts index c0af20511f2..05d3a112f21 100644 --- a/packages/loot-core/src/server/budget/goals/goalsSimple.ts +++ b/packages/loot-core/src/server/budget/goals/goalsSimple.ts @@ -1,10 +1,7 @@ // @ts-strict-ignore import { amountToInteger } from '../../../shared/util'; -export async function goalsSimple( - template, - limit, -) { +export async function goalsSimple(template, limit) { // simple has 'monthly' and/or 'limit' params let increment = 0; if (template.monthly != null) { @@ -13,6 +10,5 @@ export async function goalsSimple( } else { increment = limit; } - to_budget += increment; - return { to_budget }; + return { increment }; } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 516218496e8..fcf9724a6c6 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -1,57 +1,46 @@ // @ts-strict-ignore import { Notification } from '../../client/state-types/notifications'; -import * as monthUtils from '../../shared/months'; -import { amountToInteger, integerToAmount } from '../../shared/util'; import * as db from '../db'; import { batchMessages } from '../sync'; -import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; -import { goalsAverage } from './goals/goalsAverage'; -import { goalsBy } from './goals/goalsBy'; -import { goalsCopy } from './goals/goalsCopy'; -import { goalsPercentage } from './goals/goalsPercentage'; -import { findRemainder, goalsRemainder } from './goals/goalsRemainder'; -import { goalsSchedule } from './goals/goalsSchedule'; -import { goalsSimple } from './goals/goalsSimple'; -import { goalsSpend } from './goals/goalsSpend'; -import { goalsWeek } from './goals/goalsWeek'; +import { isReflectBudget } from './actions'; +import { categoryTemplate } from './categoryTemplate'; import { checkTemplates, storeTemplates } from './template-notes'; -const TEMPLATE_PREFIX = '#template'; - export async function applyTemplate({ month }) { - await storeTemplates(); - const category_templates = await getTemplates(null, 'template'); - const category_goals = await getTemplates(null, 'goal'); - const ret = await processTemplate(month, false, category_templates); - await processGoals(category_goals, month); - return ret; + //await storeTemplates(); + //const category_templates = await getTemplates(null, 'template'); + //const category_goals = await getTemplates(null, 'goal'); + //const ret = await processTemplate(month, false, category_templates); + //await processGoals(category_goals, month); + //return ret; + return overwriteTemplate({ month }); } export async function overwriteTemplate({ month }) { await storeTemplates(); - const category_templates = await getTemplates(null, 'template'); - const category_goals = await getTemplates(null, 'goal'); + const category_templates = await getTemplates(null); const ret = await processTemplate(month, true, category_templates); - await processGoals(category_goals, month); + //await processGoals(category_goals, month); return ret; } export async function applySingleCategoryTemplate({ month, category }) { - const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [ - category, - ]); - await storeTemplates(); - const category_templates = await getTemplates(categories[0], 'template'); - const category_goals = await getTemplates(categories[0], 'goal'); - const ret = await processTemplate( - month, - true, - category_templates, - categories[0], - ); - await processGoals(category_goals, month, categories[0]); - return ret; + //const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [ + // category, + //]); + //await storeTemplates(); + //const category_templates = await getTemplates(categories[0], 'template'); + //const category_goals = await getTemplates(categories[0], 'goal'); + //const ret = await processTemplate( + // month, + // true, + // category_templates, + // categories[0], + //); + //await processGoals(category_goals, month, categories[0]); + //return ret; + return overwriteTemplate({ month }); } export function runCheckTemplates() { @@ -69,63 +58,7 @@ async function getCategories() { ); } -function checkScheduleTemplates(template) { - let lowPriority = template[0].priority; - let errorNotice = false; - for (let l = 1; l < template.length; l++) { - if (template[l].priority !== lowPriority) { - lowPriority = Math.min(lowPriority, template[l].priority); - errorNotice = true; - } - } - return { lowPriority, errorNotice }; -} - -async function setGoalBudget({ month, templateBudget }) { - await batchMessages(async () => { - templateBudget.forEach(element => { - setBudget({ - category: element.category, - month, - amount: element.amount, - }); - }); - }); -} - -async function setCategoryTargets({ month, idealTemplate }) { - await batchMessages(async () => { - idealTemplate.forEach(element => { - setGoal({ - category: element.category, - goal: element.amount, - month, - long_goal: 0, - }); - }); - }); -} - -async function resetCategoryTargets(month, category) { - let categories = []; - if (category === null) { - categories = await getCategories(); - } else { - categories = category; - } - await batchMessages(async () => { - for (let i = 0; i < categories.length; i++) { - setGoal({ - category: categories[i].id, - goal: null, - month, - long_goal: null, - }); - } - }); -} - -async function getTemplates(category, directive: string) { +async function getTemplates(category) { //retrieves template definitions from the database const goal_def = await db.all( 'SELECT * FROM categories WHERE goal_def IS NOT NULL', @@ -138,9 +71,6 @@ async function getTemplates(category, directive: string) { if (category) { const singleCategoryTemplate = []; if (templates[category.id] !== undefined) { - singleCategoryTemplate[category.id] = templates[category.id].filter( - t => t.directive === directive, - ); return singleCategoryTemplate; } singleCategoryTemplate[category.id] = undefined; @@ -152,521 +82,82 @@ async function getTemplates(category, directive: string) { const id = categories[cc].id; if (templates[id]) { ret[id] = templates[id]; - ret[id] = ret[id].filter(t => t.directive === directive); } } return ret; } } -async function processTemplate( - month, - force, - category_templates, - category?, -): Promise { - let num_applied = 0; - let errors = []; - const idealTemplate = []; - const setToZero = []; - let priority_list = []; - +async function processTemplate(month, force, category_templates, category?) { + // get all categoryIDs that need processed + //done? + // setup objects for each category and catch errors let categories = []; - const categories_remove = []; - if (category) { - categories[0] = category; - } else { - categories = await getCategories(); - } - - //clears templated categories - for (let c = 0; c < categories.length; c++) { - const category = categories[c]; - const budgeted = await getSheetValue( - monthUtils.sheetForMonth(month), - `budget-${category.id}`, - ); - const template = category_templates[category.id]; - if (template) { - for (let l = 0; l < template.length; l++) { - //add each priority we need to a list. Will sort later - if (template[l].priority == null) { - continue; - } - priority_list.push(template[l].priority); - } - } - if (budgeted) { - if (!force) { - // save index of category to remove - categories_remove.push(c); - } else { - // add all categories with a template to the list to unset budget - if (template?.length > 0) { - setToZero.push({ - category: category.id, - }); - } + if (!category) { + const isReflect = isReflectBudget(); + const categories_long = await getCategories(); + categories_long.forEach(c => { + if (!isReflect && !c.is_income) { + categories.push(c.id); } - } + }); + } else { + categories = category.id; } + const catObjects = []; + let availBudget = 10000; + let priorities = []; + let remainderWeight = 0; + categories.forEach(c => { + let obj; + //try { + obj = new categoryTemplate(category_templates[c], c, month); + //} catch (error) { + // console.error(error); + //} + catObjects.push(obj); + }); - // remove the categories we are skipping - // Go backwards through the list so the indexes don't change - // on the categories we need - for (let i = categories_remove.length - 1; i >= 0; i--) { - categories.splice(categories_remove[i], 1); - } + // read messages - // zero out budget and goal from categories that need it - await setGoalBudget({ - month, - templateBudget: setToZero, + // get available starting balance figured out by reading originalBudget and toBudget + // gather needed priorities + // gather remainder weights + catObjects.forEach(o => { + availBudget += o.getOriginalBudget(); + const p = o.readPriorities(); + p.forEach(pr => priorities.push(pr)); + remainderWeight += o.getRemainderWeight(); }); - await resetCategoryTargets(month, categories); - // sort and filter down to just the requested priorities - priority_list = priority_list - .sort(function (a, b) { + //compress to needed, sorted priorities + priorities = priorities + .sort((a, b) => { return a - b; }) - .filter((item, index, curr) => curr.indexOf(item) === index); - - const { remainder_found, remainder_priority, remainder_weight_total } = - findRemainder(priority_list, categories, category_templates); - if (remainder_found) priority_list.push(remainder_priority); - - const sheetName = monthUtils.sheetForMonth(month); - const available_start = await getSheetValue(sheetName, `to-budget`); - let budgetAvailable = isReflectBudget() - ? await getSheetValue(sheetName, `total-saved`) - : await getSheetValue(sheetName, `to-budget`); - for (let ii = 0; ii < priority_list.length; ii++) { - const priority = priority_list[ii]; - const templateBudget = []; - - // setup scaling for remainder - let remainder_scale = 1; - if (priority === remainder_priority && remainder_found) { - const available_now = await getSheetValue(sheetName, `to-budget`); - remainder_scale = available_now / remainder_weight_total; - } - - for (let c = 0; c < categories.length; c++) { - const category = categories[c]; - let template_lines = category_templates[category.id]; - if (template_lines) { - //check that all schedule and by lines have the same priority level - let skipSchedule = false; - let isScheduleOrBy = false; - let priorityCheck = 0; - if ( - template_lines.filter( - t => - (t.type === 'schedule' || t.type === 'by') && - t.priority === priority, - ).length > 0 - ) { - template_lines = template_lines.filter( - t => - (t.priority === priority && - (t.type !== 'schedule' || t.type !== 'by')) || - t.type === 'schedule' || - t.type === 'by', - ); - const { lowPriority, errorNotice } = - await checkScheduleTemplates(template_lines); - priorityCheck = lowPriority; - skipSchedule = priorityCheck !== priority ? true : false; - isScheduleOrBy = true; - if (!skipSchedule && errorNotice) { - errors.push( - category.name + - ': Schedules and By templates should all have the same priority. Using priority ' + - priorityCheck, - ); - } - } - if (!skipSchedule) { - if (!isScheduleOrBy) { - template_lines = template_lines.filter( - t => t.priority === priority, - ); - } - if (template_lines.length > 0) { - errors = errors.concat( - template_lines - .filter(t => t.type === 'error') - .map(({ line, error }) => - [ - category.name + ': ' + error.message, - line, - ' '.repeat( - TEMPLATE_PREFIX.length + error.location.start.offset, - ) + '^', - ].join('\n'), - ), - ); - const prev_budgeted = await getSheetValue( - sheetName, - `budget-${category.id}`, - ); - const { amount: originalToBudget, errors: applyErrors } = - await applyCategoryTemplate( - category, - template_lines, - month, - remainder_scale, - available_start, - budgetAvailable, - prev_budgeted, - ); - - let to_budget = originalToBudget; - if (to_budget != null) { - num_applied++; - //only store goals from non remainder templates - if (priority !== remainder_priority) { - if ( - idealTemplate.filter(c => c.category === category.id).length > - 0 - ) { - idealTemplate.filter( - c => c.category === category.id, - )[0].amount += to_budget; - } else { - idealTemplate.push({ - category: category.id, - amount: to_budget, - }); - } - } - if (to_budget <= budgetAvailable || !priority) { - templateBudget.push({ - category: category.id, - amount: to_budget + prev_budgeted, - }); - } else if (to_budget > budgetAvailable && budgetAvailable >= 0) { - to_budget = budgetAvailable; - errors.push(`Insufficient funds.`); - templateBudget.push({ - category: category.id, - amount: to_budget + prev_budgeted, - }); - } - budgetAvailable -= to_budget; - } - if (applyErrors != null) { - errors = errors.concat( - applyErrors.map(error => `${category.name}: ${error}`), - ); - } - } - } - } - } - await setGoalBudget({ month, templateBudget }); - } - await setCategoryTargets({ month, idealTemplate }); - - if (num_applied === 0) { - if (errors.length) { - return { - type: 'error', - sticky: true, - message: `There were errors interpreting some templates:`, - pre: errors.join('\n\n'), - }; - } else { - return { type: 'message', message: 'All categories were up to date.' }; - } - } else { - const applied = `Successfully applied ${num_applied} templates.`; - if (errors.length) { - return { - sticky: true, - message: `${applied} There were errors interpreting some templates:`, - pre: errors.join('\n\n'), - }; - } else { - return { - type: 'message', - message: applied, - }; - } - } -} - -async function processGoals(goals, month, category?) { - let categories = []; - if (category) { - categories[0] = category; - } else { - categories = await getCategories(); - } - for (let c = 0; c < categories.length; c++) { - const cat_id = categories[c].id; - const goal_lines = goals[cat_id]; - if (goal_lines?.length > 0) { - await setGoal({ - month, - category: cat_id, - goal: amountToInteger(goal_lines[0].amount), - long_goal: 1, - }); + .filter((item, idx, curr) => curr.indexOf(item) === idx); + + // run each priority level + for (let pi = 0; pi < priorities.length; pi++) { + for (let i = 0; i < catObjects.length; i++) { + const ret = await catObjects[i].runTemplatesForPriority( + priorities[pi], + availBudget, + force, + ); + availBudget -= ret; } } -} - -async function applyCategoryTemplate( - category, - template_lines, - month, - remainder_scale, - available_start, - budgetAvailable, - prev_budgeted, -) { - const current_month = `${month}-01`; - let errors = []; - let all_schedule_names = await db.all( - 'SELECT name from schedules WHERE name NOT NULL AND tombstone = 0', - ); - all_schedule_names = all_schedule_names.map(v => v.name); - - let scheduleFlag = false; //only run schedules portion once - - // remove lines for past dates, calculate repeating dates - template_lines = template_lines.filter(template => { - switch (template.type) { - case 'by': - case 'spend': - let target_month = `${template.month}-01`; - let num_months = monthUtils.differenceInCalendarMonths( - target_month, - current_month, - ); - const repeat = template.annual - ? (template.repeat || 1) * 12 - : template.repeat; - - let spend_from; - if (template.type === 'spend') { - spend_from = `${template.from}-01`; - } - while (num_months < 0 && repeat) { - target_month = monthUtils.addMonths(target_month, repeat); - if (spend_from) { - spend_from = monthUtils.addMonths(spend_from, repeat); - } - num_months = monthUtils.differenceInCalendarMonths( - target_month, - current_month, - ); - } - if (num_months < 0) { - errors.push(`${template.month} is in the past.`); - return false; - } - template.month = monthUtils.format(target_month, 'yyyy-MM'); - if (spend_from) { - template.from = monthUtils.format(spend_from, 'yyyy-MM'); - } - break; - case 'schedule': - if (!all_schedule_names.includes(template.name)) { - errors.push(`Schedule ${template.name} does not exist`); - return null; - } - break; - default: - } - return true; + // run limits + catObjects.forEach(o => { + o.applyLimit(); + }); + // run remainder + catObjects.forEach(o => { + o.runRemainder(); + }); + // finish + catObjects.forEach(o => { + o.runFinish(); }); - - if (template_lines.length > 1) { - template_lines = template_lines.sort((a, b) => { - if (a.type === 'by' && !a.annual) { - return monthUtils.differenceInCalendarMonths( - `${a.month}-01`, - `${b.month}-01`, - ); - } else if (a.type === 'schedule' || b.type === 'schedule') { - return a.priority - b.priority; - } else { - return a.type.localeCompare(b.type); - } - }); - } - - const sheetName = monthUtils.sheetForMonth(month); - const spent = await getSheetValue(sheetName, `sum-amount-${category.id}`); - const balance = await getSheetValue(sheetName, `leftover-${category.id}`); - const last_month_balance = balance - spent - prev_budgeted; - let to_budget = 0; - let limit = 0; - let hold = false; - let limitCheck = false; - let remainder = 0; - - for (let l = 0; l < template_lines.length; l++) { - const template = template_lines[l]; - switch (template.type) { - case 'simple': { - const goalsReturn = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - limit = goalsReturn.limit; - limitCheck = goalsReturn.limitCheck; - hold = goalsReturn.hold; - break; - } - case 'copy': { - const goalsReturn = await goalsCopy( - template, - month, - category, - limitCheck, - errors, - limit, - hold, - to_budget, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - limit = goalsReturn.limit; - limitCheck = goalsReturn.limitCheck; - hold = goalsReturn.hold; - break; - } - case 'by': { - const goalsReturn = await goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - remainder = goalsReturn.remainder; - break; - } - case 'week': { - const goalsReturn = await goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - limit = goalsReturn.limit; - limitCheck = goalsReturn.limitCheck; - hold = goalsReturn.hold; - break; - } - case 'spend': { - const goalsReturn = await goalsSpend( - template, - last_month_balance, - current_month, - to_budget, - errors, - category, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - break; - } - case 'percentage': { - const goalsReturn = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - break; - } - case 'schedule': { - const goalsReturn = await goalsSchedule( - scheduleFlag, - template_lines, - current_month, - balance, - remainder, - last_month_balance, - to_budget, - errors, - category, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - remainder = goalsReturn.remainder; - scheduleFlag = goalsReturn.scheduleFlag; - break; - } - case 'remainder': { - const goalsReturn = await goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, - ); - to_budget = goalsReturn.to_budget; - break; - } - case 'average': { - const goalsReturn = await goalsAverage( - template, - current_month, - category, - errors, - to_budget, - ); - to_budget = goalsReturn.to_budget; - errors = goalsReturn.errors; - break; - } - case 'error': - return { errors }; - default: - } - } - - if (limitCheck) { - if (hold && balance > limit) { - to_budget = 0; - } else if (to_budget + balance > limit) { - to_budget = limit - balance; - } - } - // setup notifications - let str = category.name + ': ' + integerToAmount(last_month_balance); - str += - ' + ' + - integerToAmount(to_budget) + - ' = ' + - integerToAmount(last_month_balance + to_budget); - str += ' ' + template_lines.map(x => x.line).join('\n'); - console.log(str); - return { amount: to_budget, errors }; } diff --git a/packages/loot-core/src/server/budget/types/templates.d.ts b/packages/loot-core/src/server/budget/types/templates.d.ts index 8b6efda780d..8b5eb6e323d 100644 --- a/packages/loot-core/src/server/budget/types/templates.d.ts +++ b/packages/loot-core/src/server/budget/types/templates.d.ts @@ -55,7 +55,7 @@ interface RemainderTemplate extends BaseTemplate { interface AverageTemplate extends BaseTemplate { type: 'average'; - amount: number; + numMonths: number; } interface GoalTemplate extends BaseTemplate { @@ -63,6 +63,11 @@ interface GoalTemplate extends BaseTemplate { amount: number; } +interface CopyTemplate extends BaseTemplate { + type: 'copy'; + lookBack: number; +} + interface ErrorTemplate extends BaseTemplate { type: 'error'; line: string; @@ -79,4 +84,5 @@ export type Template = | RemainderTemplate | AverageTemplate | GoalTemplate + | CopyTemplate | ErrorTemplate; From 8c57d530ed29a21587ffa90b0147310cd889b4ef Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 30 Oct 2024 11:11:48 -0700 Subject: [PATCH 05/31] fix --- packages/loot-core/src/server/budget/categoryTemplate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 949f17b2b41..a9c25a6899b 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -84,7 +84,7 @@ export class categoryTemplate { break; } case 'spend': { - toBudget + -this.runSpend(t[i]); + toBudget += await this.runSpend(t[i]); break; } case 'percentage': { From 1821c13d14b0ec8b22108627b6989a8be834d867 Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 30 Oct 2024 13:59:36 -0700 Subject: [PATCH 06/31] basic overwrite of simple templates working --- .../src/server/budget/categoryTemplate.ts | 56 ++++-------- .../src/server/budget/goaltemplates.ts | 87 +++++++++++++------ 2 files changed, 76 insertions(+), 67 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a9c25a6899b..50be12f97c0 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -33,11 +33,6 @@ export class categoryTemplate { return this.remainderWeight; } - // return the current budgeted amount in the category - getOriginalBudget(): number { - return this.originalBudget; - } - // returns a list of priority levels in this category readPriorities(): number[] { return this.priorities; @@ -49,10 +44,10 @@ export class categoryTemplate { } // what is the full requested amount this month - runAll(force: boolean, available: number) { + runAll(available: number) { let toBudget: number = 0; this.priorities.forEach(async p => { - toBudget += await this.runTemplatesForPriority(p, available, force); + toBudget += await this.runTemplatesForPriority(p, available); }); //TODO does this need to run limits? return toBudget; @@ -63,8 +58,10 @@ export class categoryTemplate { async runTemplatesForPriority( priority: number, budgetAvail: number, - force: boolean, ) { + + if(!this.priorities.includes(priority)) return 0; + const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail || 0; let toBudget = 0; @@ -108,12 +105,12 @@ export class categoryTemplate { // don't overbudget when using a priority //TODO fix the usage of force here. Its wrong - if (priority > 0 && force === false && toBudget > available) { + if (priority > 0 && toBudget > available) { toBudget = available; } available = available - toBudget; - if (available <= 0 && force === false) { + if (available <= 0) { break; } } @@ -122,19 +119,20 @@ export class categoryTemplate { return toBudget; } - applyLimit() { + applyLimit(): number { if (this.limitCheck === false) { - this.toBudgetAmount = this.targetAmount; - return; + return 0; } if (this.limitHold && this.fromLastMonth >= this.limitAmount) { + const orig = this.toBudgetAmount; this.toBudgetAmount = 0; - return; + return orig; } - if (this.targetAmount > this.limitAmount) { + if (this.toBudgetAmount > this.limitAmount) { + const orig = this.toBudgetAmount; this.toBudgetAmount = this.limitAmount - this.fromLastMonth; + return orig-this.toBudgetAmount; } - //TODO return the value removed by the limits } // run all of the 'remainder' type templates @@ -166,21 +164,19 @@ export class categoryTemplate { private goals = []; private priorities: number[] = []; private remainderWeight: number = 0; - private originalBudget: number = 0; private toBudgetAmount: number = null; // amount that will be budgeted by the templates - private targetAmount: number = null; private isLongGoal: boolean = null; //defaulting the goals to null so templates can be unset private goalAmount: number = null; - private spentThisMonth = 0; // amount already spend this month private fromLastMonth = 0; // leftover from last month private limitAmount = null; private limitCheck = false; private limitHold = false; private msgs: string[]; - constructor(templates, categoryID: string, month: string) { + constructor(templates, categoryID: string, month: string, fromLastMonth:number) { this.categoryID = categoryID; this.month = month; + this.fromLastMonth = fromLastMonth; // sort the template lines into regular template, goals, and remainder templates if (templates) { templates.forEach(t => { @@ -203,11 +199,8 @@ export class categoryTemplate { this.findPriorities(); //find remainder weight this.findRemainderWeightSum(); - //get the starting budget amount - this.readBudgeted(); //get the from last month amount - this.readSpent(); - this.getFromLastMonth(); + this.getFromLastMonth().then(); } private findPriorities() { @@ -262,13 +255,6 @@ export class categoryTemplate { }); } - private async readBudgeted() { - this.originalBudget = await getSheetValue( - monthUtils.sheetForMonth(this.month), - `budget-${this.categoryID}`, - ); - } - private setGoal() { setGoal({ category: this.categoryID, @@ -278,14 +264,6 @@ export class categoryTemplate { }); } - private async readSpent() { - const sheetName = monthUtils.sheetForMonth(this.month); - this.spentThisMonth = await getSheetValue( - sheetName, - `sum-amount-${this.categoryID}`, - ); - } - private async getFromLastMonth() { const sheetName = monthUtils.sheetForMonth( monthUtils.subMonths(this.month, 1), diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index fcf9724a6c6..73864d6e070 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -2,8 +2,9 @@ import { Notification } from '../../client/state-types/notifications'; import * as db from '../db'; import { batchMessages } from '../sync'; +import * as monthUtils from '../../shared/months'; -import { isReflectBudget } from './actions'; +import { isReflectBudget, getSheetValue, setGoal } from './actions'; import { categoryTemplate } from './categoryTemplate'; import { checkTemplates, storeTemplates } from './template-notes'; @@ -88,7 +89,7 @@ async function getTemplates(category) { } } -async function processTemplate(month, force, category_templates, category?) { +async function processTemplate(month:string, force:boolean, categoryTemplates, category?) { // get all categoryIDs that need processed //done? // setup objects for each category and catch errors @@ -104,31 +105,53 @@ async function processTemplate(month, force, category_templates, category?) { } else { categories = category.id; } - const catObjects = []; - let availBudget = 10000; + const catObjects: categoryTemplate[] = []; + let availBudget = await getSheetValue( + monthUtils.sheetForMonth(month), + `to-budget`, + ); let priorities = []; let remainderWeight = 0; - categories.forEach(c => { - let obj; - //try { - obj = new categoryTemplate(category_templates[c], c, month); - //} catch (error) { - // console.error(error); - //} - catObjects.push(obj); - }); + for(let i = 0; i priorities.push(pr)); + remainderWeight += obj.getRemainderWeight(); + catObjects.push(obj); + + // do a reset of the goals that are orphaned + } else if(existingGoal != null && !templates) { + await setGoal({month: month, category: id, goal: null, long_goal: null}) + } + } // read messages - // get available starting balance figured out by reading originalBudget and toBudget - // gather needed priorities - // gather remainder weights - catObjects.forEach(o => { - availBudget += o.getOriginalBudget(); - const p = o.readPriorities(); - p.forEach(pr => priorities.push(pr)); - remainderWeight += o.getRemainderWeight(); - }); //compress to needed, sorted priorities priorities = priorities @@ -143,21 +166,29 @@ async function processTemplate(month, force, category_templates, category?) { const ret = await catObjects[i].runTemplatesForPriority( priorities[pi], availBudget, - force, ); availBudget -= ret; + if(availBudget<=0){ + break; + } + } + if(availBudget<=0){ + break; } } // run limits catObjects.forEach(o => { - o.applyLimit(); + availBudget += o.applyLimit(); }); // run remainder - catObjects.forEach(o => { - o.runRemainder(); - }); + if(availBudget>0){ + const perWeight = availBudget/remainderWeight; + catObjects.forEach(o => { + availBudget -= o.runRemainder(availBudget, perWeight); + }); + } // finish catObjects.forEach(o => { o.runFinish(); }); -} +} \ No newline at end of file From 32d471c6d489862f7026a2f09a4ce7f5f1f94ba7 Mon Sep 17 00:00:00 2001 From: youngcw Date: Wed, 30 Oct 2024 13:59:36 -0700 Subject: [PATCH 07/31] mostly working. By and schedule don't work --- .../src/server/budget/categoryTemplate.ts | 87 ++++------- .../src/server/budget/goaltemplates.ts | 146 ++++++++++-------- 2 files changed, 116 insertions(+), 117 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a9c25a6899b..a8c00decf64 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -4,9 +4,9 @@ import * as monthUtils from '../../shared/months'; import { amountToInteger } from '../../shared/util'; import * as db from '../db'; -import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions'; +import { getSheetValue, setBudget, setGoal } from './actions'; import { getActiveSchedules } from './statements'; -import { Template } from './types/templates'; +//import { Template } from './types/templates'; export class categoryTemplate { /*---------------------------------------------------------------------------- @@ -33,11 +33,6 @@ export class categoryTemplate { return this.remainderWeight; } - // return the current budgeted amount in the category - getOriginalBudget(): number { - return this.originalBudget; - } - // returns a list of priority levels in this category readPriorities(): number[] { return this.priorities; @@ -49,10 +44,10 @@ export class categoryTemplate { } // what is the full requested amount this month - runAll(force: boolean, available: number) { + runAll(available: number) { let toBudget: number = 0; this.priorities.forEach(async p => { - toBudget += await this.runTemplatesForPriority(p, available, force); + toBudget += await this.runTemplatesForPriority(p, available); }); //TODO does this need to run limits? return toBudget; @@ -60,11 +55,9 @@ export class categoryTemplate { // run all templates in a given priority level // return: amount budgeted in this priority level - async runTemplatesForPriority( - priority: number, - budgetAvail: number, - force: boolean, - ) { + async runTemplatesForPriority(priority: number, budgetAvail: number) { + if (!this.priorities.includes(priority)) return 0; + const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail || 0; let toBudget = 0; @@ -107,13 +100,12 @@ export class categoryTemplate { } // don't overbudget when using a priority - //TODO fix the usage of force here. Its wrong - if (priority > 0 && force === false && toBudget > available) { + if (priority > 0 && toBudget > available) { toBudget = available; } available = available - toBudget; - if (available <= 0 && force === false) { + if (priority > 0 && available <= 0) { break; } } @@ -122,19 +114,20 @@ export class categoryTemplate { return toBudget; } - applyLimit() { + applyLimit(): number { if (this.limitCheck === false) { - this.toBudgetAmount = this.targetAmount; - return; + return 0; } if (this.limitHold && this.fromLastMonth >= this.limitAmount) { + const orig = this.toBudgetAmount; this.toBudgetAmount = 0; - return; + return orig; } - if (this.targetAmount > this.limitAmount) { + if (this.toBudgetAmount + this.fromLastMonth > this.limitAmount) { + const orig = this.toBudgetAmount; this.toBudgetAmount = this.limitAmount - this.fromLastMonth; + return orig - this.toBudgetAmount; } - //TODO return the value removed by the limits } // run all of the 'remainder' type templates @@ -166,25 +159,28 @@ export class categoryTemplate { private goals = []; private priorities: number[] = []; private remainderWeight: number = 0; - private originalBudget: number = 0; private toBudgetAmount: number = null; // amount that will be budgeted by the templates - private targetAmount: number = null; private isLongGoal: boolean = null; //defaulting the goals to null so templates can be unset private goalAmount: number = null; - private spentThisMonth = 0; // amount already spend this month private fromLastMonth = 0; // leftover from last month private limitAmount = null; private limitCheck = false; private limitHold = false; private msgs: string[]; - constructor(templates, categoryID: string, month: string) { + constructor( + templates, + categoryID: string, + month: string, + fromLastMonth: number, + ) { this.categoryID = categoryID; this.month = month; + this.fromLastMonth = fromLastMonth; // sort the template lines into regular template, goals, and remainder templates if (templates) { templates.forEach(t => { - if (t.directive === 'template' && t.type != 'remainder') { + if (t.directive === 'template' && t.type !== 'remainder') { this.templates.push(t); } }); @@ -203,11 +199,6 @@ export class categoryTemplate { this.findPriorities(); //find remainder weight this.findRemainderWeightSum(); - //get the starting budget amount - this.readBudgeted(); - //get the from last month amount - this.readSpent(); - this.getFromLastMonth(); } private findPriorities() { @@ -233,7 +224,7 @@ export class categoryTemplate { this.remainderWeight = weight; } - private checkTemplates() { + private async checkTemplates() { //run all the individual checks this.checkByAndSchedule(); this.checkPercentage(); @@ -262,13 +253,6 @@ export class categoryTemplate { }); } - private async readBudgeted() { - this.originalBudget = await getSheetValue( - monthUtils.sheetForMonth(this.month), - `budget-${this.categoryID}`, - ); - } - private setGoal() { setGoal({ category: this.categoryID, @@ -278,14 +262,6 @@ export class categoryTemplate { }); } - private async readSpent() { - const sheetName = monthUtils.sheetForMonth(this.month); - this.spentThisMonth = await getSheetValue( - sheetName, - `sum-amount-${this.categoryID}`, - ); - } - private async getFromLastMonth() { const sheetName = monthUtils.sheetForMonth( monthUtils.subMonths(this.month, 1), @@ -306,7 +282,7 @@ export class categoryTemplate { .filter(t => t.type === 'schedule') .forEach(t => { if (!scheduleNames.includes(t.name.trim())) { - throw new Error(`Schedule "${t.name.trim()}" does not exist`); + throw new Error(`Schedule ${t.name.trim()} does not exist`); } }); //find lowest priority @@ -322,7 +298,7 @@ export class categoryTemplate { this.templates .filter(t => t.type === 'schedule' || t.type === 'by') .forEach(t => { - if (t.priority != lowestPriority) { + if (t.priority !== lowestPriority) { t.priority = lowestPriority; this.msgs.push( `Changed the priority of BY and SCHEDULE templates to be ${lowestPriority}`, @@ -338,8 +314,7 @@ export class categoryTemplate { const t = this.templates[i]; if (this.limitCheck) { throw new Error('Only one `up to` allowed per category'); - break; - } else if (t.limit != null) { + } else if (t.limit) { this.limitCheck = true; this.limitHold = t.limit.hold ? true : false; this.limitAmount = amountToInteger(t.limit.amount); @@ -386,7 +361,7 @@ export class categoryTemplate { private async runCopy(template) { const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, template.lookback), + monthUtils.subMonths(this.month, template.lookBack), ); return await getSheetValue(sheetName, `budget-${this.categoryID}`); } @@ -456,7 +431,7 @@ export class categoryTemplate { const cat = template.category.toLowerCase(); const prev = template.previous; let sheetName; - let monthlyIncome = 0; + let monthlyIncome = 1; //choose the sheet to find income for if (prev) { @@ -474,7 +449,7 @@ export class categoryTemplate { ); monthlyIncome = await getSheetValue( sheetName, - `sum_amount-${incomeCat.id}`, + `sum-amount-${incomeCat.id}`, ); } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index fcf9724a6c6..1e89b1e1735 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -1,46 +1,40 @@ // @ts-strict-ignore import { Notification } from '../../client/state-types/notifications'; +import * as monthUtils from '../../shared/months'; import * as db from '../db'; import { batchMessages } from '../sync'; -import { isReflectBudget } from './actions'; +import { isReflectBudget, getSheetValue, setGoal } from './actions'; import { categoryTemplate } from './categoryTemplate'; import { checkTemplates, storeTemplates } from './template-notes'; export async function applyTemplate({ month }) { - //await storeTemplates(); - //const category_templates = await getTemplates(null, 'template'); - //const category_goals = await getTemplates(null, 'goal'); - //const ret = await processTemplate(month, false, category_templates); - //await processGoals(category_goals, month); - //return ret; - return overwriteTemplate({ month }); + await storeTemplates(); + const category_templates = await getTemplates(null); + const ret = await processTemplate(month, false, category_templates, null); + return ret; } export async function overwriteTemplate({ month }) { await storeTemplates(); const category_templates = await getTemplates(null); - const ret = await processTemplate(month, true, category_templates); - //await processGoals(category_goals, month); + const ret = await processTemplate(month, true, category_templates, null); return ret; } export async function applySingleCategoryTemplate({ month, category }) { - //const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [ - // category, - //]); - //await storeTemplates(); - //const category_templates = await getTemplates(categories[0], 'template'); - //const category_goals = await getTemplates(categories[0], 'goal'); - //const ret = await processTemplate( - // month, - // true, - // category_templates, - // categories[0], - //); - //await processGoals(category_goals, month, categories[0]); - //return ret; - return overwriteTemplate({ month }); + const categories = await db.all(`SELECT * FROM v_categories WHERE id = ?`, [ + category, + ]); + await storeTemplates(); + const category_templates = await getTemplates(categories[0]); + const ret = await processTemplate( + month, + true, + category_templates, + categories, + ); + return ret; } export function runCheckTemplates() { @@ -69,12 +63,9 @@ async function getTemplates(category) { templates[goal_def[ll].id] = JSON.parse(goal_def[ll].goal_def); } if (category) { - const singleCategoryTemplate = []; - if (templates[category.id] !== undefined) { - return singleCategoryTemplate; - } - singleCategoryTemplate[category.id] = undefined; - return singleCategoryTemplate; + const ret = []; + ret[category.id] = templates[category.id]; + return ret; } else { const categories = await getCategories(); const ret = []; @@ -88,47 +79,72 @@ async function getTemplates(category) { } } -async function processTemplate(month, force, category_templates, category?) { +async function processTemplate( + month, + force: boolean, + categoryTemplates, + categoriesIn?: any[], +) { // get all categoryIDs that need processed //done? // setup objects for each category and catch errors let categories = []; - if (!category) { + if (!categoriesIn) { const isReflect = isReflectBudget(); const categories_long = await getCategories(); categories_long.forEach(c => { if (!isReflect && !c.is_income) { - categories.push(c.id); + categories.push(c); } }); } else { - categories = category.id; + categories = categoriesIn; } - const catObjects = []; - let availBudget = 10000; + const catObjects: categoryTemplate[] = []; + let availBudget = await getSheetValue( + monthUtils.sheetForMonth(month), + `to-budget`, + ); let priorities = []; let remainderWeight = 0; - categories.forEach(c => { - let obj; - //try { - obj = new categoryTemplate(category_templates[c], c, month); - //} catch (error) { - // console.error(error); - //} - catObjects.push(obj); - }); + for (let i = 0; i < categories.length; i++) { + const id = categories[i].id; + const sheetName = monthUtils.sheetForMonth(month); + const templates = categoryTemplates[id]; + //if there is a good way to add these to the class that would be nice, + // but the async getSheetValue messes things up + // only the fromLastMonth value is needed inside the class but it + // would be nice to have everything wrapped up as much as possible + const budgeted = await getSheetValue(sheetName, `budget-${id}`); + const fromLastMonth = await getSheetValue( + monthUtils.sheetForMonth(monthUtils.subMonths(month, 1)), + `leftover-${id}`, + ); + const existingGoal = await getSheetValue(sheetName, `goal-${id}`); - // read messages + // only run categories that are unbudgeted or if we are forcing it + if ((budgeted === 0 || force) && templates) { + // add to available budget + // gather needed priorities + // gather remainder weights + try { + const obj = new categoryTemplate(templates, id, month, fromLastMonth); + availBudget += budgeted; + const p = obj.readPriorities(); + p.forEach(pr => priorities.push(pr)); + remainderWeight += obj.getRemainderWeight(); + catObjects.push(obj); + } catch (e) { + console.log(`Got error in ${categories[i].name}: ${e}`); + } - // get available starting balance figured out by reading originalBudget and toBudget - // gather needed priorities - // gather remainder weights - catObjects.forEach(o => { - availBudget += o.getOriginalBudget(); - const p = o.readPriorities(); - p.forEach(pr => priorities.push(pr)); - remainderWeight += o.getRemainderWeight(); - }); + // do a reset of the goals that are orphaned + } else if (existingGoal != null && !templates) { + await setGoal({ month, category: id, goal: null, long_goal: null }); + } + } + + // read messages //compress to needed, sorted priorities priorities = priorities @@ -143,19 +159,27 @@ async function processTemplate(month, force, category_templates, category?) { const ret = await catObjects[i].runTemplatesForPriority( priorities[pi], availBudget, - force, ); availBudget -= ret; + if (availBudget <= 0) { + break; + } + } + if (availBudget <= 0) { + break; } } // run limits catObjects.forEach(o => { - o.applyLimit(); + availBudget += o.applyLimit(); }); // run remainder - catObjects.forEach(o => { - o.runRemainder(); - }); + if (availBudget > 0 && remainderWeight) { + const perWeight = availBudget / remainderWeight; + catObjects.forEach(o => { + availBudget -= o.runRemainder(availBudget, perWeight); + }); + } // finish catObjects.forEach(o => { o.runFinish(); From 9553e7b6a4d983962d7b1719a0f6cca60c3e69a2 Mon Sep 17 00:00:00 2001 From: youngcw Date: Fri, 1 Nov 2024 14:16:40 -0700 Subject: [PATCH 08/31] some cleanup, better async --- .../src/server/budget/categoryTemplate.ts | 93 +++++++++---------- .../src/server/budget/goaltemplates.ts | 18 +--- 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a8c00decf64..cafe7ed6c35 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -11,11 +11,11 @@ import { getActiveSchedules } from './statements'; export class categoryTemplate { /*---------------------------------------------------------------------------- * Using This Class: - * 1. instantiate via `new categoryTemplate(categoryID, templates, month)`; - * categoryID: the ID of the category that this Class will be for + * 1. instantiate via `await categoryTemplate.init(templates, categoryID, month)`; * templates: all templates for this category (including templates and goals) + * categoryID: the ID of the category that this Class will be for * month: the month string of the month for templates being applied - * 2. gather needed data for external use. ex: remainder weights, priorities, originalBudget + * 2. gather needed data for external use. ex: remainder weights, priorities * 3. run each priority level that is needed via runTemplatesForPriority * 4. run applyLimits to apply any existing limit to the category * 5. run the remainder templates via runRemainder (limits get applied at the start of this) @@ -28,19 +28,24 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Class interface - // returns the total remainder weight of remainder templates in this category - getRemainderWeight(): number { - return this.remainderWeight; + // set up the class and check all templates + static async init(templates, categoryID: string, month) { + // get all the needed setup values + const fromLastMonth = await getSheetValue( + monthUtils.sheetForMonth(monthUtils.subMonths(month, 1)), + `leftover-${categoryID}`, + ); + // run all checks + await categoryTemplate.checkTemplates(templates); + // call the private constructor + return new categoryTemplate(templates, categoryID, month, fromLastMonth); } - // returns a list of priority levels in this category - readPriorities(): number[] { + getPriorities() { return this.priorities; } - - // get messages that were generated during construction - readMsgs() { - return this.msgs; + getRemainderWeight() { + return this.remainderWeight; } // what is the full requested amount this month @@ -152,7 +157,7 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Implimentation - readonly categoryID: string; + readonly categoryID: string; //readonly so we can double check the category this is using private month: string; private templates = []; private remainder = []; @@ -166,9 +171,8 @@ export class categoryTemplate { private limitAmount = null; private limitCheck = false; private limitHold = false; - private msgs: string[]; - constructor( + private constructor( templates, categoryID: string, month: string, @@ -193,15 +197,10 @@ export class categoryTemplate { if (t.directive === 'goal') this.goals.push(t); }); } - //check templates and throw exception if there is something wrong - this.checkTemplates(); - //find priorities - this.findPriorities(); - //find remainder weight - this.findRemainderWeightSum(); - } + // check limits here since it needs to save states inside the object + this.checkLimit(); - private findPriorities() { + //find priorities const p = []; this.templates.forEach(t => { if (t.priority != null) { @@ -214,9 +213,8 @@ export class categoryTemplate { return a - b; }) .filter((item, idx, curr) => curr.indexOf(item) === idx); - } - private findRemainderWeightSum() { + //find remainder weight let weight = 0; this.remainder.forEach(r => { weight += r.weight; @@ -224,13 +222,6 @@ export class categoryTemplate { this.remainderWeight = weight; } - private async checkTemplates() { - //run all the individual checks - this.checkByAndSchedule(); - this.checkPercentage(); - this.checkLimit(); - } - private runGoal() { if (this.goals.length > 0) { this.isLongGoal = true; @@ -238,9 +229,8 @@ export class categoryTemplate { return; } if (this.goals.length > 1) { - this.msgs.push( - 'Can only have one #goal per category. Using the first found', - ); + //TODO make this not hard fail + throw new Error(`Can only have one #goal per category`); } this.goalAmount = this.toBudgetAmount; } @@ -275,10 +265,17 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Template Validation - private async checkByAndSchedule() { + static async checkTemplates(templates) { + //run all the individual checks + await categoryTemplate.checkByAndSchedule(templates); + await categoryTemplate.checkPercentage(templates); + //limits checked inside constructor + } + + static async checkByAndSchedule(templates) { //check schedule names const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); - this.templates + templates .filter(t => t.type === 'schedule') .forEach(t => { if (!scheduleNames.includes(t.name.trim())) { @@ -287,32 +284,30 @@ export class categoryTemplate { }); //find lowest priority let lowestPriority = null; - this.templates + templates .filter(t => t.type === 'schedule' || t.type === 'by') .forEach(t => { if (lowestPriority === null || t.priority < lowestPriority) { lowestPriority = t.priority; } }); - //set priority to needed value - this.templates + //warn if priority needs fixed + templates .filter(t => t.type === 'schedule' || t.type === 'by') .forEach(t => { if (t.priority !== lowestPriority) { - t.priority = lowestPriority; - this.msgs.push( - `Changed the priority of BY and SCHEDULE templates to be ${lowestPriority}`, + throw new Error( + `Schedule and By templates must be the same priority level. Fix by setting all Schedule and By templates to priority level ${lowestPriority}`, ); + //t.priority = lowestPriority; } }); - - //TODO add a message that the priority has been changed } private checkLimit() { for (let i = 0; i < this.templates.length; i++) { const t = this.templates[i]; - if (this.limitCheck) { + if (this.limitCheck && t.limit) { throw new Error('Only one `up to` allowed per category'); } else if (t.limit) { this.limitCheck = true; @@ -322,8 +317,8 @@ export class categoryTemplate { } } - private async checkPercentage() { - const pt = this.templates.filter(t => t.type === 'percentage'); + static async checkPercentage(templates) { + const pt = templates.filter(t => t.type === 'percentage'); const reqCategories = []; pt.forEach(t => reqCategories.push(t.category.toLowerCase())); @@ -336,7 +331,7 @@ export class categoryTemplate { }); reqCategories.forEach(n => { - if (n === 'availble funds' || n === 'all income') { + if (n === 'available funds' || n === 'all income') { //skip the name check since these are special } else if (!availNames.includes(n)) { throw new Error( diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 1e89b1e1735..ca41812a1e2 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -107,19 +107,12 @@ async function processTemplate( ); let priorities = []; let remainderWeight = 0; + const errors = []; for (let i = 0; i < categories.length; i++) { const id = categories[i].id; const sheetName = monthUtils.sheetForMonth(month); const templates = categoryTemplates[id]; - //if there is a good way to add these to the class that would be nice, - // but the async getSheetValue messes things up - // only the fromLastMonth value is needed inside the class but it - // would be nice to have everything wrapped up as much as possible const budgeted = await getSheetValue(sheetName, `budget-${id}`); - const fromLastMonth = await getSheetValue( - monthUtils.sheetForMonth(monthUtils.subMonths(month, 1)), - `leftover-${id}`, - ); const existingGoal = await getSheetValue(sheetName, `goal-${id}`); // only run categories that are unbudgeted or if we are forcing it @@ -128,24 +121,23 @@ async function processTemplate( // gather needed priorities // gather remainder weights try { - const obj = new categoryTemplate(templates, id, month, fromLastMonth); + const obj = await categoryTemplate.init(templates, id, month); availBudget += budgeted; - const p = obj.readPriorities(); + const p = obj.getPriorities(); p.forEach(pr => priorities.push(pr)); remainderWeight += obj.getRemainderWeight(); catObjects.push(obj); } catch (e) { console.log(`Got error in ${categories[i].name}: ${e}`); + errors.push(e); } // do a reset of the goals that are orphaned - } else if (existingGoal != null && !templates) { + } else if (existingGoal !== null && !templates) { await setGoal({ month, category: id, goal: null, long_goal: null }); } } - // read messages - //compress to needed, sorted priorities priorities = priorities .sort((a, b) => { From 2f929ebf53a5f428c32cd52a53a989b2b9285a1d Mon Sep 17 00:00:00 2001 From: youngcw Date: Fri, 1 Nov 2024 19:48:06 -0700 Subject: [PATCH 09/31] add notifications --- .../src/server/budget/categoryTemplate.ts | 46 ++++++++++--------- .../src/server/budget/goaltemplates.ts | 21 ++++++++- 2 files changed, 43 insertions(+), 24 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index cafe7ed6c35..47b62a853b8 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -36,7 +36,8 @@ export class categoryTemplate { `leftover-${categoryID}`, ); // run all checks - await categoryTemplate.checkTemplates(templates); + await categoryTemplate.checkByAndSchedule(templates); + await categoryTemplate.checkPercentage(templates); // call the private constructor return new categoryTemplate(templates, categoryID, month, fromLastMonth); } @@ -199,6 +200,7 @@ export class categoryTemplate { } // check limits here since it needs to save states inside the object this.checkLimit(); + this.checkSpend(); //find priorities const p = []; @@ -265,13 +267,6 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Template Validation - static async checkTemplates(templates) { - //run all the individual checks - await categoryTemplate.checkByAndSchedule(templates); - await categoryTemplate.checkPercentage(templates); - //limits checked inside constructor - } - static async checkByAndSchedule(templates) { //check schedule names const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); @@ -304,19 +299,6 @@ export class categoryTemplate { }); } - private checkLimit() { - for (let i = 0; i < this.templates.length; i++) { - const t = this.templates[i]; - if (this.limitCheck && t.limit) { - throw new Error('Only one `up to` allowed per category'); - } else if (t.limit) { - this.limitCheck = true; - this.limitHold = t.limit.hold ? true : false; - this.limitAmount = amountToInteger(t.limit.amount); - } - } - } - static async checkPercentage(templates) { const pt = templates.filter(t => t.type === 'percentage'); const reqCategories = []; @@ -335,12 +317,32 @@ export class categoryTemplate { //skip the name check since these are special } else if (!availNames.includes(n)) { throw new Error( - `Category ${n} is not found in available income categories`, + `Category \x22${n}\x22 is not found in available income categories`, ); } }); } + private checkLimit() { + for (let i = 0; i < this.templates.length; i++) { + const t = this.templates[i]; + if (this.limitCheck && t.limit) { + throw new Error('Only one `up to` allowed per category'); + } else if (t.limit) { + this.limitCheck = true; + this.limitHold = t.limit.hold ? true : false; + this.limitAmount = amountToInteger(t.limit.amount); + } + } + } + + private checkSpend() { + const st = this.templates.filter(t => t.type=== 'spend'); + if(st.length>1){ + throw new Error('Only one spend template is allowed per category'); + } + } + //----------------------------------------------------------------------------- // Processor Functions diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index ca41812a1e2..bfe26f871aa 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -128,8 +128,8 @@ async function processTemplate( remainderWeight += obj.getRemainderWeight(); catObjects.push(obj); } catch (e) { - console.log(`Got error in ${categories[i].name}: ${e}`); - errors.push(e); + //console.log(`${categories[i].name}: ${e}`); + errors.push(`${categories[i].name}: ${e.message}`); } // do a reset of the goals that are orphaned @@ -138,6 +138,18 @@ async function processTemplate( } } + //break early if nothing to do, or there are errors + if(catObjects.length===0 && errors.length===0){ + errors.push('Everything is up to date'); + } + if(errors.length>0){ + return { + sticky: true, + message: `There were errors interpreting some templates:`, + pre: errors.join(`\n\n`), + }; + } + //compress to needed, sorted priorities priorities = priorities .sort((a, b) => { @@ -176,4 +188,9 @@ async function processTemplate( catObjects.forEach(o => { o.runFinish(); }); + + return { + type: 'message', + message: `Successfully applied templates to ${catObjects.length} categories`, + } } From aa48f2ad0a27fe7cb8239cec786778f239ec454e Mon Sep 17 00:00:00 2001 From: youngcw Date: Fri, 1 Nov 2024 20:04:33 -0700 Subject: [PATCH 10/31] add daily weekly limits --- .../src/server/budget/categoryTemplate.ts | 23 +++++++++++- .../src/server/budget/goal-template.pegjs | 35 +++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 47b62a853b8..0c034623c36 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -329,9 +329,30 @@ export class categoryTemplate { if (this.limitCheck && t.limit) { throw new Error('Only one `up to` allowed per category'); } else if (t.limit) { + if (t.limit.period === 'daily'){ + const numDays = monthUtils.differenceInCalendarDays( + monthUtils.addMonths(this.month,1), + this.month, + ); + this.limitAmount += amountToInteger(t.limit.amount) * numDays; + } else if(t.limit.period === 'weekly') { + const nextMonth = monthUtils.nextMonth(this.month); + let week = t.limit.start; + const baseLimit = amountToInteger(t.limit.amount); + while (week < nextMonth) { + if(week >= this.month) { + this.limitAmount += baseLimit; + } + week = monthUtils.addWeeks(week,1); + } + } else if (t.limit.period === 'monthly') { + this.limitAmount = amountToInteger(t.limit.amount); + } else { + throw new Error('Invalid limit period. Check template syntax'); + } + //amount is good save the rest this.limitCheck = true; this.limitHold = t.limit.hold ? true : false; - this.limitAmount = amountToInteger(t.limit.amount); } } } diff --git a/packages/loot-core/src/server/budget/goal-template.pegjs b/packages/loot-core/src/server/budget/goal-template.pegjs index 4da54588299..fcf3201652f 100644 --- a/packages/loot-core/src/server/budget/goal-template.pegjs +++ b/packages/loot-core/src/server/budget/goal-template.pegjs @@ -13,33 +13,37 @@ expr ...(repeat ? repeat[3] : {}), from, priority: template.priority, directive: template.directive - } } + }} / template: template _ monthly: amount limit: limit? - { return { type: 'simple', monthly, limit, priority: template.priority, directive: template.directive } } + { return { type: 'simple', monthly, limit, priority: template.priority, directive: template.directive }} / template: template _ limit: limit - { return { type: 'simple', limit , priority: template.priority, directive: template.directive } } + { return { type: 'simple', monthly: null, limit, priority: template.priority, directive: template.directive }} / template: template _ schedule _ full:full? name: name - { return { type: 'schedule', name, priority: template.priority, directive: template.directive, full } } + { return { type: 'schedule', name, priority: template.priority, directive: template.directive, full }} / template: template _ remainder: remainder limit: limit? - { return { type: 'remainder', priority: null, directive: template.directive, weight: remainder, limit } } + { return { type: 'remainder', priority: null, directive: template.directive, weight: remainder, limit }} / template: template _ 'average'i _ amount: positive _ 'months'i? { return { type: 'average', numMonths: +amount, priority: template.priority, directive: template.directive }} / template: template _ 'copy from'i _ lookBack: positive _ 'months ago'i limit:limit? { return { type: 'copy', priority: template.priority, directive: template.directive, lookBack: +lookBack, limit }} - / goal: goal amount: amount { return {type: 'simple', amount: amount, priority: null, directive: 'goal' }} + / goal: goal amount: amount { return {type: 'simple', amount: amount, priority: null, directive: goal }} repeat 'repeat interval' - = 'month'i { return { annual: false } } - / months: positive _ 'months'i { return { annual: false, repeat: +months } } - / 'year'i { return { annual: true } } - / years: positive _ 'years'i { return { annual: true, repeat: +years } } + = 'month'i { return { annual: false }} + / months: positive _ 'months'i { return { annual: false, repeat: +months }} + / 'year'i { return { annual: true }} + / years: positive _ 'years'i { return { annual: true, repeat: +years }} -limit = _? upTo _ amount: amount _ 'hold'i { return {amount: amount, hold: true } } - / _? upTo _ amount: amount { return {amount: amount, hold: false } } +limit = _? upTo _ amount: amount _ 'per week starting'i _ start:date _? hold:hold? + { return {amount: amount, hold: hold, period: 'weekly', start: start }} + / _? upTo _ amount: amount _ 'per day'i _? hold: hold? + { return {amount: amount, hold: hold, period: 'daily', start:null }} + / _? upTo _ amount: amount _? hold: hold? + { return {amount: amount, hold: hold, period: 'monthly', start:null }} -percentOf = percent:percent _ of _ 'previous'i _ { return { percent: percent, prev: true} } - / percent:percent _ of _ { return { percent: percent, prev: false} } +percentOf = percent:percent _ of _ 'previous'i _ { return { percent: percent, prev: true}} + / percent:percent _ of _ { return { percent: percent, prev: false}} weekCount = week { return null } @@ -54,12 +58,13 @@ of = 'of'i repeatEvery = 'repeat'i _ 'every'i starting = 'starting'i upTo = 'up'i _ 'to'i +hold = 'hold'i {return true} schedule = 'schedule'i full = 'full'i _ {return true} priority = '-'i number: number {return number} remainder = 'remainder'i _? weight: positive? { return +weight || 1 } template = '#template' priority: priority? {return {priority: +priority, directive: 'template'}} -goal = '#goal' +goal = '#goal'i { return 'goal'} _ 'space' = ' '+ d 'digit' = [0-9] From 31edc84f77c6d0e3b14f8c9cb9944f2b978daa02 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 09:30:45 -0700 Subject: [PATCH 11/31] by is working I think --- .../src/server/budget/categoryTemplate.ts | 56 +++++++++---------- .../src/server/budget/goals/goalsBy.ts | 2 +- .../src/server/budget/goaltemplates.ts | 6 +- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 0c034623c36..cc1d635e994 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -55,7 +55,7 @@ export class categoryTemplate { this.priorities.forEach(async p => { toBudget += await this.runTemplatesForPriority(p, available); }); - //TODO does this need to run limits? + //TODO does this need to run limits? maybe pass in option to ignore previous balance? return toBudget; } @@ -67,6 +67,8 @@ export class categoryTemplate { const t = this.templates.filter(t => t.priority === priority); let available = budgetAvail || 0; let toBudget = 0; + let first = true; // needed for by templates + let remainder = 0; // switch on template type and calculate the amount for the line for (let i = 0; i < t.length; i++) { switch (t[i].type) { @@ -91,11 +93,15 @@ export class categoryTemplate { break; } case 'by': { - //TODO add the logic to run all of these at once - toBudget += this.runBy(t[i], this.templates, 0, budgetAvail); + //TODO add the logic to run all of these at once or whatever is needed + const ret = this.runBy(t[i], first, remainder); + toBudget += ret.ret; + remainder = ret.remainder; + first = false; break; } case 'schedule': { + //TODO add the logic to run all of these at once or whatever needs to happen toBudget += this.runSchedule(t[i]); break; } @@ -231,7 +237,6 @@ export class categoryTemplate { return; } if (this.goals.length > 1) { - //TODO make this not hard fail throw new Error(`Can only have one #goal per category`); } this.goalAmount = this.toBudgetAmount; @@ -254,17 +259,6 @@ export class categoryTemplate { }); } - private async getFromLastMonth() { - const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(this.month, 1), - ); - //TODO see if this is accurate from the sheet for the balance last month - this.fromLastMonth = await getSheetValue( - sheetName, - `leftover-${this.categoryID}`, - ); - } - //----------------------------------------------------------------------------- // Template Validation static async checkByAndSchedule(templates) { @@ -329,21 +323,21 @@ export class categoryTemplate { if (this.limitCheck && t.limit) { throw new Error('Only one `up to` allowed per category'); } else if (t.limit) { - if (t.limit.period === 'daily'){ + if (t.limit.period === 'daily') { const numDays = monthUtils.differenceInCalendarDays( - monthUtils.addMonths(this.month,1), + monthUtils.addMonths(this.month, 1), this.month, ); this.limitAmount += amountToInteger(t.limit.amount) * numDays; - } else if(t.limit.period === 'weekly') { + } else if (t.limit.period === 'weekly') { const nextMonth = monthUtils.nextMonth(this.month); let week = t.limit.start; const baseLimit = amountToInteger(t.limit.amount); while (week < nextMonth) { - if(week >= this.month) { + if (week >= this.month) { this.limitAmount += baseLimit; } - week = monthUtils.addWeeks(week,1); + week = monthUtils.addWeeks(week, 1); } } else if (t.limit.period === 'monthly') { this.limitAmount = amountToInteger(t.limit.amount); @@ -358,8 +352,8 @@ export class categoryTemplate { } private checkSpend() { - const st = this.templates.filter(t => t.type=== 'spend'); - if(st.length>1){ + const st = this.templates.filter(t => t.type === 'spend'); + if (st.length > 1) { throw new Error('Only one spend template is allowed per category'); } } @@ -485,24 +479,25 @@ export class categoryTemplate { return -Math.round(sum / template.amount); } - private runBy(template, allTemplates, l: number, remainder: number) { + private runBy(template, first: boolean, remainder: number) { let target = 0; - let targetMonth = `${allTemplates[l].month}`; + let targetMonth = `${template.month}`; let numMonths = monthUtils.differenceInCalendarMonths( targetMonth, this.month, ); - const repeat = - template.type === 'by' ? template.repeat : (template.repeat || 1) * 12; + const repeat = template.annual + ? (template.repeat || 1) * 12 + : template.repeat; while (numMonths < 0 && repeat) { targetMonth = monthUtils.addMonths(targetMonth, repeat); numMonths = monthUtils.differenceInCalendarMonths( - allTemplates[l].month, + targetMonth, this.month, ); } - if (l === 0) remainder = this.fromLastMonth; - remainder = amountToInteger(allTemplates[l].amount) - remainder; + if (first) remainder = this.fromLastMonth; + remainder = amountToInteger(template.amount) - remainder; if (remainder >= 0) { target = remainder; remainder = 0; @@ -510,7 +505,8 @@ export class categoryTemplate { target = 0; remainder = Math.abs(remainder); } - return numMonths >= 0 ? Math.round(target / (numMonths + 1)) : 0; + const ret = numMonths >= 0 ? Math.round(target / (numMonths + 1)) : 0; + return { ret, remainder }; } private runSchedule(template_lines) { diff --git a/packages/loot-core/src/server/budget/goals/goalsBy.ts b/packages/loot-core/src/server/budget/goals/goalsBy.ts index a02abe20bb9..f638bdb8b1c 100644 --- a/packages/loot-core/src/server/budget/goals/goalsBy.ts +++ b/packages/loot-core/src/server/budget/goals/goalsBy.ts @@ -26,7 +26,7 @@ export async function goalsBy( while (num_months < 0 && repeat) { target_month = monthUtils.addMonths(target_month, repeat); num_months = monthUtils.differenceInCalendarMonths( - template_lines[l].month, + target_month, current_month, ); } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index bfe26f871aa..73e9a4dfc84 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -139,10 +139,10 @@ async function processTemplate( } //break early if nothing to do, or there are errors - if(catObjects.length===0 && errors.length===0){ + if (catObjects.length === 0 && errors.length === 0) { errors.push('Everything is up to date'); } - if(errors.length>0){ + if (errors.length > 0) { return { sticky: true, message: `There were errors interpreting some templates:`, @@ -192,5 +192,5 @@ async function processTemplate( return { type: 'message', message: `Successfully applied templates to ${catObjects.length} categories`, - } + }; } From b1e78b4c0871dabde582f7fb3f094a67eef95905 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 12:10:52 -0700 Subject: [PATCH 12/31] mostly working --- .../src/server/budget/categoryTemplate.ts | 64 ++++++++++++++----- .../src/server/budget/goaltemplates.ts | 2 + 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index cc1d635e994..0649057332c 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -5,6 +5,7 @@ import { amountToInteger } from '../../shared/util'; import * as db from '../db'; import { getSheetValue, setBudget, setGoal } from './actions'; +import { goalsSchedule } from './goals/goalsSchedule'; import { getActiveSchedules } from './statements'; //import { Template } from './types/templates'; @@ -36,7 +37,7 @@ export class categoryTemplate { `leftover-${categoryID}`, ); // run all checks - await categoryTemplate.checkByAndSchedule(templates); + await categoryTemplate.checkByAndScheduleAndSpend(templates,month); await categoryTemplate.checkPercentage(templates); // call the private constructor return new categoryTemplate(templates, categoryID, month, fromLastMonth); @@ -53,7 +54,7 @@ export class categoryTemplate { runAll(available: number) { let toBudget: number = 0; this.priorities.forEach(async p => { - toBudget += await this.runTemplatesForPriority(p, available); + toBudget += await this.runTemplatesForPriority(p, available,available); }); //TODO does this need to run limits? maybe pass in option to ignore previous balance? return toBudget; @@ -61,7 +62,7 @@ export class categoryTemplate { // run all templates in a given priority level // return: amount budgeted in this priority level - async runTemplatesForPriority(priority: number, budgetAvail: number) { + async runTemplatesForPriority(priority: number, budgetAvail: number, availStart: number) { if (!this.priorities.includes(priority)) return 0; const t = this.templates.filter(t => t.priority === priority); @@ -69,6 +70,7 @@ export class categoryTemplate { let toBudget = 0; let first = true; // needed for by templates let remainder = 0; + let scheduleFlag = false; // switch on template type and calculate the amount for the line for (let i = 0; i < t.length; i++) { switch (t[i].type) { @@ -89,7 +91,7 @@ export class categoryTemplate { break; } case 'percentage': { - toBudget += await this.runPercentage(t[i], budgetAvail); + toBudget += await this.runPercentage(t[i], availStart); break; } case 'by': { @@ -101,8 +103,26 @@ export class categoryTemplate { break; } case 'schedule': { - //TODO add the logic to run all of these at once or whatever needs to happen - toBudget += this.runSchedule(t[i]); + //TODO add this...... + //TODO remember to trim the schedule name + const budgeted = getSheetValue( + monthUtils.sheetForMonth(this.month), + `leftover-${this.categoryID}`, + ); + const ret = await goalsSchedule( + scheduleFlag, + t, + this.month, + budgeted, + remainder, + this.fromLastMonth, + toBudget, + [], + this.categoryID, + ); + toBudget = ret.to_budget; + remainder = ret.remainder; + scheduleFlag = ret.scheduleFlag; break; } case 'average': { @@ -140,10 +160,12 @@ export class categoryTemplate { this.toBudgetAmount = this.limitAmount - this.fromLastMonth; return orig - this.toBudgetAmount; } + return 0; } // run all of the 'remainder' type templates runRemainder(budgetAvail: number, perWeight: number) { + if(this.remainder.length===0) return 0; const toBudget = Math.round(this.remainderWeight * perWeight); //check possible overbudget from rounding, 1cent leftover if (toBudget > budgetAvail) { @@ -207,6 +229,7 @@ export class categoryTemplate { // check limits here since it needs to save states inside the object this.checkLimit(); this.checkSpend(); + this.checkGoal(); //find priorities const p = []; @@ -236,9 +259,6 @@ export class categoryTemplate { this.goalAmount = amountToInteger(this.goals[0].amount); return; } - if (this.goals.length > 1) { - throw new Error(`Can only have one #goal per category`); - } this.goalAmount = this.toBudgetAmount; } @@ -261,7 +281,7 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Template Validation - static async checkByAndSchedule(templates) { + static async checkByAndScheduleAndSpend(templates, month) { //check schedule names const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); templates @@ -291,6 +311,18 @@ export class categoryTemplate { //t.priority = lowestPriority; } }); + // check if the target date is past and not repeating + templates + .filter(t => t.type === 'by' || t.type === 'spend') + .forEach(t => { + const range = monthUtils.differenceInCalendarMonths( + `${t.month}`, + month, + ); + if(range < 0 && !t.annual){ + throw new Error(`Target month has passed, remove or update the target month`); + } + }); } static async checkPercentage(templates) { @@ -358,6 +390,12 @@ export class categoryTemplate { } } + private checkGoal(){ + if(this.goals.length>1) { + throw new Error(`Only one #goal is allowed per category`); + } + } + //----------------------------------------------------------------------------- // Processor Functions @@ -509,9 +547,5 @@ export class categoryTemplate { return { ret, remainder }; } - private runSchedule(template_lines) { - //TODO add this...... - //TODO remember to trim the schedule name - return 0; - } + //private async runSchedule(template_lines) {} } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 73e9a4dfc84..2c0a207fa5f 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -159,10 +159,12 @@ async function processTemplate( // run each priority level for (let pi = 0; pi < priorities.length; pi++) { + const availStart = availBudget; for (let i = 0; i < catObjects.length; i++) { const ret = await catObjects[i].runTemplatesForPriority( priorities[pi], availBudget, + availStart, ); availBudget -= ret; if (availBudget <= 0) { From a0fdfd4e1e51225dfdc2e5c7ef9c274890583d72 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 12:43:40 -0700 Subject: [PATCH 13/31] some fixes, make faster --- .../src/server/budget/categoryTemplate.ts | 24 ++--------- .../src/server/budget/goaltemplates.ts | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 0649057332c..73d80e8aa5e 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -178,10 +178,9 @@ export class categoryTemplate { return toBudget; } - runFinish() { + getValues() { this.runGoal(); - this.setBudget(); - this.setGoal(); + return { budgeted: this.toBudgetAmount, goal: this.goalAmount} } //----------------------------------------------------------------------------- @@ -262,23 +261,6 @@ export class categoryTemplate { this.goalAmount = this.toBudgetAmount; } - private setBudget() { - setBudget({ - category: this.categoryID, - month: this.month, - amount: this.toBudgetAmount, - }); - } - - private setGoal() { - setGoal({ - category: this.categoryID, - goal: this.goalAmount, - month: this.month, - long_goal: this.isLongGoal ? 1 : 0, - }); - } - //----------------------------------------------------------------------------- // Template Validation static async checkByAndScheduleAndSpend(templates, month) { @@ -319,7 +301,7 @@ export class categoryTemplate { `${t.month}`, month, ); - if(range < 0 && !t.annual){ + if(range < 0 && !(t.repeat || t.annual)){ throw new Error(`Target month has passed, remove or update the target month`); } }); diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 2c0a207fa5f..ca1fd816cc7 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -4,18 +4,18 @@ import * as monthUtils from '../../shared/months'; import * as db from '../db'; import { batchMessages } from '../sync'; -import { isReflectBudget, getSheetValue, setGoal } from './actions'; +import { isReflectBudget, getSheetValue, setGoal, setBudget } from './actions'; import { categoryTemplate } from './categoryTemplate'; import { checkTemplates, storeTemplates } from './template-notes'; -export async function applyTemplate({ month }) { +export async function applyTemplate({ month }): Promise { await storeTemplates(); const category_templates = await getTemplates(null); const ret = await processTemplate(month, false, category_templates, null); return ret; } -export async function overwriteTemplate({ month }) { +export async function overwriteTemplate({ month }) :Promise { await storeTemplates(); const category_templates = await getTemplates(null); const ret = await processTemplate(month, true, category_templates, null); @@ -79,14 +79,37 @@ async function getTemplates(category) { } } +async function setBudgets( month, templateBudget ) { + await batchMessages(async () => { + templateBudget.forEach(element => { + setBudget({ + category: element.category, + month, + amount: element.budgeted, + }); + }); + }); +} + +async function setGoals( month, idealTemplate ) { + await batchMessages(async () => { + idealTemplate.forEach(element => { + setGoal({ + category: element.category, + goal: element.goal, + month, + long_goal: 0, + }); + }); + }); +} + async function processTemplate( month, force: boolean, categoryTemplates, categoriesIn?: any[], -) { - // get all categoryIDs that need processed - //done? +) : Promise { // setup objects for each category and catch errors let categories = []; if (!categoriesIn) { @@ -187,9 +210,15 @@ async function processTemplate( }); } // finish + let budgetList= []; + let goalList= []; catObjects.forEach(o => { - o.runFinish(); + const ret = o.getValues(); + budgetList.push({category: o.categoryID, budgeted: ret.budgeted}); + goalList.push({category: o.categoryID, goal: ret.goal}); }); + await setBudgets(month, budgetList); + await setGoals(month, goalList); return { type: 'message', From bc3000c4a1fb57bc6217001d670f437853d6477c Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 13:24:03 -0700 Subject: [PATCH 14/31] lint, note --- .../src/server/budget/categoryTemplate.ts | 26 ++++++++++++------- .../src/server/budget/goaltemplates.ts | 16 ++++++------ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 73d80e8aa5e..a4190b97343 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -4,7 +4,7 @@ import * as monthUtils from '../../shared/months'; import { amountToInteger } from '../../shared/util'; import * as db from '../db'; -import { getSheetValue, setBudget, setGoal } from './actions'; +import { getSheetValue} from './actions'; import { goalsSchedule } from './goals/goalsSchedule'; import { getActiveSchedules } from './statements'; //import { Template } from './types/templates'; @@ -37,7 +37,7 @@ export class categoryTemplate { `leftover-${categoryID}`, ); // run all checks - await categoryTemplate.checkByAndScheduleAndSpend(templates,month); + await categoryTemplate.checkByAndScheduleAndSpend(templates, month); await categoryTemplate.checkPercentage(templates); // call the private constructor return new categoryTemplate(templates, categoryID, month, fromLastMonth); @@ -54,7 +54,7 @@ export class categoryTemplate { runAll(available: number) { let toBudget: number = 0; this.priorities.forEach(async p => { - toBudget += await this.runTemplatesForPriority(p, available,available); + toBudget += await this.runTemplatesForPriority(p, available, available); }); //TODO does this need to run limits? maybe pass in option to ignore previous balance? return toBudget; @@ -62,7 +62,11 @@ export class categoryTemplate { // run all templates in a given priority level // return: amount budgeted in this priority level - async runTemplatesForPriority(priority: number, budgetAvail: number, availStart: number) { + async runTemplatesForPriority( + priority: number, + budgetAvail: number, + availStart: number, + ) { if (!this.priorities.includes(priority)) return 0; const t = this.templates.filter(t => t.priority === priority); @@ -165,7 +169,7 @@ export class categoryTemplate { // run all of the 'remainder' type templates runRemainder(budgetAvail: number, perWeight: number) { - if(this.remainder.length===0) return 0; + if (this.remainder.length === 0) return 0; const toBudget = Math.round(this.remainderWeight * perWeight); //check possible overbudget from rounding, 1cent leftover if (toBudget > budgetAvail) { @@ -180,7 +184,7 @@ export class categoryTemplate { getValues() { this.runGoal(); - return { budgeted: this.toBudgetAmount, goal: this.goalAmount} + return { budgeted: this.toBudgetAmount, goal: this.goalAmount }; } //----------------------------------------------------------------------------- @@ -301,8 +305,10 @@ export class categoryTemplate { `${t.month}`, month, ); - if(range < 0 && !(t.repeat || t.annual)){ - throw new Error(`Target month has passed, remove or update the target month`); + if (range < 0 && !(t.repeat || t.annual)) { + throw new Error( + `Target month has passed, remove or update the target month`, + ); } }); } @@ -372,8 +378,8 @@ export class categoryTemplate { } } - private checkGoal(){ - if(this.goals.length>1) { + private checkGoal() { + if (this.goals.length > 1) { throw new Error(`Only one #goal is allowed per category`); } } diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index ca1fd816cc7..00c353c6c2d 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -15,7 +15,7 @@ export async function applyTemplate({ month }): Promise { return ret; } -export async function overwriteTemplate({ month }) :Promise { +export async function overwriteTemplate({ month }): Promise { await storeTemplates(); const category_templates = await getTemplates(null); const ret = await processTemplate(month, true, category_templates, null); @@ -79,7 +79,7 @@ async function getTemplates(category) { } } -async function setBudgets( month, templateBudget ) { +async function setBudgets(month, templateBudget) { await batchMessages(async () => { templateBudget.forEach(element => { setBudget({ @@ -91,7 +91,7 @@ async function setBudgets( month, templateBudget ) { }); } -async function setGoals( month, idealTemplate ) { +async function setGoals(month, idealTemplate) { await batchMessages(async () => { idealTemplate.forEach(element => { setGoal({ @@ -109,7 +109,7 @@ async function processTemplate( force: boolean, categoryTemplates, categoriesIn?: any[], -) : Promise { +): Promise { // setup objects for each category and catch errors let categories = []; if (!categoriesIn) { @@ -210,12 +210,12 @@ async function processTemplate( }); } // finish - let budgetList= []; - let goalList= []; + const budgetList = []; + const goalList = []; catObjects.forEach(o => { const ret = o.getValues(); - budgetList.push({category: o.categoryID, budgeted: ret.budgeted}); - goalList.push({category: o.categoryID, goal: ret.goal}); + budgetList.push({ category: o.categoryID, budgeted: ret.budgeted }); + goalList.push({ category: o.categoryID, goal: ret.goal }); }); await setBudgets(month, budgetList); await setGoals(month, goalList); From 87a7132bb7375bf932fd5f5ef66b0de794552d49 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 13:25:58 -0700 Subject: [PATCH 15/31] note --- upcoming-release-notes/3754.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 upcoming-release-notes/3754.md diff --git a/upcoming-release-notes/3754.md b/upcoming-release-notes/3754.md new file mode 100644 index 00000000000..a2ecd8f273b --- /dev/null +++ b/upcoming-release-notes/3754.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [youngcw] +--- + +Reorganize goal template code From 4f346bfe5dd225045ea823c8b7d6cf7793f67a6e Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 13:31:59 -0700 Subject: [PATCH 16/31] cleanup old stuff --- .../src/server/budget/categoryTemplate.ts | 6 +- .../server/budget/goals/goalsAverage.test.ts | 89 --------- .../src/server/budget/goals/goalsAverage.ts | 30 --- .../src/server/budget/goals/goalsBy.test.ts | 134 ------------- .../src/server/budget/goals/goalsBy.ts | 49 ----- .../src/server/budget/goals/goalsCopy.ts | 38 ---- .../budget/goals/goalsPercentage.test.ts | 180 ------------------ .../server/budget/goals/goalsPercentage.ts | 56 ------ .../budget/goals/goalsRemainder.test.ts | 79 -------- .../src/server/budget/goals/goalsRemainder.ts | 47 ----- .../server/budget/goals/goalsSimple.test.ts | 147 -------------- .../src/server/budget/goals/goalsSimple.ts | 14 -- .../server/budget/goals/goalsSpend.test.ts | 105 ---------- .../src/server/budget/goals/goalsSpend.ts | 53 ------ .../src/server/budget/goals/goalsWeek.test.ts | 124 ------------ .../src/server/budget/goals/goalsWeek.ts | 37 ---- .../budget/{goals => }/goalsSchedule.test.ts | 0 .../budget/{goals => }/goalsSchedule.ts | 0 18 files changed, 3 insertions(+), 1185 deletions(-) delete mode 100644 packages/loot-core/src/server/budget/goals/goalsAverage.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsAverage.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsBy.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsBy.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsCopy.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsPercentage.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsPercentage.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsRemainder.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsRemainder.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsSimple.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsSimple.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsSpend.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsSpend.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsWeek.test.ts delete mode 100644 packages/loot-core/src/server/budget/goals/goalsWeek.ts rename packages/loot-core/src/server/budget/{goals => }/goalsSchedule.test.ts (100%) rename packages/loot-core/src/server/budget/{goals => }/goalsSchedule.ts (100%) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index a4190b97343..fa82bc18b9d 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -4,8 +4,8 @@ import * as monthUtils from '../../shared/months'; import { amountToInteger } from '../../shared/util'; import * as db from '../db'; -import { getSheetValue} from './actions'; -import { goalsSchedule } from './goals/goalsSchedule'; +import { getSheetValue } from './actions'; +import { goalsSchedule } from './goalsSchedule'; import { getActiveSchedules } from './statements'; //import { Template } from './types/templates'; @@ -20,7 +20,7 @@ export class categoryTemplate { * 3. run each priority level that is needed via runTemplatesForPriority * 4. run applyLimits to apply any existing limit to the category * 5. run the remainder templates via runRemainder (limits get applied at the start of this) - * 6. finish processing by running runFinish() + * 6. finish processing by running getValues() for batch processing. * Alternate: * If the situation calls for it you can run all templates in a catagory in one go using the * method runAll which will run all templates and goals for reference, and can optionally be saved diff --git a/packages/loot-core/src/server/budget/goals/goalsAverage.test.ts b/packages/loot-core/src/server/budget/goals/goalsAverage.test.ts deleted file mode 100644 index 07285d34c16..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsAverage.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as actions from '../actions'; - -import { goalsAverage } from './goalsAverage'; - -jest.mock('../actions'); - -describe('goalsAverage', () => { - const mockGetSheetValue = actions.getSheetValue as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should calculate the amount to budget based on the average of only one previous month of spending', async () => { - // Given - const template = { amount: 1 }; - const month = '2024-07'; - const category = { id: 1 }; - const errors: string[] = []; - const to_budget = 0; - - mockGetSheetValue.mockResolvedValueOnce(200); - - // When - const result = await goalsAverage( - template, - month, - category, - errors, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(-200); - expect(result.errors).toHaveLength(0); - }); - - it('should calculate the amount to budget based on the average of multiple previous months of spending', async () => { - // Given - const template = { amount: 4 }; - const month = '2024-08'; - const category = { id: 1 }; - const errors: string[] = []; - const to_budget = 0; - - mockGetSheetValue - .mockResolvedValueOnce(200) - .mockResolvedValueOnce(300) - .mockResolvedValueOnce(100) - .mockResolvedValueOnce(400); - - // When - const result = await goalsAverage( - template, - month, - category, - errors, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(-250); - expect(result.errors).toHaveLength(0); - }); - - it('should return error when template amount passed in is <= 0', async () => { - // Given - const template = { amount: 0 }; - const month = '2024-08'; - const category = { id: 1 }; - const errors: string[] = []; - const to_budget = 1000; - - // When - const result = await goalsAverage( - template, - month, - category, - errors, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(1000); - expect(result.errors).toStrictEqual([ - 'Number of months to average is not valid', - ]); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsAverage.ts b/packages/loot-core/src/server/budget/goals/goalsAverage.ts deleted file mode 100644 index 2dd7472ad06..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsAverage.ts +++ /dev/null @@ -1,30 +0,0 @@ -// @ts-strict-ignore - -import * as monthUtils from '../../../shared/months'; -import { getSheetValue } from '../actions'; - -export async function goalsAverage( - template, - month, - category, - errors, - to_budget, -) { - let increment = 0; - if (template.amount) { - let sum = 0; - for (let i = 1; i <= template.amount; i++) { - // add up other months - const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(month, i), - ); - sum += await getSheetValue(sheetName, `sum-amount-${category.id}`); - } - increment = sum / template.amount; - } else { - errors.push('Number of months to average is not valid'); - return { to_budget, errors }; - } - to_budget += -Math.round(increment); - return { to_budget, errors }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsBy.test.ts b/packages/loot-core/src/server/budget/goals/goalsBy.test.ts deleted file mode 100644 index cafec5ae716..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsBy.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import * as actions from '../actions'; - -import { goalsBy } from './goalsBy'; - -jest.mock('../actions'); - -describe('goalsBy', () => { - const mockIsReflectBudget = actions.isReflectBudget as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should return correct budget amount with target in the future and no current balance', async () => { - // Given - const template = { amount: 100, month: '2024-12' }; - const current_month = '2024-08'; - const last_month_balance = 0; - const to_budget = 0; - const errors: string[] = []; - const template_lines = [template]; - const l = 0; - const remainder = 0; - mockIsReflectBudget.mockReturnValue(false); - - // When - const result = await goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(2000); - expect(result.errors).toHaveLength(0); - expect(result.remainder).toBe(0); - }); - - it('should return correct budget amount with target in the future and existing balance towards goal', async () => { - // Given - const template = { amount: 100, month: '2024-12' }; - const current_month = '2024-08'; - const last_month_balance = 5000; - const to_budget = 0; - const errors: string[] = []; - const template_lines = [template]; - const l = 0; - const remainder = 0; - mockIsReflectBudget.mockReturnValue(false); - - // When - const result = await goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(1000); - expect(result.errors).toHaveLength(0); - expect(result.remainder).toBe(0); - }); - - it('should return correct budget amount when target balance met early', async () => { - // Given - const template = { amount: 100, month: '2024-12' }; - const current_month = '2024-08'; - const last_month_balance = 10000; - const to_budget = 0; - const errors: string[] = []; - const template_lines = [template]; - const l = 0; - const remainder = 0; - mockIsReflectBudget.mockReturnValue(false); - - // When - const result = await goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toHaveLength(0); - expect(result.remainder).toBe(0); - }); - - it('should return error when budget is a reflect budget', async () => { - // Given - const template = { amount: -100, month: '2024-08', repeat: 1 }; - const current_month = '2024-08'; - const last_month_balance = 0; - const to_budget = 0; - const errors: string[] = []; - const template_lines = [template]; - const l = 0; - const remainder = 0; - mockIsReflectBudget.mockReturnValue(true); - - // When - const result = await goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toStrictEqual([ - 'by templates are not supported in Report budgets', - ]); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsBy.ts b/packages/loot-core/src/server/budget/goals/goalsBy.ts deleted file mode 100644 index f638bdb8b1c..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsBy.ts +++ /dev/null @@ -1,49 +0,0 @@ -// @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import { amountToInteger } from '../../../shared/util'; -import { isReflectBudget } from '../actions'; - -export async function goalsBy( - template_lines, - current_month, - template, - l, - remainder, - last_month_balance, - to_budget, - errors, -) { - // by has 'amount' and 'month' params - if (!isReflectBudget()) { - let target = 0; - let target_month = `${template_lines[l].month}-01`; - let num_months = monthUtils.differenceInCalendarMonths( - target_month, - current_month, - ); - const repeat = - template.type === 'by' ? template.repeat : (template.repeat || 1) * 12; - while (num_months < 0 && repeat) { - target_month = monthUtils.addMonths(target_month, repeat); - num_months = monthUtils.differenceInCalendarMonths( - target_month, - current_month, - ); - } - if (l === 0) remainder = last_month_balance; - remainder = amountToInteger(template_lines[l].amount) - remainder; - if (remainder >= 0) { - target = remainder; - remainder = 0; - } else { - target = 0; - remainder = Math.abs(remainder); - } - const increment = - num_months >= 0 ? Math.round(target / (num_months + 1)) : 0; - to_budget += increment; - } else { - errors.push(`by templates are not supported in Report budgets`); - } - return { to_budget, errors, remainder }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsCopy.ts b/packages/loot-core/src/server/budget/goals/goalsCopy.ts deleted file mode 100644 index 7932cd7447d..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsCopy.ts +++ /dev/null @@ -1,38 +0,0 @@ -// @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import { amountToInteger } from '../../../shared/util'; -import { getSheetValue } from '../actions'; - -export async function goalsCopy( - template, - month, - category, - limitCheck, - errors, - limit, - hold, - to_budget, -) { - // simple has 'monthly' and/or 'limit' params - if (template.limit != null) { - if (limitCheck) { - errors.push(`More than one “up to” limit found.`); - return { to_budget, errors, limit, limitCheck, hold }; - } else { - limitCheck = true; - limit = amountToInteger(template.limit.amount); - hold = template.limit.hold; - } - } - let increment = 0; - if (template.lookBack) { - const sheetName = monthUtils.sheetForMonth( - monthUtils.subMonths(month, template.lookBack), - ); - increment = await getSheetValue(sheetName, `budget-${category.id}`); - } else { - errors.push('Missing number of months to look back'); - } - to_budget += increment; - return { to_budget, errors, limit, limitCheck, hold }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsPercentage.test.ts b/packages/loot-core/src/server/budget/goals/goalsPercentage.test.ts deleted file mode 100644 index 4dca6ac3706..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsPercentage.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import * as db from '../../db'; -import * as actions from '../actions'; - -import { goalsPercentage } from './goalsPercentage'; - -jest.mock('../actions'); -jest.mock('../../db'); - -describe('goalsPercentage', () => { - const mockGetSheetValue = actions.getSheetValue as jest.Mock; - const mockGetCategories = db.getCategories as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should calculate the budget based on a percentage of all income for the current month', async () => { - // Given - const template = { percent: 10, category: 'all income' }; - const month = '2024-08'; - const available_start = 0; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - mockGetSheetValue.mockResolvedValueOnce(1000); - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(100); - expect(result.errors).toHaveLength(0); - }); - - it('should calculate the budget based on a percentage of all income for the previous month', async () => { - // Given - const template = { percent: 10, category: 'all income', previous: true }; - const month = '2024-08'; - const available_start = 0; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - mockGetSheetValue.mockResolvedValueOnce(1000); - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(100); - expect(result.errors).toHaveLength(0); - }); - - it('should calculate the budget based on a percentage of available funds', async () => { - // Given - const template = { percent: 10, category: 'available funds' }; - const month = '2024-08'; - const available_start = 1000; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(100); - expect(result.errors).toHaveLength(0); - }); - - it('should calculate the budget based on a percentage of a specific income category for the current month', async () => { - // Given - const template = { percent: 10, category: 'Salary' }; - const month = '2024-08'; - const available_start = 0; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - mockGetCategories.mockResolvedValueOnce([ - { id: 1, name: 'Salary', is_income: true }, - ]); - mockGetSheetValue.mockResolvedValueOnce(1000); - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(100); - expect(result.errors).toHaveLength(0); - }); - - it('should calculate the budget based on a percentage of a specific income category for the previous month', async () => { - // Given - const template = { percent: 10, category: 'Salary', previous: true }; - const month = '2024-08'; - const available_start = 0; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - mockGetCategories.mockResolvedValueOnce([ - { id: 1, name: 'Salary', is_income: true }, - ]); - mockGetSheetValue.mockResolvedValueOnce(1000); - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(100); - expect(result.errors).toHaveLength(0); - }); - - it('should return an error if the specified income category does not exist', async () => { - // Given - const template = { percent: 10, category: 'NonExistentCategory' }; - const month = '2024-08'; - const available_start = 0; - const sheetName = '2024-08'; - const to_budget = 0; - const errors: string[] = []; - - mockGetCategories.mockResolvedValueOnce([ - { id: 1, name: 'Salary', is_income: true }, - ]); - - // When - const result = await goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toStrictEqual([ - 'Could not find category “NonExistentCategory”', - ]); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsPercentage.ts b/packages/loot-core/src/server/budget/goals/goalsPercentage.ts deleted file mode 100644 index 175d5efe560..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsPercentage.ts +++ /dev/null @@ -1,56 +0,0 @@ -// @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import * as db from '../../db'; -import { getSheetValue } from '../actions'; - -export async function goalsPercentage( - template, - month, - available_start, - sheetName, - to_budget, - errors, -) { - const percent = template.percent; - let monthlyIncome = 0; - - if (template.category.toLowerCase() === 'all income') { - if (template.previous) { - const sheetName_lastmonth = monthUtils.sheetForMonth( - monthUtils.addMonths(month, -1), - ); - monthlyIncome = await getSheetValue(sheetName_lastmonth, 'total-income'); - } else { - monthlyIncome = await getSheetValue(sheetName, `total-income`); - } - } else if (template.category.toLowerCase() === 'available funds') { - monthlyIncome = available_start; - } else { - const income_category = (await db.getCategories()).find( - c => - c.is_income && c.name.toLowerCase() === template.category.toLowerCase(), - ); - if (!income_category) { - errors.push(`Could not find category “${template.category}”`); - return { to_budget, errors }; - } - if (template.previous) { - const sheetName_lastmonth = monthUtils.sheetForMonth( - monthUtils.addMonths(month, -1), - ); - monthlyIncome = await getSheetValue( - sheetName_lastmonth, - `sum-amount-${income_category.id}`, - ); - } else { - monthlyIncome = await getSheetValue( - sheetName, - `sum-amount-${income_category.id}`, - ); - } - } - - const increment = Math.max(0, Math.round(monthlyIncome * (percent / 100))); - to_budget += increment; - return { to_budget, errors }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsRemainder.test.ts b/packages/loot-core/src/server/budget/goals/goalsRemainder.test.ts deleted file mode 100644 index b68b9897d96..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsRemainder.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { goalsRemainder } from './goalsRemainder'; - -describe('goalsRemainder', () => { - it('should calculate the budget correctly when remainder_scale is greater than 0', async () => { - // Given - const template = { weight: 100 }; - const budgetAvailable = 1000; - const remainder_scale = 0.5; - const to_budget = 0; - - // When - const result = await goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(50); - }); - - it('should calculate the budget correctly when remainder_scale is 0', async () => { - // Given - const template = { weight: 100 }; - const budgetAvailable = 1000; - const remainder_scale = 0; - const to_budget = 0; - - // When - const result = await goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(100); - }); - - it('should calculate the budget correctly when when the calculated budget exceeds the budget available', async () => { - // Given - const template = { weight: 1000 }; - const budgetAvailable = 500; - const remainder_scale = 1; - const to_budget = 0; - - // When - const result = await goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(500); - }); - - it('should calculate the budget correctly when there is 1 minor unit leftover from rounding', async () => { - // Given - const template = { weight: 499 }; - const budgetAvailable = 500; - const remainder_scale = 1; - const to_budget = 0; - - // When - const result = await goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, - ); - - // Then - expect(result.to_budget).toBe(500); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsRemainder.ts b/packages/loot-core/src/server/budget/goals/goalsRemainder.ts deleted file mode 100644 index 3b7e1d3be8e..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsRemainder.ts +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-strict-ignore -export async function goalsRemainder( - template, - budgetAvailable, - remainder_scale, - to_budget, -) { - if (remainder_scale >= 0) { - to_budget += - remainder_scale === 0 - ? Math.round(template.weight) - : Math.round(remainder_scale * template.weight); - // can over budget with the rounding, so checking that - if (to_budget >= budgetAvailable) { - to_budget = budgetAvailable; - // check if there is 1 cent leftover from rounding - } else if (budgetAvailable - to_budget === 1) { - to_budget = to_budget + 1; - } - } - return { to_budget }; -} - -export function findRemainder(priority_list, categories, category_templates) { - // find all remainder templates, place them at highest priority - let remainder_found; - let remainder_weight_total = 0; - const remainder_priority = priority_list[priority_list.length - 1] + 1; - for (let c = 0; c < categories.length; c++) { - const category = categories[c]; - const templates = category_templates[category.id]; - if (templates) { - for (let i = 0; i < templates.length; i++) { - if (templates[i].type === 'remainder') { - templates[i].priority = remainder_priority; - remainder_weight_total += templates[i].weight; - remainder_found = true; - } - } - } - } - return { - remainder_found, - remainder_priority, - remainder_weight_total, - }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsSimple.test.ts b/packages/loot-core/src/server/budget/goals/goalsSimple.test.ts deleted file mode 100644 index fd7f2ac2ed1..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsSimple.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { goalsSimple } from './goalsSimple'; - -describe('goalsSimple', () => { - it('should return correct budget amount when limit set and no balance left from previous months', async () => { - // Given - const template = { limit: { amount: 100, hold: false } }; - const limitCheck = false; - const errors: string[] = []; - const limit = 0; - const hold = false; - const to_budget = 0; - const last_month_balance = 0; - - // When - const result = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - - // Then - expect(result.to_budget).toBe(10000); - expect(result.errors).toHaveLength(0); - expect(result.limitCheck).toBe(true); - expect(result.limit).toBe(10000); - expect(result.hold).toBe(false); - }); - - it('should return correct budget amount when limit set and balance from previous month left over', async () => { - // Given - const template = { limit: { amount: 100, hold: false } }; - const limitCheck = false; - const errors: string[] = []; - const limit = 0; - const hold = false; - const to_budget = 0; - const last_month_balance = 2000; - - // When - const result = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - - // Then - expect(result.to_budget).toBe(8000); - expect(result.errors).toHaveLength(0); - expect(result.limitCheck).toBe(true); - expect(result.limit).toBe(10000); - }); - - it('should return correct budget amount when assigned from previous month is greater than the limit set', async () => { - // Given - const template = { limit: { amount: 100, hold: false } }; - const limitCheck = false; - const errors: string[] = []; - const limit = 0; - const hold = false; - const to_budget = 0; - const last_month_balance = 20000; - - // When - const result = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - - // Then - expect(result.to_budget).toBe(-10000); - expect(result.errors).toHaveLength(0); - expect(result.limitCheck).toBe(true); - expect(result.limit).toBe(10000); - expect(result.hold).toBe(false); - }); - - it('should return correct budget amount when both limit and monthly limit set', async () => { - // Given - const template = { monthly: 50, limit: { amount: 100, hold: false } }; - const limitCheck = false; - const errors: string[] = []; - const limit = 0; - const hold = false; - const to_budget = 0; - const last_month_balance = 0; - - // When - const result = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - - // Then - expect(result.to_budget).toBe(5000); - expect(result.errors).toHaveLength(0); - expect(result.limitCheck).toBe(true); - expect(result.limit).toBe(10000); - expect(result.hold).toBe(false); - }); - - it('should fail when multiple limit checks exist', async () => { - // Given - const template = { limit: { amount: 100, hold: true } }; - const limitCheck = true; - const errors: string[] = []; - const limit = 0; - const hold = true; - const to_budget = 0; - const last_month_balance = 200; - - // When - const result = await goalsSimple( - template, - limitCheck, - errors, - limit, - hold, - to_budget, - last_month_balance, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toStrictEqual(['More than one “up to” limit found.']); - expect(result.limitCheck).toBe(true); - expect(result.limit).toBe(0); - expect(result.hold).toBe(true); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsSimple.ts b/packages/loot-core/src/server/budget/goals/goalsSimple.ts deleted file mode 100644 index 05d3a112f21..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsSimple.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @ts-strict-ignore -import { amountToInteger } from '../../../shared/util'; - -export async function goalsSimple(template, limit) { - // simple has 'monthly' and/or 'limit' params - let increment = 0; - if (template.monthly != null) { - const monthly = amountToInteger(template.monthly); - increment = monthly; - } else { - increment = limit; - } - return { increment }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsSpend.test.ts b/packages/loot-core/src/server/budget/goals/goalsSpend.test.ts deleted file mode 100644 index 0b69e800e94..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsSpend.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getSheetValue } from '../actions'; - -import { goalsSpend } from './goalsSpend'; - -jest.mock('../actions'); - -describe('goalsSpend', () => { - const mockGetSheetValue = getSheetValue as jest.Mock; - - beforeEach(() => { - jest.clearAllMocks(); - }); - - it('should return correct budget amount for range when no spending has happened', async () => { - // Given - const template = { amount: 60, from: '2024-01', month: '2024-12' }; - const last_month_balance = 0; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - const category = { id: 'uuid' }; - - mockGetSheetValue - .mockResolvedValueOnce(0) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500); - - // When - const result = await goalsSpend( - template, - last_month_balance, - current_month, - to_budget, - errors, - category, - ); - - // Then - expect(result.to_budget).toBe(500); - expect(result.errors).toHaveLength(0); - }); - - it('should return correct budget amount for range when spending has happened', async () => { - // Given - const template = { amount: 60, from: '2024-01', month: '2024-12' }; - const last_month_balance = 0; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - const category = { id: 'uuid' }; - - mockGetSheetValue - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500) - .mockResolvedValueOnce(500); - - // When - const result = await goalsSpend( - template, - last_month_balance, - current_month, - to_budget, - errors, - category, - ); - - // Then - expect(result.to_budget).toBe(600); - expect(result.errors).toHaveLength(0); - }); - - it('should return error when range is in the past', async () => { - // Given - const template = { amount: 60, from: '2024-01', month: '2024-05' }; - const last_month_balance = 0; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - const category = { id: 'uuid' }; - - // When - const result = await goalsSpend( - template, - last_month_balance, - current_month, - to_budget, - errors, - category, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toStrictEqual(['2024-05 is in the past.']); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsSpend.ts b/packages/loot-core/src/server/budget/goals/goalsSpend.ts deleted file mode 100644 index ca03c0072c2..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsSpend.ts +++ /dev/null @@ -1,53 +0,0 @@ -// @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import { amountToInteger } from '../../../shared/util'; -import { getSheetValue } from '../actions'; - -export async function goalsSpend( - template, - last_month_balance, - current_month, - to_budget, - errors, - category, -) { - // spend has 'amount' and 'from' and 'month' params - const from_month = `${template.from}-01`; - const to_month = `${template.month}-01`; - let already_budgeted = last_month_balance; - let first_month = true; - for ( - let m = from_month; - monthUtils.differenceInCalendarMonths(current_month, m) > 0; - m = monthUtils.addMonths(m, 1) - ) { - const sheetName = monthUtils.sheetForMonth(monthUtils.format(m, 'yyyy-MM')); - - if (first_month) { - const spent = await getSheetValue(sheetName, `sum-amount-${category.id}`); - const balance = await getSheetValue(sheetName, `leftover-${category.id}`); - already_budgeted = balance - spent; - first_month = false; - } else { - const budgeted = await getSheetValue(sheetName, `budget-${category.id}`); - already_budgeted += budgeted; - } - } - const num_months = monthUtils.differenceInCalendarMonths( - to_month, - monthUtils._parse(current_month), - ); - const target = amountToInteger(template.amount); - - let increment = 0; - if (num_months < 0) { - errors.push(`${template.month} is in the past.`); - return { to_budget, errors }; - } else if (num_months === 0) { - increment = target - already_budgeted; - } else { - increment = Math.round((target - already_budgeted) / (num_months + 1)); - } - to_budget = increment; - return { to_budget, errors }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsWeek.test.ts b/packages/loot-core/src/server/budget/goals/goalsWeek.test.ts deleted file mode 100644 index c1351ca8cb7..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsWeek.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { goalsWeek } from './goalsWeek'; - -describe('goalsWeek', () => { - it('should return the correct budget amount for a weekly repeat', async () => { - // Given - const template = { amount: 100, starting: '2024-08-01', weeks: 1 }; - const limit = 0; - const limitCheck = false; - const hold = false; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - - // When - const result = await goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(50000); - expect(result.errors).toHaveLength(0); - expect(result.limit).toBe(0); - expect(result.limitCheck).toBe(false); - expect(result.hold).toBe(false); - }); - - it('should return the correct budget amount for a bi-weekly repeat', async () => { - // Given - const template = { amount: '100', starting: '2024-08-01', weeks: 2 }; - const limit = 0; - const limitCheck = false; - const hold = false; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - - // When - const result = await goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(30000); - expect(result.errors).toHaveLength(0); - expect(result.limit).toBe(0); - expect(result.limitCheck).toBe(false); - expect(result.hold).toBe(false); - }); - - it('should return the correct budget when limit set', async () => { - // Given - const template = { amount: 100, starting: '2024-08-01', weeks: 1 }; - const limit = 20000; - const limitCheck = false; - const hold = false; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - - // When - const result = await goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(50000); - expect(result.errors).toHaveLength(0); - expect(result.limit).toBe(20000); - expect(result.limitCheck).toBe(false); - expect(result.hold).toBe(false); - }); - - it('should return error when multiple limit checks exist', async () => { - // Given - const template = { - amount: '100', - starting: '2024-08-01', - weeks: 1, - limit: { amount: 100, hold: true }, - }; - const limit = 1000; - const limitCheck = true; - const hold = false; - const current_month = '2024-08-01'; - const to_budget = 0; - const errors: string[] = []; - - // When - const result = await goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, - ); - - // Then - expect(result.to_budget).toBe(0); - expect(result.errors).toStrictEqual(['More than one “up to” limit found.']); - expect(result.limit).toBe(1000); - expect(result.limitCheck).toBe(true); - expect(result.hold).toBe(false); - }); -}); diff --git a/packages/loot-core/src/server/budget/goals/goalsWeek.ts b/packages/loot-core/src/server/budget/goals/goalsWeek.ts deleted file mode 100644 index 6f05c1ce188..00000000000 --- a/packages/loot-core/src/server/budget/goals/goalsWeek.ts +++ /dev/null @@ -1,37 +0,0 @@ -// @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import { amountToInteger } from '../../../shared/util'; - -export async function goalsWeek( - template, - limit, - limitCheck, - hold, - current_month, - to_budget, - errors, -) { - // week has 'amount', 'starting', 'weeks' and optional 'limit' params - const amount = amountToInteger(template.amount); - const weeks = template.weeks != null ? Math.round(template.weeks) : 1; - if (template.limit != null) { - if (limit > 0) { - errors.push(`More than one “up to” limit found.`); - return { to_budget, errors, limit, limitCheck, hold }; - } else { - limitCheck = true; - limit = amountToInteger(template.limit.amount); - hold = template.limit.hold; - } - } - let w = template.starting; - const next_month = monthUtils.addMonths(current_month, 1); - - while (w < next_month) { - if (w >= current_month) { - to_budget += amount; - } - w = monthUtils.addWeeks(w, weeks); - } - return { to_budget, errors, limit, limitCheck, hold }; -} diff --git a/packages/loot-core/src/server/budget/goals/goalsSchedule.test.ts b/packages/loot-core/src/server/budget/goalsSchedule.test.ts similarity index 100% rename from packages/loot-core/src/server/budget/goals/goalsSchedule.test.ts rename to packages/loot-core/src/server/budget/goalsSchedule.test.ts diff --git a/packages/loot-core/src/server/budget/goals/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts similarity index 100% rename from packages/loot-core/src/server/budget/goals/goalsSchedule.ts rename to packages/loot-core/src/server/budget/goalsSchedule.ts From d8fd63b676a5b1ae4a2fe0a2102889c647f2ca66 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 13:34:10 -0700 Subject: [PATCH 17/31] fix paths --- packages/loot-core/src/server/budget/goalsSchedule.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/loot-core/src/server/budget/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts index 6abeec39b94..39eda93d0ef 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.ts @@ -1,13 +1,13 @@ // @ts-strict-ignore -import * as monthUtils from '../../../shared/months'; -import { extractScheduleConds } from '../../../shared/schedules'; -import * as db from '../../db'; +import * as monthUtils from '../../shared/months'; +import { extractScheduleConds } from '../../shared/schedules'; +import * as db from '../db'; import { getRuleForSchedule, getNextDate, getDateWithSkippedWeekend, -} from '../../schedules/app'; -import { isReflectBudget } from '../actions'; +} from '../schedules/app'; +import { isReflectBudget } from './actions'; async function createScheduleList(template, current_month, category) { const t = []; From 8a76b2fa929687e085736ed87e67fd942e29e7f8 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 13:46:29 -0700 Subject: [PATCH 18/31] test fixes --- packages/loot-core/src/server/budget/goalsSchedule.test.ts | 6 +++--- packages/loot-core/src/server/budget/goalsSchedule.ts | 1 + packages/loot-core/src/server/budget/goaltemplates.ts | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/loot-core/src/server/budget/goalsSchedule.test.ts b/packages/loot-core/src/server/budget/goalsSchedule.test.ts index 29eefba3895..27d3418a7cc 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.test.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.test.ts @@ -1,7 +1,7 @@ -import * as db from '../../db'; -import { getRuleForSchedule } from '../../schedules/app'; -import { isReflectBudget } from '../actions'; +import * as db from '../db'; +import { getRuleForSchedule } from '../schedules/app'; +import { isReflectBudget } from './actions'; import { goalsSchedule } from './goalsSchedule'; jest.mock('../../db'); diff --git a/packages/loot-core/src/server/budget/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts index 39eda93d0ef..431a7ffe0e2 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.ts @@ -7,6 +7,7 @@ import { getNextDate, getDateWithSkippedWeekend, } from '../schedules/app'; + import { isReflectBudget } from './actions'; async function createScheduleList(template, current_month, category) { diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 00c353c6c2d..abcc5a92819 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -108,7 +108,7 @@ async function processTemplate( month, force: boolean, categoryTemplates, - categoriesIn?: any[], + categoriesIn?, ): Promise { // setup objects for each category and catch errors let categories = []; From 10c891e865545e049138ca91f8a5898fb53279d9 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 14:08:09 -0700 Subject: [PATCH 19/31] fix test --- .../loot-core/src/server/budget/goalsSchedule.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/loot-core/src/server/budget/goalsSchedule.test.ts b/packages/loot-core/src/server/budget/goalsSchedule.test.ts index 27d3418a7cc..6fa59205e8a 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.test.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.test.ts @@ -4,10 +4,10 @@ import { getRuleForSchedule } from '../schedules/app'; import { isReflectBudget } from './actions'; import { goalsSchedule } from './goalsSchedule'; -jest.mock('../../db'); -jest.mock('../actions'); -jest.mock('../../schedules/app', () => { - const actualModule = jest.requireActual('../../schedules/app'); +jest.mock('../db'); +jest.mock('./actions'); +jest.mock('../schedules/app', () => { + const actualModule = jest.requireActual('../schedules/app'); return { ...actualModule, getRuleForSchedule: jest.fn(), From 1bda7d291672021489aa3797861b9dfc90f1e2b9 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 14:23:00 -0700 Subject: [PATCH 20/31] fix note --- packages/loot-core/src/server/budget/categoryTemplate.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index fa82bc18b9d..2c707e1677d 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -19,8 +19,8 @@ export class categoryTemplate { * 2. gather needed data for external use. ex: remainder weights, priorities * 3. run each priority level that is needed via runTemplatesForPriority * 4. run applyLimits to apply any existing limit to the category - * 5. run the remainder templates via runRemainder (limits get applied at the start of this) - * 6. finish processing by running getValues() for batch processing. + * 5. run the remainder templates via runRemainder() + * 6. finish processing by running getValues() and saving values for batch processing. * Alternate: * If the situation calls for it you can run all templates in a catagory in one go using the * method runAll which will run all templates and goals for reference, and can optionally be saved From c497c2624d12959f9313603240b2d2f6ad055c2c Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 15:09:54 -0700 Subject: [PATCH 21/31] rabbit, and fix long goal --- .../loot-core/src/server/budget/categoryTemplate.ts | 13 +++++++------ .../loot-core/src/server/budget/goaltemplates.ts | 9 ++++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 2c707e1677d..ce8a11a44f6 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -51,11 +51,12 @@ export class categoryTemplate { } // what is the full requested amount this month - runAll(available: number) { + async runAll(available: number) { let toBudget: number = 0; - this.priorities.forEach(async p => { + for (let i=0; i < this.priorities.length; i++) { + const p = this.priorities[i]; toBudget += await this.runTemplatesForPriority(p, available, available); - }); + } //TODO does this need to run limits? maybe pass in option to ignore previous balance? return toBudget; } @@ -184,7 +185,7 @@ export class categoryTemplate { getValues() { this.runGoal(); - return { budgeted: this.toBudgetAmount, goal: this.goalAmount }; + return { budgeted: this.toBudgetAmount, goal: this.goalAmount, longGoal: this.isLongGoal }; } //----------------------------------------------------------------------------- @@ -196,11 +197,11 @@ export class categoryTemplate { private goals = []; private priorities: number[] = []; private remainderWeight: number = 0; - private toBudgetAmount: number = null; // amount that will be budgeted by the templates + private toBudgetAmount: number = 0; // amount that will be budgeted by the templates private isLongGoal: boolean = null; //defaulting the goals to null so templates can be unset private goalAmount: number = null; private fromLastMonth = 0; // leftover from last month - private limitAmount = null; + private limitAmount = 0; private limitCheck = false; private limitHold = false; diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index abcc5a92819..fd9f431fabf 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -98,7 +98,7 @@ async function setGoals(month, idealTemplate) { category: element.category, goal: element.goal, month, - long_goal: 0, + long_goal: element.long_goal, }); }); }); @@ -163,7 +163,10 @@ async function processTemplate( //break early if nothing to do, or there are errors if (catObjects.length === 0 && errors.length === 0) { - errors.push('Everything is up to date'); + return { + type: 'message', + message: 'Everything is up to date', + }; } if (errors.length > 0) { return { @@ -215,7 +218,7 @@ async function processTemplate( catObjects.forEach(o => { const ret = o.getValues(); budgetList.push({ category: o.categoryID, budgeted: ret.budgeted }); - goalList.push({ category: o.categoryID, goal: ret.goal }); + goalList.push({ category: o.categoryID, goal: ret.goal, longGoal: ret.longGoal }); }); await setBudgets(month, budgetList); await setGoals(month, goalList); From e5c7940bae342d3fd43235939f70bbf60c76aac8 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 15:11:18 -0700 Subject: [PATCH 22/31] lint --- packages/loot-core/src/server/budget/categoryTemplate.ts | 8 ++++++-- packages/loot-core/src/server/budget/goaltemplates.ts | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index ce8a11a44f6..7cf51fb6bbc 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -53,7 +53,7 @@ export class categoryTemplate { // what is the full requested amount this month async runAll(available: number) { let toBudget: number = 0; - for (let i=0; i < this.priorities.length; i++) { + for (let i = 0; i < this.priorities.length; i++) { const p = this.priorities[i]; toBudget += await this.runTemplatesForPriority(p, available, available); } @@ -185,7 +185,11 @@ export class categoryTemplate { getValues() { this.runGoal(); - return { budgeted: this.toBudgetAmount, goal: this.goalAmount, longGoal: this.isLongGoal }; + return { + budgeted: this.toBudgetAmount, + goal: this.goalAmount, + longGoal: this.isLongGoal, + }; } //----------------------------------------------------------------------------- diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index fd9f431fabf..1ef3d7a0185 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -218,7 +218,11 @@ async function processTemplate( catObjects.forEach(o => { const ret = o.getValues(); budgetList.push({ category: o.categoryID, budgeted: ret.budgeted }); - goalList.push({ category: o.categoryID, goal: ret.goal, longGoal: ret.longGoal }); + goalList.push({ + category: o.categoryID, + goal: ret.goal, + longGoal: ret.longGoal, + }); }); await setBudgets(month, budgetList); await setGoals(month, goalList); From 04922ca0a3ab008cc5c999aa8d2e2952f2e7134f Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 17:50:42 -0700 Subject: [PATCH 23/31] some fixes --- .../src/server/budget/categoryTemplate.ts | 74 +++++++++---------- .../src/server/budget/goaltemplates.ts | 13 ++-- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 7cf51fb6bbc..502b4c08112 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -9,7 +9,7 @@ import { goalsSchedule } from './goalsSchedule'; import { getActiveSchedules } from './statements'; //import { Template } from './types/templates'; -export class categoryTemplate { +export class CategoryTemplate { /*---------------------------------------------------------------------------- * Using This Class: * 1. instantiate via `await categoryTemplate.init(templates, categoryID, month)`; @@ -37,16 +37,16 @@ export class categoryTemplate { `leftover-${categoryID}`, ); // run all checks - await categoryTemplate.checkByAndScheduleAndSpend(templates, month); - await categoryTemplate.checkPercentage(templates); + await CategoryTemplate.checkByAndScheduleAndSpend(templates, month); + await CategoryTemplate.checkPercentage(templates); // call the private constructor - return new categoryTemplate(templates, categoryID, month, fromLastMonth); + return new CategoryTemplate(templates, categoryID, month, fromLastMonth); } - getPriorities() { + getPriorities(): number[] { return this.priorities; } - getRemainderWeight() { + getRemainderWeight(): number { return this.remainderWeight; } @@ -67,7 +67,7 @@ export class categoryTemplate { priority: number, budgetAvail: number, availStart: number, - ) { + ): Promise { if (!this.priorities.includes(priority)) return 0; const t = this.templates.filter(t => t.priority === priority); @@ -183,7 +183,7 @@ export class categoryTemplate { return toBudget; } - getValues() { + getValues(): { budgeted; goal; longGoal } { this.runGoal(); return { budgeted: this.toBudgetAmount, @@ -193,7 +193,7 @@ export class categoryTemplate { } //----------------------------------------------------------------------------- - // Implimentation + // Implementation readonly categoryID: string; //readonly so we can double check the category this is using private month: string; private templates = []; @@ -283,14 +283,11 @@ export class categoryTemplate { } }); //find lowest priority - let lowestPriority = null; - templates - .filter(t => t.type === 'schedule' || t.type === 'by') - .forEach(t => { - if (lowestPriority === null || t.priority < lowestPriority) { - lowestPriority = t.priority; - } - }); + const lowestPriority = Math.min( + ...templates + .filter(t => t.type === 'schedule' || t.type === 'by') + .map(t => t.priority), + ); //warn if priority needs fixed templates .filter(t => t.type === 'schedule' || t.type === 'by') @@ -320,16 +317,13 @@ export class categoryTemplate { static async checkPercentage(templates) { const pt = templates.filter(t => t.type === 'percentage'); - const reqCategories = []; - pt.forEach(t => reqCategories.push(t.category.toLowerCase())); + if (pt.length === 0) return; + const reqCategories = pt.map(t => t.category.toLowerCase()); const availCategories = await db.getCategories(); - const availNames = []; - availCategories.forEach(c => { - if (c.is_income) { - availNames.push(c.name.toLowerCase()); - } - }); + const availNames = availCategories + .filter(c => c.is_income) + .map(c => c.name.toLocaleLowerCase()); reqCategories.forEach(n => { if (n === 'available funds' || n === 'all income') { @@ -343,9 +337,9 @@ export class categoryTemplate { } private checkLimit() { - for (let i = 0; i < this.templates.length; i++) { - const t = this.templates[i]; - if (this.limitCheck && t.limit) { + for (const t of this.templates) { + if (!t.limit) continue; + if (this.limitCheck) { throw new Error('Only one `up to` allowed per category'); } else if (t.limit) { if (t.limit.period === 'daily') { @@ -392,24 +386,22 @@ export class categoryTemplate { //----------------------------------------------------------------------------- // Processor Functions - private runSimple(template, limit) { - let toBudget = 0; + private runSimple(template, limit): number { if (template.monthly != null) { - toBudget = amountToInteger(template.monthly); + return amountToInteger(template.monthly); } else { - toBudget = limit || 0; + return limit; } - return toBudget; } - private async runCopy(template) { + private async runCopy(template): Promise { const sheetName = monthUtils.sheetForMonth( monthUtils.subMonths(this.month, template.lookBack), ); return await getSheetValue(sheetName, `budget-${this.categoryID}`); } - private runWeek(template) { + private runWeek(template): number { let toBudget = 0; const amount = amountToInteger(template.amount); const weeks = template.weeks != null ? Math.round(template.weeks) : 1; @@ -425,7 +417,7 @@ export class categoryTemplate { return toBudget; } - private async runSpend(template) { + private async runSpend(template): Promise { const fromMonth = `${template.from}`; const toMonth = `${template.month}`; let alreadyBudgeted = this.fromLastMonth; @@ -469,7 +461,7 @@ export class categoryTemplate { } } - private async runPercentage(template, availableFunds) { + private async runPercentage(template, availableFunds): Promise { const percent = template.percent; const cat = template.category.toLowerCase(); const prev = template.previous; @@ -499,7 +491,7 @@ export class categoryTemplate { return Math.max(0, Math.round(monthlyIncome * (percent / 100))); } - private async runAverage(template) { + private async runAverage(template): Promise { let sum = 0; for (let i = 1; i <= template.numMonths; i++) { const sheetName = monthUtils.sheetForMonth( @@ -510,7 +502,11 @@ export class categoryTemplate { return -Math.round(sum / template.amount); } - private runBy(template, first: boolean, remainder: number) { + private runBy( + template, + first: boolean, + remainder: number, + ): { ret: number; remainder: number } { let target = 0; let targetMonth = `${template.month}`; let numMonths = monthUtils.differenceInCalendarMonths( diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 1ef3d7a0185..91825e1ee1e 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -5,7 +5,7 @@ import * as db from '../db'; import { batchMessages } from '../sync'; import { isReflectBudget, getSheetValue, setGoal, setBudget } from './actions'; -import { categoryTemplate } from './categoryTemplate'; +import { CategoryTemplate } from './categoryTemplate'; import { checkTemplates, storeTemplates } from './template-notes'; export async function applyTemplate({ month }): Promise { @@ -123,7 +123,7 @@ async function processTemplate( } else { categories = categoriesIn; } - const catObjects: categoryTemplate[] = []; + const catObjects: CategoryTemplate[] = []; let availBudget = await getSheetValue( monthUtils.sheetForMonth(month), `to-budget`, @@ -144,7 +144,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, id, month); availBudget += budgeted; const p = obj.getPriorities(); p.forEach(pr => priorities.push(pr)); @@ -186,16 +186,15 @@ async function processTemplate( // run each priority level for (let pi = 0; pi < priorities.length; pi++) { const availStart = availBudget; + const p = priorities[pi]; for (let i = 0; i < catObjects.length; i++) { + if (availBudget <= 0 && p > 0) break; const ret = await catObjects[i].runTemplatesForPriority( - priorities[pi], + p, availBudget, availStart, ); availBudget -= ret; - if (availBudget <= 0) { - break; - } } if (availBudget <= 0) { break; From f871248cfc6db5400898f7da77620d76c22416f2 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 17:54:27 -0700 Subject: [PATCH 24/31] more typing --- packages/loot-core/src/server/budget/categoryTemplate.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 502b4c08112..590171f673c 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -7,7 +7,7 @@ import * as db from '../db'; import { getSheetValue } from './actions'; import { goalsSchedule } from './goalsSchedule'; import { getActiveSchedules } from './statements'; -//import { Template } from './types/templates'; +import { Template } from './types/templates'; export class CategoryTemplate { /*---------------------------------------------------------------------------- @@ -30,7 +30,7 @@ export class CategoryTemplate { // Class interface // set up the class and check all templates - static async init(templates, categoryID: string, month) { + static async init(templates: Template[], categoryID: string, month) { // get all the needed setup values const fromLastMonth = await getSheetValue( monthUtils.sheetForMonth(monthUtils.subMonths(month, 1)), @@ -210,7 +210,7 @@ export class CategoryTemplate { private limitHold = false; private constructor( - templates, + templates: Template[], categoryID: string, month: string, fromLastMonth: number, From 1700cc868e5ff4aba5c69f0c968dce21e03a5f72 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 17:57:06 -0700 Subject: [PATCH 25/31] fix save error --- packages/loot-core/src/server/budget/goaltemplates.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 91825e1ee1e..c4999e73bbb 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -98,7 +98,7 @@ async function setGoals(month, idealTemplate) { category: element.category, goal: element.goal, month, - long_goal: element.long_goal, + long_goal: element.longGoal, }); }); }); From b0ec0d68981ab60e0d1b8437a7adab64ccff7cd4 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sat, 2 Nov 2024 18:28:19 -0700 Subject: [PATCH 26/31] last bunny fixes --- packages/loot-core/src/server/budget/categoryTemplate.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 590171f673c..64f436d1ec1 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -108,9 +108,7 @@ export class CategoryTemplate { break; } case 'schedule': { - //TODO add this...... - //TODO remember to trim the schedule name - const budgeted = getSheetValue( + const budgeted = await getSheetValue( monthUtils.sheetForMonth(this.month), `leftover-${this.categoryID}`, ); From 018a6556870cb45bacc36475064b8190834c910e Mon Sep 17 00:00:00 2001 From: youngcw Date: Sun, 3 Nov 2024 01:51:51 -0700 Subject: [PATCH 27/31] fix save, trim schedule names --- packages/loot-core/src/server/budget/categoryTemplate.ts | 2 +- packages/loot-core/src/server/budget/goalsSchedule.ts | 4 ++-- packages/loot-core/src/server/budget/goaltemplates.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 64f436d1ec1..8269cfcd41b 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -272,7 +272,7 @@ export class CategoryTemplate { // Template Validation static async checkByAndScheduleAndSpend(templates, month) { //check schedule names - const scheduleNames = (await getActiveSchedules()).map(({ name }) => name); + const scheduleNames = (await getActiveSchedules()).map(({ name }) => name.trim()); templates .filter(t => t.type === 'schedule') .forEach(t => { diff --git a/packages/loot-core/src/server/budget/goalsSchedule.ts b/packages/loot-core/src/server/budget/goalsSchedule.ts index 431a7ffe0e2..87a84485c7e 100644 --- a/packages/loot-core/src/server/budget/goalsSchedule.ts +++ b/packages/loot-core/src/server/budget/goalsSchedule.ts @@ -16,8 +16,8 @@ async function createScheduleList(template, current_month, category) { 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], + 'SELECT * FROM schedules WHERE TRIM(name) = ? AND tombstone = 0', + [template[ll].name.trim()], ); const rule = await getRuleForSchedule(sid); const conditions = rule.serialize().conditions; diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index c4999e73bbb..a36be6dba90 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -95,9 +95,9 @@ async function setGoals(month, idealTemplate) { await batchMessages(async () => { idealTemplate.forEach(element => { setGoal({ + month, category: element.category, goal: element.goal, - month, long_goal: element.longGoal, }); }); @@ -220,7 +220,7 @@ async function processTemplate( goalList.push({ category: o.categoryID, goal: ret.goal, - longGoal: ret.longGoal, + longGoal: ret.longGoal ? 1 : null, }); }); await setBudgets(month, budgetList); From e8fb817e457fb112b166bd39e63f25ebd895d908 Mon Sep 17 00:00:00 2001 From: youngcw Date: Sun, 3 Nov 2024 01:54:45 -0700 Subject: [PATCH 28/31] lint --- packages/loot-core/src/server/budget/categoryTemplate.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 8269cfcd41b..2c6225b59b4 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -272,7 +272,9 @@ export class CategoryTemplate { // Template Validation static async checkByAndScheduleAndSpend(templates, month) { //check schedule names - const scheduleNames = (await getActiveSchedules()).map(({ name }) => name.trim()); + const scheduleNames = (await getActiveSchedules()).map(({ name }) => + name.trim(), + ); templates .filter(t => t.type === 'schedule') .forEach(t => { From c55a446130f5adc8a19908bced7867b558a41d3b Mon Sep 17 00:00:00 2001 From: youngcw Date: Mon, 4 Nov 2024 10:15:10 -0700 Subject: [PATCH 29/31] minor fixes --- .../src/server/budget/categoryTemplate.ts | 2 +- .../src/server/budget/goaltemplates.ts | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/loot-core/src/server/budget/categoryTemplate.ts b/packages/loot-core/src/server/budget/categoryTemplate.ts index 2c6225b59b4..9a70e655784 100644 --- a/packages/loot-core/src/server/budget/categoryTemplate.ts +++ b/packages/loot-core/src/server/budget/categoryTemplate.ts @@ -499,7 +499,7 @@ export class CategoryTemplate { ); sum += await getSheetValue(sheetName, `sum-amount-${this.categoryID}`); } - return -Math.round(sum / template.amount); + return -Math.round(sum / template.numMonths); } private runBy( diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index a36be6dba90..d0ffaea96f0 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -10,15 +10,15 @@ import { checkTemplates, storeTemplates } from './template-notes'; export async function applyTemplate({ month }): Promise { await storeTemplates(); - const category_templates = await getTemplates(null); - const ret = await processTemplate(month, false, category_templates, null); + const categoryTemplates = await getTemplates(null); + const ret = await processTemplate(month, false, categoryTemplates, null); return ret; } export async function overwriteTemplate({ month }): Promise { await storeTemplates(); - const category_templates = await getTemplates(null); - const ret = await processTemplate(month, true, category_templates, null); + const categoryTemplates = await getTemplates(null); + const ret = await processTemplate(month, true, categoryTemplates, null); return ret; } @@ -27,11 +27,11 @@ export async function applySingleCategoryTemplate({ month, category }) { category, ]); await storeTemplates(); - const category_templates = await getTemplates(categories[0]); + const categoryTemplates = await getTemplates(categories[0]); const ret = await processTemplate( month, true, - category_templates, + categoryTemplates, categories, ); return ret; @@ -54,13 +54,13 @@ async function getCategories() { async function getTemplates(category) { //retrieves template definitions from the database - const goal_def = await db.all( + const goalDef = await db.all( 'SELECT * FROM categories WHERE goal_def IS NOT NULL', ); const templates = []; - for (let ll = 0; ll < goal_def.length; ll++) { - templates[goal_def[ll].id] = JSON.parse(goal_def[ll].goal_def); + for (let ll = 0; ll < goalDef.length; ll++) { + templates[goalDef[ll].id] = JSON.parse(goalDef[ll].goalDef); } if (category) { const ret = []; @@ -114,8 +114,8 @@ async function processTemplate( let categories = []; if (!categoriesIn) { const isReflect = isReflectBudget(); - const categories_long = await getCategories(); - categories_long.forEach(c => { + const categoriesLong = await getCategories(); + categoriesLong.forEach(c => { if (!isReflect && !c.is_income) { categories.push(c); } From e149f670bca543637ed3fe58b16f7d861faf0699 Mon Sep 17 00:00:00 2001 From: youngcw Date: Mon, 4 Nov 2024 11:07:20 -0700 Subject: [PATCH 30/31] last fixes --- .../src/server/budget/goaltemplates.ts | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index a1cf390dec0..484602dfa9c 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -27,15 +27,13 @@ export async function applyMultipleCategoryTemplates({ month, categoryIds }) { const query = `SELECT * FROM v_categories WHERE id IN (${placeholders})`; const categories = await db.all(query, categoryIds); await storeTemplates(); - const category_templates = await getTemplates(categories, 'template'); - const category_goals = await getTemplates(categories, 'goal'); + const categoryTemplates = await getTemplates(categories); const ret = await processTemplate( month, true, - category_templates, + categoryTemplates, categories, ); - await processGoals(category_goals, month); return ret; } @@ -45,12 +43,7 @@ export async function applySingleCategoryTemplate({ month, category }) { ]); await storeTemplates(); const categoryTemplates = await getTemplates(categories[0]); - const ret = await processTemplate( - month, - true, - categoryTemplates, - categories, - ); + const ret = await processTemplate(month, true, categoryTemplates, categories); return ret; } @@ -77,7 +70,7 @@ async function getTemplates(category) { const templates = []; for (let ll = 0; ll < goalDef.length; ll++) { - templates[goalDef[ll].id] = JSON.parse(goalDef[ll].goalDef); + templates[goalDef[ll].id] = JSON.parse(goalDef[ll].goal_def); } if (Array.isArray(category)) { const multipleCategoryTemplates = []; @@ -85,9 +78,6 @@ async function getTemplates(category) { const categoryId = category[dd].id; if (templates[categoryId] !== undefined) { multipleCategoryTemplates[categoryId] = templates[categoryId]; - multipleCategoryTemplates[categoryId] = multipleCategoryTemplates[ - categoryId - ].filter(t => t.directive === directive); } } return multipleCategoryTemplates; @@ -159,7 +149,7 @@ async function processTemplate( monthUtils.sheetForMonth(month), `to-budget`, ); - const priorities = []; + let priorities = []; let remainderWeight = 0; const errors = []; const budgetList = []; From 875adbaa120406a24e692ea09a76dd522e875a43 Mon Sep 17 00:00:00 2001 From: youngcw Date: Mon, 4 Nov 2024 11:10:42 -0700 Subject: [PATCH 31/31] lint --- packages/loot-core/src/server/budget/goaltemplates.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/loot-core/src/server/budget/goaltemplates.ts b/packages/loot-core/src/server/budget/goaltemplates.ts index 484602dfa9c..a91516c2157 100644 --- a/packages/loot-core/src/server/budget/goaltemplates.ts +++ b/packages/loot-core/src/server/budget/goaltemplates.ts @@ -28,12 +28,7 @@ export async function applyMultipleCategoryTemplates({ month, categoryIds }) { const categories = await db.all(query, categoryIds); await storeTemplates(); const categoryTemplates = await getTemplates(categories); - const ret = await processTemplate( - month, - true, - categoryTemplates, - categories, - ); + const ret = await processTemplate(month, true, categoryTemplates, categories); return ret; }