Skip to content

Commit cb2cd7d

Browse files
committed
Fix account breakdown percentage calculation in net totals mode and enhance unit tests
- Corrected the denominator used for account breakdown percentages in net totals mode to reflect the sum of absolute values of net spending, ensuring accurate percentage calculations. - Added unit tests for account breakdown percentage calculations in both net totals and absolute modes, including scenarios with negative net spending and edge cases.
1 parent d695ed8 commit cb2cd7d

File tree

3 files changed

+133
-9
lines changed

3 files changed

+133
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2626
- Fixed category totals calculation bug where income transactions (e.g., refunds) in the same category as expenses were not reducing the category total. Now correctly shows net spending (e.g., -$100 expense + $50 income = $50 net spending)
2727
- Fixed payee totals calculation: Income transactions (e.g., refunds) now reduce payee totals when net totals mode is enabled
2828
- Fixed account breakdown totals: Income transactions now reduce account totals when net totals mode is enabled
29+
- Fixed account breakdown percentage calculation: Account breakdown percentages now use the correct denominator when net totals mode is enabled (sum of absolute values of net spending) instead of total expenses, fixing incorrect percentages that didn't sum to 100% and could be negative
2930
- Fixed day of week spending totals: Income transactions now reduce day totals when net totals mode is enabled
3031

3132
### Tests
3233

3334
- Added comprehensive unit tests for net totals calculation mode, covering categories, payees, and account breakdown in both new mode (with income) and old mode (expenses only), including scenarios with multiple income and expense transactions
35+
- Added unit tests for account breakdown percentage calculation in both net totals mode and absolute mode, including edge cases with negative net spending
3436

3537
## 2026-01-05
3638

src/utils/dataTransform.test.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,119 @@ describe('transformToWrappedData', () => {
19311931
expect(checking?.totalSpending).toBe(100); // Only $100 (absolute spending, ignores income)
19321932
expect(checking?.transactionCount).toBe(2); // Count still includes both
19331933
});
1934+
1935+
it('calculates account breakdown percentages correctly when includeIncomeInCategories is true (net totals mode)', () => {
1936+
const transactions: Transaction[] = [
1937+
createMockTransaction({ id: 't1', account: 'acc1', amount: -10000 }), // -$100 expense
1938+
createMockTransaction({ id: 't2', account: 'acc1', amount: 5000 }), // +$50 income (refund) → net = $50
1939+
createMockTransaction({ id: 't3', account: 'acc2', amount: -20000 }), // -$200 expense → net = $200
1940+
];
1941+
1942+
const accounts: Account[] = [
1943+
createMockAccount({ id: 'acc1', name: 'Checking' }),
1944+
createMockAccount({ id: 'acc2', name: 'Savings' }),
1945+
];
1946+
1947+
// New mode: includeIncomeInCategories = true (default)
1948+
const result = transformToWrappedData(transactions, [], [], accounts);
1949+
1950+
const checking = result.accountBreakdown.find(a => a.accountId === 'acc1');
1951+
const savings = result.accountBreakdown.find(a => a.accountId === 'acc2');
1952+
1953+
expect(checking).toBeDefined();
1954+
expect(checking?.totalSpending).toBe(50); // $100 - $50 = $50 (net spending)
1955+
expect(savings).toBeDefined();
1956+
expect(savings?.totalSpending).toBe(200); // $200 (net spending)
1957+
1958+
// Percentages should use sum of absolute values: $50 + $200 = $250
1959+
// Account 1: $50 / $250 = 20%
1960+
// Account 2: $200 / $250 = 80%
1961+
// Total should be approximately 100%
1962+
const totalPercentage = result.accountBreakdown.reduce((sum, acc) => sum + acc.percentage, 0);
1963+
expect(totalPercentage).toBeCloseTo(100, 1);
1964+
expect(checking?.percentage).toBeCloseTo(20, 1);
1965+
expect(savings?.percentage).toBeCloseTo(80, 1);
1966+
});
1967+
1968+
it('calculates account breakdown percentages correctly when includeIncomeInCategories is false (absolute mode)', () => {
1969+
const transactions: Transaction[] = [
1970+
createMockTransaction({ id: 't1', account: 'acc1', amount: -10000 }), // -$100 expense
1971+
createMockTransaction({ id: 't2', account: 'acc1', amount: 5000 }), // +$50 income (ignored)
1972+
createMockTransaction({ id: 't3', account: 'acc2', amount: -20000 }), // -$200 expense
1973+
];
1974+
1975+
const accounts: Account[] = [
1976+
createMockAccount({ id: 'acc1', name: 'Checking' }),
1977+
createMockAccount({ id: 'acc2', name: 'Savings' }),
1978+
];
1979+
1980+
// Old mode: includeIncomeInCategories = false
1981+
const result = transformToWrappedData(
1982+
transactions,
1983+
[],
1984+
[],
1985+
accounts,
1986+
2025,
1987+
false,
1988+
true,
1989+
false,
1990+
'$',
1991+
undefined,
1992+
new Map(),
1993+
new Map(),
1994+
false, // includeIncomeInCategories = false
1995+
);
1996+
1997+
const checking = result.accountBreakdown.find(a => a.accountId === 'acc1');
1998+
const savings = result.accountBreakdown.find(a => a.accountId === 'acc2');
1999+
2000+
expect(checking).toBeDefined();
2001+
expect(checking?.totalSpending).toBe(100); // Only $100 (absolute spending, ignores income)
2002+
expect(savings).toBeDefined();
2003+
expect(savings?.totalSpending).toBe(200); // $200 (absolute spending)
2004+
2005+
// Percentages should use totalExpenses: $100 + $200 = $300
2006+
// Account 1: $100 / $300 = 33.33%
2007+
// Account 2: $200 / $300 = 66.67%
2008+
// Total should be approximately 100%
2009+
const totalPercentage = result.accountBreakdown.reduce((sum, acc) => sum + acc.percentage, 0);
2010+
expect(totalPercentage).toBeCloseTo(100, 1);
2011+
expect(checking?.percentage).toBeCloseTo(33.33, 1);
2012+
expect(savings?.percentage).toBeCloseTo(66.67, 1);
2013+
});
2014+
2015+
it('handles negative net spending in account breakdown percentages correctly', () => {
2016+
const transactions: Transaction[] = [
2017+
createMockTransaction({ id: 't1', account: 'acc1', amount: -10000 }), // -$100 expense
2018+
createMockTransaction({ id: 't2', account: 'acc1', amount: 15000 }), // +$150 income → net = -$50 (negative)
2019+
createMockTransaction({ id: 't3', account: 'acc2', amount: -20000 }), // -$200 expense → net = $200
2020+
];
2021+
2022+
const accounts: Account[] = [
2023+
createMockAccount({ id: 'acc1', name: 'Checking' }),
2024+
createMockAccount({ id: 'acc2', name: 'Savings' }),
2025+
];
2026+
2027+
// New mode: includeIncomeInCategories = true (default)
2028+
const result = transformToWrappedData(transactions, [], [], accounts);
2029+
2030+
const checking = result.accountBreakdown.find(a => a.accountId === 'acc1');
2031+
const savings = result.accountBreakdown.find(a => a.accountId === 'acc2');
2032+
2033+
expect(checking).toBeDefined();
2034+
expect(checking?.totalSpending).toBe(-50); // $100 - $150 = -$50 (negative net spending)
2035+
expect(savings).toBeDefined();
2036+
expect(savings?.totalSpending).toBe(200); // $200 (net spending)
2037+
2038+
// Percentages should use sum of absolute values: |-$50| + $200 = $250
2039+
// Account 1: $50 / $250 = 20%
2040+
// Account 2: $200 / $250 = 80%
2041+
// Total should be approximately 100%
2042+
const totalPercentage = result.accountBreakdown.reduce((sum, acc) => sum + acc.percentage, 0);
2043+
expect(totalPercentage).toBeCloseTo(100, 1);
2044+
expect(checking?.percentage).toBeCloseTo(20, 1);
2045+
expect(savings?.percentage).toBeCloseTo(80, 1);
2046+
});
19342047
});
19352048

