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
37 changes: 25 additions & 12 deletions src/libs/ExpensifyCardStatementUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,16 @@ function isWithdrawalIDGroup(value: SearchResultDataType[keyof SearchResultDataT
return typeof value === 'object' && value !== null && 'entryID' in value && typeof value.entryID === 'number';
}

function getSelectedSettlementGroups(selectedTransactions: SelectedTransactions, searchData: SearchResultDataType | undefined): SearchWithdrawalIDGroup[] {
function getSelectedSettlementGroups(selectedTransactions: SelectedTransactions, searchData: SearchResultDataType | undefined): SearchWithdrawalIDGroup[] | undefined {
if (!searchData) {
return [];
}

// Only offer the statement when a whole settlement is selected, never a single transaction inside it. A settlement
// is selected either directly (its group key is in selectedTransactions, e.g. a collapsed row) or by selecting all
// of its transactions (each tagged with the group key), so we count tagged children and require the full count.
// A settlement can be selected two ways: its collapsed row is checked directly (its group key lands in
// selectedTransactions), or its row is expanded and its transactions are checked (each tagged with the group key).
// Collect both so we can tell, per settlement, whether the whole thing or only part of it is selected.
const directlySelectedGroupKeys = new Set<string>();
const selectedCountByGroupKey = new Map<string, number>();
const selectedTransactionCountByGroupKey = new Map<string, number>();
for (const [key, selection] of Object.entries(selectedTransactions)) {
if (!selection?.isSelected) {
continue;
Expand All @@ -129,20 +129,33 @@ function getSelectedSettlementGroups(selectedTransactions: SelectedTransactions,
directlySelectedGroupKeys.add(key);
}
if (selection.groupKey?.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
selectedCountByGroupKey.set(selection.groupKey, (selectedCountByGroupKey.get(selection.groupKey) ?? 0) + 1);
selectedTransactionCountByGroupKey.set(selection.groupKey, (selectedTransactionCountByGroupKey.get(selection.groupKey) ?? 0) + 1);
}
}

const settlementGroups: SearchWithdrawalIDGroup[] = [];
for (const [key, value] of Object.entries(searchData)) {
if (!isWithdrawalIDGroup(value)) {
for (const [groupKey, group] of Object.entries(searchData)) {
if (!isWithdrawalIDGroup(group)) {
continue;
}
const isWholeSettlementSelected = directlySelectedGroupKeys.has(key) || (value.count > 0 && (selectedCountByGroupKey.get(key) ?? 0) >= value.count);
if (!isWholeSettlementSelected) {

const selectedTransactionCount = selectedTransactionCountByGroupKey.get(groupKey) ?? 0;
const isRowSelectedDirectly = directlySelectedGroupKeys.has(groupKey);
const areAllTransactionsSelected = group.count > 0 && selectedTransactionCount >= group.count;

// Whole settlement selected: include it in the statement.
if (isRowSelectedDirectly || areAllTransactionsSelected) {
settlementGroups.push(group);
continue;
}
settlementGroups.push(value);

// Only some of this settlement's transactions are selected. The statement always covers the whole settlement,
// so a partial selection would export more than what's on screen. Hide the action entirely.
if (selectedTransactionCount > 0) {
return undefined;
}

// Otherwise the settlement isn't part of the selection at all, so leave it out.
}

return settlementGroups;
Expand Down Expand Up @@ -170,7 +183,7 @@ function getExpensifyCardStatementSelection(
}

const selectedSettlementGroups = getSelectedSettlementGroups(selectedTransactions, searchData);
if (selectedSettlementGroups.length === 0) {
if (!selectedSettlementGroups || selectedSettlementGroups.length === 0) {
return undefined;
}

Expand Down
17 changes: 17 additions & 0 deletions tests/unit/ExpensifyCardStatementUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ describe('ExpensifyCardStatementUtils', () => {
expect(getExpensifyCardStatementSelection(expensifyCardStatementQueryJSON, selectedTransactions, searchData)).toBeUndefined();
});

it('hides the export when one settlement is whole but another is only partially selected', () => {
const wholeGroupKey = `${CONST.SEARCH.GROUP_PREFIX}123`;
const partialGroupKey = `${CONST.SEARCH.GROUP_PREFIX}456`;
// Settlement 123 fully selected, settlement 456 missing one transaction. A partial settlement narrows the
// selection, so the action must disappear rather than silently exporting only the whole one.
const selectedTransactions: SelectedTransactions = {
...makeSettlementSelection(wholeGroupKey, 2),
...makeSettlementSelection(partialGroupKey, 1),
};
const searchData = makeSearchData({
[wholeGroupKey]: makeSettlementGroup({entryID: 123, count: 2}),
[partialGroupKey]: makeSettlementGroup({entryID: 456, count: 2}),
});

expect(getExpensifyCardStatementSelection(expensifyCardStatementQueryJSON, selectedTransactions, searchData)).toBeUndefined();
});

it('includes a settlement when all of its transactions are selected', () => {
const groupKey = `${CONST.SEARCH.GROUP_PREFIX}123`;
// Every transaction in the (expanded) settlement is selected, so the whole settlement is selected.
Expand Down
Loading