Skip to content

Commit

Permalink
Add option to make a transfer from two selected transactions (#2398)
Browse files Browse the repository at this point in the history
* Adds option to make a transfer from two selected transactions

- Transactions amount must match
- Transactions must be from different accounts
- Split transactions not eligible
  • Loading branch information
twk3 authored Mar 3, 2024
1 parent 4b03446 commit 6626164
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 2 deletions.
51 changes: 49 additions & 2 deletions packages/desktop-client/e2e/accounts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ test.describe('Accounts', () => {
let page;
let navigation;
let configurationPage;
let accountPage;

test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
Expand All @@ -22,7 +23,7 @@ test.describe('Accounts', () => {
});

test('creates a new account and views the initial balance transaction', async () => {
const accountPage = await navigation.createAccount({
accountPage = await navigation.createAccount({
name: 'New Account',
offBudget: false,
balance: 100,
Expand All @@ -38,7 +39,7 @@ test.describe('Accounts', () => {
});

test('closes an account', async () => {
const accountPage = await navigation.goToAccountPage('Roth IRA');
accountPage = await navigation.goToAccountPage('Roth IRA');

await expect(accountPage.accountName).toHaveText('Roth IRA');

Expand All @@ -50,4 +51,50 @@ test.describe('Accounts', () => {
await expect(accountPage.accountName).toHaveText('Closed: Roth IRA');
await expect(page).toMatchThemeScreenshots();
});

test.describe('Budgeted Accounts', () => {
// Reset filters
test.afterEach(async () => {
await accountPage.removeFilter(0);
});

test('creates a transfer from two existing transactions', async () => {
accountPage = await navigation.goToAccountPage('For budget');
await expect(accountPage.accountName).toHaveText('Budgeted Accounts');

await accountPage.filterByNote('Test Acc Transfer');

await accountPage.createSingleTransaction({
account: 'Ally Savings',
payee: '',
notes: 'Test Acc Transfer',
category: 'Food',
debit: '34.56',
});

await accountPage.createSingleTransaction({
account: 'HSBC',
payee: '',
notes: 'Test Acc Transfer',
category: 'Food',
credit: '34.56',
});

await accountPage.selectNthTransaction(0);
await accountPage.selectNthTransaction(1);
await accountPage.clickSelectAction('Make transfer');

let transaction = accountPage.getNthTransaction(0);
await expect(transaction.payee).toHaveText('Ally Savings');
await expect(transaction.category).toHaveText('Transfer');
await expect(transaction.credit).toHaveText('34.56');
await expect(transaction.account).toHaveText('HSBC');

transaction = accountPage.getNthTransaction(1);
await expect(transaction.payee).toHaveText('HSBC');
await expect(transaction.category).toHaveText('Transfer');
await expect(transaction.debit).toHaveText('34.56');
await expect(transaction.account).toHaveText('Ally Savings');
});
});
});
30 changes: 30 additions & 0 deletions packages/desktop-client/e2e/page-models/account-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export class AccountPage {

this.filterButton = this.page.getByRole('button', { name: 'Filter' });
this.filterSelectTooltip = this.page.getByTestId('filters-select-tooltip');

this.selectButton = this.page.getByTestId('transactions-select-button');
this.selectTooltip = this.page.getByTestId('transactions-select-tooltip');
}

/**
Expand Down Expand Up @@ -68,14 +71,21 @@ export class AccountPage {
await this.cancelTransactionButton.click();
}

async selectNthTransaction(index) {
const row = this.transactionTableRow.nth(index);
await row.getByTestId('select').click();
}

/**
* Retrieve the data for the nth-transaction.
* 0-based index
*/
getNthTransaction(index) {
const row = this.transactionTableRow.nth(index);
const account = row.getByTestId('account');

return {
...(account ? { account } : {}),
payee: row.getByTestId('payee'),
notes: row.getByTestId('notes'),
category: row.getByTestId('category'),
Expand All @@ -84,6 +94,11 @@ export class AccountPage {
};
}

async clickSelectAction(action) {
await this.selectButton.click();
await this.selectTooltip.getByRole('button', { name: action }).click();
}

/**
* Open the modal for closing the account.
*/
Expand All @@ -106,6 +121,15 @@ export class AccountPage {
return new FilterTooltip(this.page.getByTestId('filters-menu-tooltip'));
}

/**
* Filter to a specific note
*/
async filterByNote(note) {
const filterTooltip = await this.filterBy('Note');
await this.page.keyboard.type(note);
await filterTooltip.applyButton.click();
}

/**
* Remove the nth filter
*/
Expand All @@ -117,6 +141,12 @@ export class AccountPage {
}

async _fillTransactionFields(transactionRow, transaction) {
if (transaction.account) {
await transactionRow.getByTestId('account').click();
await this.page.keyboard.type(transaction.account);
await this.page.keyboard.press('Tab');
}

if (transaction.payee) {
await transactionRow.getByTestId('payee').click();
await this.page.keyboard.type(transaction.payee);
Expand Down
48 changes: 48 additions & 0 deletions packages/desktop-client/src/components/accounts/Account.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Navigate, useParams, useLocation, useMatch } from 'react-router-dom';
import { debounce } from 'debounce';
import { bindActionCreators } from 'redux';

import { validForTransfer } from 'loot-core/client/transfer';
import * as actions from 'loot-core/src/client/actions';
import { useFilters } from 'loot-core/src/client/data-hooks/filters';
import {
Expand Down Expand Up @@ -1059,6 +1060,52 @@ class AccountInternal extends PureComponent {
this.props.pushModal('edit-rule', { rule });
};

onSetTransfer = async ids => {
const onConfirmTransfer = async ids => {
this.setState({ workingHard: true });

const payees = await this.props.getPayees();
const { data: transactions } = await runQuery(
q('transactions')
.filter({ id: { $oneof: ids } })
.select('*'),
);
const [fromTrans, toTrans] = transactions;

if (transactions.length === 2 && validForTransfer(fromTrans, toTrans)) {
const fromPayee = payees.find(
p => p.transfer_acct === fromTrans.account,
);
const toPayee = payees.find(p => p.transfer_acct === toTrans.account);

const changes = {
updated: [
{
...fromTrans,
payee: toPayee.id,
transfer_id: toTrans.id,
},
{
...toTrans,
payee: fromPayee.id,
transfer_id: fromTrans.id,
},
],
};

await send('transactions-batch-update', changes);
}

await this.refetchTransactions();
};

await this.checkForReconciledTransactions(
ids,
'batchEditWithReconciled',
onConfirmTransfer,
);
};

onCondOpChange = (value, filters) => {
this.setState({ conditionsOp: value });
this.setState({ filterId: { ...this.state.filterId, status: 'changed' } });
Expand Down Expand Up @@ -1443,6 +1490,7 @@ class AccountInternal extends PureComponent {
onDeleteFilter={this.onDeleteFilter}
onApplyFilter={this.onApplyFilter}
onScheduleAction={this.onScheduleAction}
onSetTransfer={this.onSetTransfer}
/>

<View style={{ flex: 1 }}>
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop-client/src/components/accounts/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function AccountHeader({
onCondOpChange,
onDeleteFilter,
onScheduleAction,
onSetTransfer,
}) {
const [menuOpen, setMenuOpen] = useState(false);
const searchInput = useRef(null);
Expand All @@ -94,6 +95,9 @@ export function AccountHeader({
canSync = !!accounts.find(account => !!account.account_id) && isUsingServer;
}

// Only show the ability to make linked transfers on multi-account views.
const showMakeTransfer = !account;

function onToggleSplits() {
if (tableRef.current) {
splitsExpanded.dispatch({
Expand Down Expand Up @@ -276,8 +280,10 @@ export function AccountHeader({
onEdit={onBatchEdit}
onUnlink={onBatchUnlink}
onCreateRule={onCreateRule}
onSetTransfer={onSetTransfer}
onScheduleAction={onScheduleAction}
pushModal={pushModal}
showMakeTransfer={showMakeTransfer}
/>
)}
<Button
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop-client/src/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,7 @@ export function SelectedItemsButton({ name, keyHandlers, items, onSelect }) {
type="bare"
style={{ color: theme.pageTextPositive }}
onClick={() => setMenuOpen(true)}
data-testid={name + '-select-button'}
>
<SvgExpandArrow
width={8}
Expand All @@ -816,6 +817,7 @@ export function SelectedItemsButton({ name, keyHandlers, items, onSelect }) {
width={200}
style={{ padding: 0, backgroundColor: theme.menuBackground }}
onClose={() => setMenuOpen(false)}
data-testid={name + '-select-tooltip'}
>
<Menu
onMenuSelect={name => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useMemo } from 'react';

import { validForTransfer } from 'loot-core/src/client/transfer';

import { useSelectedItems } from '../../hooks/useSelected';
import { Menu } from '../common/Menu';
import { SelectedItemsButton } from '../table';
Expand All @@ -14,8 +16,10 @@ export function SelectedTransactionsButton({
onEdit,
onUnlink,
onCreateRule,
onSetTransfer,
onScheduleAction,
pushModal,
showMakeTransfer,
}) {
const selectedItems = useSelectedItems();

Expand Down Expand Up @@ -43,6 +47,23 @@ export function SelectedTransactionsButton({
);
}, [types.preview, selectedItems, getTransaction]);

const canBeTransfer = useMemo(() => {
// only two selected
if (selectedItems.size !== 2) {
return false;
}
const transactions = [...selectedItems];
const fromTrans = getTransaction(transactions[0]);
const toTrans = getTransaction(transactions[1]);

// previously selected transactions aren't always present in current transaction list
if (!fromTrans || !toTrans) {
return false;
}

return validForTransfer(fromTrans, toTrans);
}, [selectedItems, getTransaction]);

return (
<SelectedItemsButton
name="transactions"
Expand Down Expand Up @@ -91,6 +112,15 @@ export function SelectedTransactionsButton({
text: 'Create rule',
},
]),
...(showMakeTransfer
? [
{
name: 'set-transfer',
text: 'Make transfer',
disabled: !canBeTransfer,
},
]
: []),
Menu.line,
{ type: Menu.label, name: 'Edit field' },
{ name: 'date', text: 'Date' },
Expand Down Expand Up @@ -145,6 +175,9 @@ export function SelectedTransactionsButton({
case 'create-rule':
onCreateRule([...selectedItems]);
break;
case 'set-transfer':
onSetTransfer([...selectedItems]);
break;
default:
onEdit(name, [...selectedItems]);
}
Expand Down
Loading

0 comments on commit 6626164

Please sign in to comment.