mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
* add percentage adjustments to schedule templates * update release note * remove unecessary parentheses * Update packages/loot-core/src/server/budget/goalsSchedule.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * PR comments addressed * Linting fixes * Updated error handling, added tests * Linting fixes --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: youngcw <calebyoung94@gmail.com>
This commit is contained in:
@@ -18,8 +18,8 @@ expr
|
||||
{ return { type: 'simple', monthly, limit, priority: template.priority, directive: template.directive }}
|
||||
/ template: template _ limit: limit
|
||||
{ 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 }}
|
||||
/ template: template _ schedule:schedule _ full:full? name:rawScheduleName modifiers:modifiers?
|
||||
{ return { type: 'schedule', name: name.trim(), priority: template.priority, directive: template.directive, full, adjustment: modifiers?.adjustment }}
|
||||
/ 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?
|
||||
@@ -28,6 +28,13 @@ expr
|
||||
{ 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 }}
|
||||
|
||||
modifiers = _ '[' modifier:modifier ']' { return modifier }
|
||||
|
||||
modifier
|
||||
= op:('increase'i / 'decrease'i) _ value:percent {
|
||||
const multiplier = op.toLowerCase() === 'increase' ? 1 : -1;
|
||||
return { adjustment: multiplier * +value }
|
||||
}
|
||||
|
||||
repeat 'repeat interval'
|
||||
= 'month'i { return { annual: false }}
|
||||
@@ -59,24 +66,37 @@ repeatEvery = 'repeat'i _ 'every'i
|
||||
starting = 'starting'i
|
||||
upTo = 'up'i _ 'to'i
|
||||
hold = 'hold'i {return true}
|
||||
schedule = 'schedule'i
|
||||
schedule = 'schedule'i { return text() }
|
||||
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'i { return 'goal'}
|
||||
|
||||
_ 'space' = ' '+
|
||||
_ "whitespace" = [ \t]* { return text() }
|
||||
__ "mandatory whitespace" = [ \t]+ { return text() }
|
||||
|
||||
d 'digit' = [0-9]
|
||||
number 'number' = $(d+)
|
||||
positive = $([1-9][0-9]*)
|
||||
amount 'amount' = currencySymbol? _? amount: $('-'?d+ ('.' (d d?)?)?) { return +amount }
|
||||
percent 'percentage' = percent: $(d+ ('.' (d+)?)?) _? '%' { return +percent }
|
||||
percent 'percentage' = percent: $(d+ ('.' (d+)?)?) _? '%' { return percent }
|
||||
year 'year' = $(d d d d)
|
||||
month 'month' = $(year '-' d d)
|
||||
day 'day' = $(d d)
|
||||
date = $(month '-' day)
|
||||
currencySymbol 'currency symbol' = symbol: . & { return /\p{Sc}/u.test(symbol) }
|
||||
|
||||
name 'Name' = $([^\r\n\t]+)
|
||||
// Match schedule name including spaces up until we see a [, looking ahead to make sure it's followed by increase/decrease
|
||||
rawScheduleName = $(
|
||||
(
|
||||
[^ \t\r\n\[] // First character can't be space or [
|
||||
(
|
||||
[^\r\n\[] // Subsequent characters can include spaces but not [
|
||||
/
|
||||
(![^\r\n\[]* '['('increase'i/'decrease'i)) [ ] // Or spaces if not followed by [increase/decrease
|
||||
)*
|
||||
)
|
||||
) { return text() }
|
||||
|
||||
name 'Name' = $([^\r\n\t]+) { return text() }
|
||||
@@ -29,11 +29,16 @@ async function createScheduleList(
|
||||
const conditions = rule.serialize().conditions;
|
||||
const { date: dateConditions, amount: amountCondition } =
|
||||
extractScheduleConds(conditions);
|
||||
const scheduleAmount =
|
||||
let scheduleAmount =
|
||||
amountCondition.op === 'isbetween'
|
||||
? Math.round(amountCondition.value.num1 + amountCondition.value.num2) /
|
||||
2
|
||||
: amountCondition.value;
|
||||
// Apply adjustment percentage if specified
|
||||
if (template[ll].adjustment) {
|
||||
const adjustmentFactor = 1 + template[ll].adjustment / 100;
|
||||
scheduleAmount = Math.round(scheduleAmount * adjustmentFactor);
|
||||
}
|
||||
const { amount: postRuleAmount, subtransactions } = rule.execActions({
|
||||
amount: scheduleAmount,
|
||||
category: category.id,
|
||||
|
||||
@@ -227,6 +227,38 @@ describe('checkTemplates', () => {
|
||||
pre: 'Category 1: Schedule “Non-existent Schedule” does not exist',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Returns errors for invalid increase schedule adjustments',
|
||||
mockTemplateNotes: [
|
||||
{
|
||||
id: 'cat1',
|
||||
name: 'Category 1',
|
||||
note: '#template schedule Mock Schedule 1 [increase 1001%]',
|
||||
},
|
||||
],
|
||||
mockSchedules: mockSchedules(),
|
||||
expected: {
|
||||
sticky: true,
|
||||
message: 'There were errors interpreting some templates:',
|
||||
pre: 'Category 1: #template schedule Mock Schedule 1 [increase 1001%]\nError: Invalid adjustment percentage (1001%). Must be between -100% and 1000%',
|
||||
},
|
||||
},
|
||||
{
|
||||
description: 'Returns errors for invalid decrease schedule adjustments',
|
||||
mockTemplateNotes: [
|
||||
{
|
||||
id: 'cat1',
|
||||
name: 'Category 1',
|
||||
note: '#template schedule Mock Schedule 1 [decrease 101%]',
|
||||
},
|
||||
],
|
||||
mockSchedules: mockSchedules(),
|
||||
expected: {
|
||||
sticky: true,
|
||||
message: 'There were errors interpreting some templates:',
|
||||
pre: 'Category 1: #template schedule Mock Schedule 1 [decrease 101%]\nError: Invalid adjustment percentage (-101%). Must be between -100% and 1000%',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(testCases)(
|
||||
|
||||
@@ -43,7 +43,12 @@ export async function checkTemplates(): Promise<Notification> {
|
||||
categoryWithTemplates.forEach(({ name, templates }) => {
|
||||
templates.forEach(template => {
|
||||
if (template.type === 'error') {
|
||||
errors.push(`${name}: ${template.line}`);
|
||||
// Only show detailed error for adjustment-related errors
|
||||
if (template.error && template.error.includes('adjustment')) {
|
||||
errors.push(`${name}: ${template.line}\nError: ${template.error}`);
|
||||
} else {
|
||||
errors.push(`${name}: ${template.line}`);
|
||||
}
|
||||
} else if (
|
||||
template.type === 'schedule' &&
|
||||
!scheduleNames.includes(template.name)
|
||||
@@ -91,6 +96,21 @@ async function getCategoriesWithTemplates(): Promise<CategoryWithTemplates[]> {
|
||||
try {
|
||||
const parsedTemplate: Template = parse(trimmedLine);
|
||||
|
||||
// Validate schedule adjustments
|
||||
if (
|
||||
parsedTemplate.type === 'schedule' &&
|
||||
parsedTemplate.adjustment !== undefined
|
||||
) {
|
||||
if (
|
||||
parsedTemplate.adjustment <= -100 ||
|
||||
parsedTemplate.adjustment > 1000
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid adjustment percentage (${parsedTemplate.adjustment}%). Must be between -100% and 1000%`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parsedTemplates.push(parsedTemplate);
|
||||
} catch (e: unknown) {
|
||||
parsedTemplates.push({
|
||||
|
||||
@@ -45,6 +45,7 @@ interface ScheduleTemplate extends BaseTemplate {
|
||||
type: 'schedule';
|
||||
name: string;
|
||||
full?: boolean;
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
interface RemainderTemplate extends BaseTemplate {
|
||||
|
||||
6
upcoming-release-notes/4257.md
Normal file
6
upcoming-release-notes/4257.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [MattFaz]
|
||||
---
|
||||
|
||||
Add percentage adjustments to schedule templates
|
||||
Reference in New Issue
Block a user