19362049
describe('Spending Streaks', () => {

src/utils/dataTransform.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -967,18 +967,27 @@ export function transformToWrappedData(
967967
}
968968
});
969969

970-
const accountBreakdown: AccountBreakdown[] = Array.from(accountMap.entries())
971-
.map(([accountId, data]) => ({
972-
accountId,
973-
accountName: accountIdToName.get(accountId) || accountId,
974-
totalSpending: data.totalSpending,
975-
transactionCount: data.transactionCount,
976-
percentage: 0, // Will calculate after sorting
977-
}))
970+
const accountBreakdownUnsorted = Array.from(accountMap.entries()).map(([accountId, data]) => ({
971+
accountId,
972+
accountName: accountIdToName.get(accountId) || accountId,
973+
totalSpending: data.totalSpending,
974+
transactionCount: data.transactionCount,
975+
percentage: 0, // Will calculate after sorting
976+
}));
977+
978+
// Calculate the correct denominator for percentages
979+
// When includeIncomeInCategories is true (net totals mode), use sum of absolute values of net spending
980+
// When false (absolute mode), use totalExpenses
981+
const totalForPercentage = includeIncomeInCategories
982+
? accountBreakdownUnsorted.reduce((sum, acc) => sum + Math.abs(acc.totalSpending), 0)
983+
: totalExpenses;
984+
985+
const accountBreakdown: AccountBreakdown[] = accountBreakdownUnsorted
978986
.sort((a, b) => b.totalSpending - a.totalSpending)
979987
.map(acc => ({
980988
...acc,
981-
percentage: totalExpenses > 0 ? (acc.totalSpending / totalExpenses) * 100 : 0,
989+
percentage:
990+
totalForPercentage > 0 ? (Math.abs(acc.totalSpending) / totalForPercentage) * 100 : 0,
982991
}));
983992

984993
// Spending Streaks

0 commit comments

Comments
 (0)