Skip to content

Commit 2250793

Browse files
committed
coderabbit again.
1 parent d8c3cad commit 2250793

11 files changed

Lines changed: 91 additions & 79 deletions

File tree

packages/desktop-client/src/components/budget/goals/TemplateSentence.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Trans } from 'react-i18next';
22

33
import type { Template } from '@actual-app/core/types/models/templates';
4+
import type { TransObjectLiteral } from '@actual-app/core/types/util';
45

56
import { BySaveAutomationReadOnly } from './editor/BySaveAutomationReadOnly';
67
import { FixedAutomationReadOnly } from './editor/FixedAutomationReadOnly';
@@ -46,8 +47,14 @@ export function TemplateSentence({
4647
case 'simple':
4748
case 'spend':
4849
case 'goal':
49-
case 'error':
50-
return <Trans>Unsupported template type</Trans>;
50+
case 'error': {
51+
const type = template.type;
52+
return (
53+
<Trans>
54+
Unsupported template type: {{ type } satisfies TransObjectLiteral}
55+
</Trans>
56+
);
57+
}
5158
default:
5259
template satisfies never;
5360
return null;

packages/desktop-client/src/components/budget/goals/editor/FixedAutomationReadOnly.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ type FixedAutomationReadOnlyProps = {
1111
template: PeriodicTemplate;
1212
};
1313

14-
export const FixedAutomationReadOnly = ({
14+
export function FixedAutomationReadOnly({
1515
template,
16-
}: FixedAutomationReadOnlyProps) => {
16+
}: FixedAutomationReadOnlyProps) {
1717
const format = useFormat();
1818
const amount = format(
1919
amountToInteger(template.amount, format.currency.decimalPlaces),
@@ -97,4 +97,4 @@ export const FixedAutomationReadOnly = ({
9797
default:
9898
return null;
9999
}
100-
};
100+
}

packages/desktop-client/src/components/modals/BudgetAutomationsModal/BudgetAutomationMigrationWarning.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,7 @@ export function BudgetAutomationMigrationWarning({
1919
if (!notes) return null;
2020
const templates = notes
2121
.split('\n')
22-
.flatMap(line => {
23-
if (line.trim().startsWith('#template')) return line;
24-
if (line.trim().startsWith('#goal')) return line;
25-
if (line.trim().startsWith('#cleanup')) return line;
26-
return [];
27-
})
22+
.filter(line => /^\s*#(template|goal|cleanup)\b/.test(line))
2823
.join('\n');
2924

3025
if (!templates) return null;

packages/desktop-client/src/components/modals/BudgetAutomationsModal/BudgetAutomationsBody.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export function BudgetAutomationsBody({
165165
]);
166166
for (const group of categories) {
167167
for (const cat of group.categories ?? []) {
168+
if (!cat.is_income) continue;
168169
validPercentageSources.add(cat.id);
169170
if (cat.name) validPercentageSources.add(cat.name.toLowerCase());
170171
}

packages/desktop-client/src/components/modals/BudgetAutomationsModal/BudgetAutomationsModal.test.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,40 @@ import type { Template } from '@actual-app/core/types/models/templates';
33
import { migrateTemplatesToAutomations } from './migrateTemplatesToAutomations';
44

55
describe('migrateTemplatesToAutomations', () => {
6-
it('preserves simple templates that have no limit and no monthly amount', () => {
6+
it('drops simple templates that have no limit and no monthly amount', () => {
7+
// these would otherwise be pushed as a phantom 'fixed' entry that
8+
// crashes FixedAutomationReadOnly (no .amount, no .period)
79
const simpleTemplate = {
810
type: 'simple',
911
directive: 'template',
1012
priority: 5,
1113
} satisfies Template;
1214

13-
const result = migrateTemplatesToAutomations([simpleTemplate]);
15+
expect(migrateTemplatesToAutomations([simpleTemplate])).toEqual([]);
16+
});
1417

15-
expect(result).toHaveLength(1);
16-
expect(result[0].displayType).toBe('fixed');
17-
expect(result[0].template).toEqual(simpleTemplate);
18-
expect(result[0].id).toMatch(/^automation-/);
18+
it('drops simple templates whose monthly amount is zero with no limit', () => {
19+
const simpleTemplate = {
20+
type: 'simple',
21+
directive: 'template',
22+
priority: 5,
23+
monthly: 0,
24+
} satisfies Template;
25+
26+
expect(migrateTemplatesToAutomations([simpleTemplate])).toEqual([]);
27+
});
28+
29+
it('throws when a goal directive reaches migration', () => {
30+
const goalTemplate = {
31+
type: 'goal',
32+
amount: 1000,
33+
directive: 'goal',
34+
priority: null,
35+
} satisfies Template;
36+
37+
expect(() => migrateTemplatesToAutomations([goalTemplate])).toThrow(
38+
/Unsupported template type/,
39+
);
1940
});
2041

2142
it('expands a simple template with limit into limit and refill entries', () => {

packages/desktop-client/src/components/modals/BudgetAutomationsModal/TypePicker.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useTranslation } from 'react-i18next';
2+
13
import { Text } from '@actual-app/components/text';
24
import { theme } from '@actual-app/components/theme';
35
import { View } from '@actual-app/components/view';
@@ -13,9 +15,11 @@ type TypePickerProps = {
1315
};
1416

1517
export function TypePicker({ active, disabledTypes, onPick }: TypePickerProps) {
18+
const { t } = useTranslation();
1619
const entries = displayTemplateTypes.map(
1720
id => [id, getDisplayTemplateMeta(id)] as const,
1821
);
22+
const disabledHint = t('Only one of this type allowed per category');
1923

2024
return (
2125
<View
@@ -36,6 +40,7 @@ export function TypePicker({ active, disabledTypes, onPick }: TypePickerProps) {
3640
tabIndex={isDisabled ? -1 : 0}
3741
aria-pressed={isActive}
3842
aria-disabled={isDisabled}
43+
title={isDisabled ? disabledHint : undefined}
3944
onClick={() => {
4045
if (!isDisabled) onPick(id);
4146
}}

packages/desktop-client/src/components/modals/BudgetAutomationsModal/UnsupportedDirectivesNotice.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,9 @@ export function UnsupportedDirectivesNotice({
9292
);
9393
}
9494

95+
const CLEANUP_DIRECTIVE = /^\s*#cleanup\b/;
96+
9597
export function hasCleanupLine(notes: string | null | undefined): boolean {
9698
if (!notes) return false;
97-
return notes
98-
.split('\n')
99-
.some(line => line.trimStart().startsWith('#cleanup'));
99+
return notes.split('\n').some(line => CLEANUP_DIRECTIVE.test(line));
100100
}

packages/desktop-client/src/components/modals/BudgetAutomationsModal/migrateTemplatesToAutomations.ts

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,16 @@ function getDisplayTypeFromTemplate(template: Template): DisplayTemplateType {
2525
return 'by';
2626
case 'remainder':
2727
return 'remainder';
28-
default:
29-
return 'fixed';
28+
case 'goal':
29+
case 'error':
30+
case 'spend':
31+
// filtered upstream by hasUnsupportedDirective; surface if it ever isn't
32+
throw new Error(`Unsupported template type reached migration`);
33+
default: {
34+
const _exhaustive: never = template;
35+
void _exhaustive;
36+
throw new Error(`Unhandled template type`);
37+
}
3038
}
3139
}
3240

@@ -37,11 +45,10 @@ export function migrateTemplatesToAutomations(
3745

3846
templates.forEach(template => {
3947
if (template.type === 'simple') {
40-
let hasExpandedTemplate = false;
41-
const hasMonthly = template.monthly != null && template.monthly !== 0;
48+
const monthly = template.monthly;
49+
const hasMonthly = monthly != null && monthly !== 0;
4250

4351
if (template.limit) {
44-
hasExpandedTemplate = true;
4552
entries.push(
4653
createAutomationEntry(
4754
{
@@ -73,13 +80,12 @@ export function migrateTemplatesToAutomations(
7380
);
7481
}
7582
}
76-
if (template.monthly != null && template.monthly !== 0) {
77-
hasExpandedTemplate = true;
83+
if (hasMonthly) {
7884
entries.push(
7985
createAutomationEntry(
8086
{
8187
type: 'periodic',
82-
amount: template.monthly,
88+
amount: monthly,
8389
period: { period: 'month', amount: 1 },
8490
starting: dayFromDate(firstDayOfMonth(new Date())),
8591
directive: 'template',
@@ -90,11 +96,9 @@ export function migrateTemplatesToAutomations(
9096
);
9197
}
9298

93-
if (!hasExpandedTemplate) {
94-
entries.push(
95-
createAutomationEntry(template, getDisplayTypeFromTemplate(template)),
96-
);
97-
}
99+
// a simple template with neither monthly nor limit is a no-op; drop it
100+
// rather than passing through as a phantom 'fixed' entry that would
101+
// crash FixedAutomationReadOnly (no .amount, no .period)
98102
return;
99103
}
100104

packages/loot-core/src/server/budget/category-template-context.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,21 @@ export class CategoryTemplateContext {
301301
// projections reflect any clamping that occurred above. The limit branch
302302
// can produce a negative scale when the carried-over balance already
303303
// exceeds the cap; floor at 0 here so per-row UI projections never go
304-
// negative (engine totals are unaffected).
304+
// negative (engine totals are unaffected). Give the last entry the
305+
// residual so the per-row sum equals toBudget exactly even when scale
306+
// produces fractional cents.
305307
const perRowScale = Math.max(0, scale);
306-
for (const [template, value] of perTemplateLocal) {
307-
const scaled = Math.round(value * perRowScale);
308+
const items = Array.from(perTemplateLocal);
309+
let remaining = Math.max(0, toBudget);
310+
items.forEach(([template, value], i) => {
311+
const isLast = i === items.length - 1;
312+
const share = isLast
313+
? remaining
314+
: Math.max(0, Math.min(remaining, Math.round(value * perRowScale)));
308315
const existing = this.perTemplateContribution.get(template) ?? 0;
309-
this.perTemplateContribution.set(template, existing + scaled);
310-
}
316+
this.perTemplateContribution.set(template, existing + share);
317+
remaining -= share;
318+
});
311319
return this.category.is_income ? -toBudget : toBudget;
312320
}
313321

packages/loot-core/src/server/budget/goal-template.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ function setupAqlForCategoryLookup(cat: CategoryEntity) {
6767
if (queryStr.includes('defaultCurrencyCode')) {
6868
return { data: [{ value: 'USD' }], dependencies: [] };
6969
}
70-
if (queryStr.includes('categories')) {
71-
return { data: [cat], dependencies: [] };
70+
if (queryStr.includes('category_groups')) {
71+
return {
72+
data: [{ id: cat.group, hidden: false, categories: [cat] }],
73+
dependencies: [],
74+
};
7275
}
7376
return { data: [], dependencies: [] };
7477
});

0 commit comments

Comments
 (0)