Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions src/libs/ReportLayoutUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type Report from '@src/types/onyx/Report';
import type Transaction from '@src/types/onyx/Transaction';
import {isCategoryMissing} from './CategoryUtils';
import isTagMissing from './TagUtils';
import {getAmount, getCategory, getTag} from './TransactionUtils';
import {getAmount, getCategory, getCurrency, getTag, isTransactionPendingDelete} from './TransactionUtils';

/**
* Sorts groups alphabetically (A→Z) with empty keys at the end
Expand All @@ -32,11 +32,17 @@ function getConvertedAmount(transaction: Transaction): number {

/**
* Calculates group total using amount for same-currency transactions, falls back to convertedAmount for multi-currency
* Excludes transactions that are pending delete
*/
function calculateGroupTotal(transactionList: Transaction[], reportCurrency: string): number {
let total = 0;
for (const transaction of transactionList) {
if (transaction.currency === reportCurrency) {
if (isTransactionPendingDelete(transaction)) {
continue;
}

const transactionCurrency = getCurrency(transaction);
if (transactionCurrency === reportCurrency) {
total += getAmount(transaction, true, false, true);
} else if (transaction.convertedAmount) {
total += getConvertedAmount(transaction);
Expand Down
169 changes: 169 additions & 0 deletions tests/unit/ReportLayoutUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {groupTransactionsByCategory, groupTransactionsByTag} from '@libs/ReportLayoutUtils';
import CONST from '@src/CONST';
import type {Report, Transaction} from '@src/types/onyx';

const mockLocaleCompare = (a: string, b: string) => a.localeCompare(b);
Expand Down Expand Up @@ -121,6 +122,26 @@ describe('groupTransactionsByCategory', () => {
expect(travelGroup?.subTotalAmount).toBe(500);
});

it('uses modifiedCurrency to determine transaction currency when edited', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({
transactionID: '1',
category: 'Travel',
amount: -1200,
currency: 'USD',
modifiedCurrency: 'AED',
convertedAmount: -8,
}),
createMockTransaction({transactionID: '2', category: 'Travel', amount: -500, currency: 'USD'}),
];

const result = groupTransactionsByCategory(transactions, report, mockLocaleCompare);
const travelGroup = result.find((g) => g.groupKey === 'Travel');

expect(travelGroup?.subTotalAmount).toBe(508);
});

it('sets groupName equal to groupKey', () => {
const report = createMockReport();
const transactions = [createMockTransaction({transactionID: '1', category: 'Travel', amount: -1000})];
Expand All @@ -144,6 +165,70 @@ describe('groupTransactionsByCategory', () => {
expect(result.at(0)?.groupKey).toBe('');
expect(result.at(0)?.transactions).toHaveLength(2);
});

it('excludes pending delete transactions from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', category: 'Travel', amount: -1000, currency: 'USD'}),
createMockTransaction({transactionID: '2', category: 'Travel', amount: -1200, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByCategory(transactions, report, mockLocaleCompare);
const travelGroup = result.find((g) => g.groupKey === 'Travel');

expect(travelGroup?.subTotalAmount).toBe(1000);
expect(travelGroup?.transactions).toHaveLength(2);
});

it('returns zero subtotal when all transactions in group are pending delete', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', category: 'Travel', amount: -1000, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
createMockTransaction({transactionID: '2', category: 'Travel', amount: -1200, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByCategory(transactions, report, mockLocaleCompare);
const travelGroup = result.find((g) => g.groupKey === 'Travel');

expect(travelGroup?.subTotalAmount).toBe(0);
expect(travelGroup?.transactions).toHaveLength(2);
});

it('excludes pending delete multi-currency transactions from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', category: 'Travel', amount: -1000, currency: 'EUR', convertedAmount: -1200}),
createMockTransaction({transactionID: '2', category: 'Travel', amount: -500, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByCategory(transactions, report, mockLocaleCompare);
const travelGroup = result.find((g) => g.groupKey === 'Travel');

expect(travelGroup?.subTotalAmount).toBe(1200);
expect(travelGroup?.transactions).toHaveLength(2);
});

it('excludes pending delete transactions with modifiedCurrency from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', category: 'Travel', amount: -1000, currency: 'USD'}),
createMockTransaction({
transactionID: '2',
category: 'Travel',
amount: -1200,
currency: 'USD',
modifiedCurrency: 'AED',
convertedAmount: -50,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
}),
];

const result = groupTransactionsByCategory(transactions, report, mockLocaleCompare);
const travelGroup = result.find((g) => g.groupKey === 'Travel');

expect(travelGroup?.subTotalAmount).toBe(1000);
expect(travelGroup?.transactions).toHaveLength(2);
});
});

