mirror of
https://github.com/actualbudget/actual.git
synced 2026-05-06 15:12:35 -05:00
* Fix leftover balance usage in budget covering logic (#7131) * [autofix.ci] apply automated fixes * Add regression unit tests for `coverOverbudgeted` fixes * Update release note for a more user-facing sentence --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
99
packages/loot-core/src/server/budget/actions.test.ts
Normal file
99
packages/loot-core/src/server/budget/actions.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import * as db from '../db';
|
||||
import * as sheet from '../sheet';
|
||||
|
||||
import {
|
||||
coverOverbudgeted,
|
||||
getSheetValue,
|
||||
setBudget,
|
||||
setCategoryCarryover,
|
||||
} from './actions';
|
||||
import * as budget from './base';
|
||||
|
||||
describe('coverOverbudgeted', () => {
|
||||
beforeEach(global.emptyDatabase());
|
||||
afterEach(global.emptyDatabase());
|
||||
|
||||
it('fully covers the overbudget when category has sufficient leftover', async () => {
|
||||
// Setup: cat1 has 100 leftover, overbudget is 90
|
||||
await prepareDatabase();
|
||||
|
||||
await coverOverbudgeted({
|
||||
month: '2024-02',
|
||||
category: 'cat1',
|
||||
currencyCode: 'USD',
|
||||
});
|
||||
await sheet.waitOnSpreadsheet();
|
||||
|
||||
const sheetName = 'budget202402';
|
||||
expect(await getSheetValue(sheetName, 'to-budget')).toBe(0);
|
||||
expect(await getSheetValue(sheetName, 'leftover-cat1')).toBe(10);
|
||||
});
|
||||
|
||||
it('partially covers the overbudget when category leftover is insufficient', async () => {
|
||||
// Setup: cat3 has 10 leftover, overbudget is 90
|
||||
await prepareDatabase();
|
||||
|
||||
await coverOverbudgeted({
|
||||
month: '2024-02',
|
||||
category: 'cat3',
|
||||
currencyCode: 'USD',
|
||||
});
|
||||
await sheet.waitOnSpreadsheet();
|
||||
|
||||
const sheetName = 'budget202402';
|
||||
expect(await getSheetValue(sheetName, 'to-budget')).toBe(-80);
|
||||
expect(await getSheetValue(sheetName, 'leftover-cat3')).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Setup: 2024-02, is 90 overbudgeted
|
||||
// with balances of cat1 = 100, cat2 = -20, cat3 = 10
|
||||
async function prepareDatabase() {
|
||||
await db.insertCategoryGroup({
|
||||
id: 'income-group',
|
||||
name: 'Income',
|
||||
is_income: 1,
|
||||
});
|
||||
await db.insertCategory({
|
||||
id: 'income-cat',
|
||||
name: 'Income',
|
||||
cat_group: 'income-group',
|
||||
is_income: 1,
|
||||
});
|
||||
|
||||
await db.insertCategoryGroup({ id: 'group1', name: 'group1', is_income: 0 });
|
||||
await db.insertCategory({
|
||||
id: 'cat1',
|
||||
name: 'cat1',
|
||||
cat_group: 'group1',
|
||||
is_income: 0,
|
||||
});
|
||||
await db.insertCategory({
|
||||
id: 'cat2',
|
||||
name: 'cat2',
|
||||
cat_group: 'group1',
|
||||
is_income: 0,
|
||||
});
|
||||
await db.insertCategory({
|
||||
id: 'cat3',
|
||||
name: 'cat3',
|
||||
cat_group: 'group1',
|
||||
is_income: 0,
|
||||
});
|
||||
|
||||
await setBudget({ category: 'cat1', month: '2024-01', amount: 100 });
|
||||
await setBudget({ category: 'cat2', month: '2024-01', amount: -20 });
|
||||
await setBudget({ category: 'cat3', month: '2024-01', amount: 10 });
|
||||
|
||||
await sheet.loadSpreadsheet(db);
|
||||
await budget.createBudget(['2024-01', '2024-02']);
|
||||
|
||||
await setCategoryCarryover({
|
||||
startMonth: '2024-01',
|
||||
category: 'cat2',
|
||||
flag: true,
|
||||
});
|
||||
await sheet.waitOnSpreadsheet();
|
||||
}
|
||||
@@ -520,6 +520,10 @@ export async function coverOverbudgeted({
|
||||
}): Promise<void> {
|
||||
const sheetName = monthUtils.sheetForMonth(month);
|
||||
const categoryBudget = await getSheetValue(sheetName, 'budget-' + category);
|
||||
const categoryLeftover = await getSheetValue(
|
||||
sheetName,
|
||||
'leftover-' + category,
|
||||
);
|
||||
|
||||
// Cover provided amount (can be partial) or full overbudgeted amount.
|
||||
const amountToCover = amount
|
||||
@@ -527,12 +531,12 @@ export async function coverOverbudgeted({
|
||||
-amount
|
||||
: await getSheetValue(sheetName, 'to-budget');
|
||||
|
||||
if (amountToCover >= 0 || categoryBudget <= 0) {
|
||||
if (amountToCover >= 0 || categoryLeftover <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't allow the budget of the covering category to go negative.
|
||||
const coverableAmount = Math.min(Math.abs(amountToCover), categoryBudget);
|
||||
// Don't exceed the available balance of the covering category.
|
||||
const coverableAmount = Math.min(Math.abs(amountToCover), categoryLeftover);
|
||||
|
||||
await batchMessages(async () => {
|
||||
await setBudget({
|
||||
|
||||
6
upcoming-release-notes/7272.md
Normal file
6
upcoming-release-notes/7272.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Bugfixes
|
||||
authors: [JSkinnerUK]
|
||||
---
|
||||
|
||||
Fixed an issue that prevented category's leftover from being able to cover an overbudgeted amount.
|
||||
Reference in New Issue
Block a user