describe('groupTransactionsByTag', () => {
Expand Down Expand Up @@ -246,6 +331,26 @@ describe('groupTransactionsByTag', () => {
expect(projectAGroup?.subTotalAmount).toBe(500);
});

it('uses modifiedCurrency to determine transaction currency when edited', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({
transactionID: '1',
tag: 'Project A',
amount: -1200,
currency: 'USD',
modifiedCurrency: 'AED',
convertedAmount: -8,
}),
createMockTransaction({transactionID: '2', tag: 'Project A', amount: -500, currency: 'USD'}),
];

const result = groupTransactionsByTag(transactions, report, mockLocaleCompare);
const projectAGroup = result.find((g) => g.groupKey === 'Project A');

expect(projectAGroup?.subTotalAmount).toBe(508);
});

it('sets groupName equal to groupKey', () => {
const report = createMockReport();
const transactions = [createMockTransaction({transactionID: '1', tag: 'Project A', amount: -1000})];
Expand All @@ -265,4 +370,68 @@ describe('groupTransactionsByTag', () => {
expect(result).toHaveLength(2);
expect(result.find((g) => g.groupKey === '')?.transactions).toHaveLength(1);
});

it('excludes pending delete transactions from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', tag: 'Project A', amount: -1000, currency: 'USD'}),
createMockTransaction({transactionID: '2', tag: 'Project A', amount: -1200, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByTag(transactions, report, mockLocaleCompare);
const projectAGroup = result.find((g) => g.groupKey === 'Project A');

expect(projectAGroup?.subTotalAmount).toBe(1000);
expect(projectAGroup?.transactions).toHaveLength(2);
});

it('returns zero subtotal when all transactions in group are pending delete', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', tag: 'Project A', amount: -1000, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
createMockTransaction({transactionID: '2', tag: 'Project A', amount: -1200, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByTag(transactions, report, mockLocaleCompare);
const projectAGroup = result.find((g) => g.groupKey === 'Project A');

expect(projectAGroup?.subTotalAmount).toBe(0);
expect(projectAGroup?.transactions).toHaveLength(2);
});

it('excludes pending delete multi-currency transactions from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', tag: 'Project A', amount: -1000, currency: 'EUR', convertedAmount: -1200}),
createMockTransaction({transactionID: '2', tag: 'Project A', amount: -500, currency: 'USD', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}),
];

const result = groupTransactionsByTag(transactions, report, mockLocaleCompare);
const projectAGroup = result.find((g) => g.groupKey === 'Project A');

expect(projectAGroup?.subTotalAmount).toBe(1200);
expect(projectAGroup?.transactions).toHaveLength(2);
});

it('excludes pending delete transactions with modifiedCurrency from subtotal calculation', () => {
const report = createMockReport({currency: 'USD'});
const transactions = [
createMockTransaction({transactionID: '1', tag: 'Project A', amount: -1000, currency: 'USD'}),
createMockTransaction({
transactionID: '2',
tag: 'Project A',
amount: -1200,
currency: 'USD',
modifiedCurrency: 'AED',
convertedAmount: -50,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
}),
];

const result = groupTransactionsByTag(transactions, report, mockLocaleCompare);
const projectAGroup = result.find((g) => g.groupKey === 'Project A');

expect(projectAGroup?.subTotalAmount).toBe(1000);
expect(projectAGroup?.transactions).toHaveLength(2);
});
});
Loading