Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Failed IOU requests are still deletable #18829

Closed
Closed
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
41 changes: 40 additions & 1 deletion src/libs/IOUUtils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
import CONST from '../CONST';

/**
Expand Down Expand Up @@ -118,4 +119,42 @@ function isIOUReportPendingCurrencyConversion(reportActions, iouReport) {
return hasPendingRequests;
}

export {calculateAmount, updateIOUOwnerAndTotal, getIOUReportActions, isIOUReportPendingCurrencyConversion};
/**
* Builds and returns the deletableTransactionIDs array. A transaction must meet multiple requirements in order
* to be deletable. We must exclude transactions not associated with the iouReportID, actions which have already
* been deleted, those which are not of type 'create', and those with errors.
*
* @param {Object[]} reportActions
* @param {String} iouReportID
* @param {String} userEmail
* @param {Boolean} isIOUSettled
* @returns {Array}
*/
function getDeletableTransactions(reportActions, iouReportID, userEmail, isIOUSettled = false) {
if (isIOUSettled) {
return [];
}

// iouReportIDs should be strings, but we still have places that send them as ints so we convert them both to Numbers for comparison
const actionsForIOUReport = _.filter(
reportActions,
(action) => action.originalMessage && action.originalMessage.type && Number(action.originalMessage.IOUReportID) === Number(iouReportID),
);

const deletedTransactionIDs = _.chain(actionsForIOUReport)
.filter((action) => _.contains([CONST.IOU.REPORT_ACTION_TYPE.CANCEL, CONST.IOU.REPORT_ACTION_TYPE.DECLINE, CONST.IOU.REPORT_ACTION_TYPE.DELETE], action.originalMessage.type))
.map((deletedAction) => lodashGet(deletedAction, 'originalMessage.IOUTransactionID', ''))
.compact()
.value();

return _.chain(actionsForIOUReport)
.filter((action) => action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE)
.filter((action) => !_.contains(deletedTransactionIDs, action.originalMessage.IOUTransactionID))
.filter((action) => userEmail === action.actorEmail)
.filter((action) => _.isEmpty(action.errors))
.map((action) => lodashGet(action, 'originalMessage.IOUTransactionID', ''))
.compact()
.value();
}

export {calculateAmount, updateIOUOwnerAndTotal, getIOUReportActions, isIOUReportPendingCurrencyConversion, getDeletableTransactions};
45 changes: 3 additions & 42 deletions src/pages/iou/IOUTransactions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
import PropTypes from 'prop-types';
import lodashGet from 'lodash/get';
import styles from '../../styles/styles';
import ONYXKEYS from '../../ONYXKEYS';
import * as ReportActionsUtils from '../../libs/ReportActionsUtils';
import * as IOUUtils from '../../libs/IOUUtils';
import reportActionPropTypes from '../home/report/reportActionPropTypes';
import ReportTransaction from '../../components/ReportTransaction';
import CONST from '../../CONST';

const propTypes = {
/** Actions from the ChatReport */
Expand All @@ -34,47 +33,10 @@ const defaultProps = {
};

class IOUTransactions extends Component {
constructor(props) {
super(props);

this.getDeletableTransactions = this.getDeletableTransactions.bind(this);
}

/**
* Builds and returns the deletableTransactionIDs array. A transaction must meet multiple requirements in order
* to be deletable. We must exclude transactions not associated with the iouReportID, actions which have already
* been deleted, and those which are not of type 'create'.
*
* @returns {Array}
*/
getDeletableTransactions() {
if (this.props.isIOUSettled) {
return [];
}

// iouReportIDs should be strings, but we still have places that send them as ints so we convert them both to Numbers for comparison
const actionsForIOUReport = _.filter(
this.props.reportActions,
(action) => action.originalMessage && action.originalMessage.type && Number(action.originalMessage.IOUReportID) === Number(this.props.iouReportID),
);

const deletedTransactionIDs = _.chain(actionsForIOUReport)
.filter((action) => _.contains([CONST.IOU.REPORT_ACTION_TYPE.CANCEL, CONST.IOU.REPORT_ACTION_TYPE.DECLINE, CONST.IOU.REPORT_ACTION_TYPE.DELETE], action.originalMessage.type))
.map((deletedAction) => lodashGet(deletedAction, 'originalMessage.IOUTransactionID', ''))
.compact()
.value();

return _.chain(actionsForIOUReport)
.filter((action) => action.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE)
.filter((action) => !_.contains(deletedTransactionIDs, action.originalMessage.IOUTransactionID))
.filter((action) => this.props.userEmail === action.actorEmail)
.map((action) => lodashGet(action, 'originalMessage.IOUTransactionID', ''))
.compact()
.value();
}

render() {
const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(this.props.reportActions);
const deletableTransactions = IOUUtils.getDeletableTransactions(this.props.reportActions, this.props.iouReportID, this.props.userEmail, this.props.isIOUSettled);

return (
<View style={[styles.mt3]}>
{_.map(sortedReportActions, (reportAction) => {
Expand All @@ -83,7 +45,6 @@ class IOUTransactions extends Component {
return;
}

const deletableTransactions = this.getDeletableTransactions();
const canBeDeleted = _.contains(deletableTransactions, reportAction.originalMessage.IOUTransactionID);
return (
<ReportTransaction
Expand Down
39 changes: 38 additions & 1 deletion tests/unit/IOUUtilsTest.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import * as IOUUtils from '../../src/libs/IOUUtils';
import * as ReportUtils from '../../src/libs/ReportUtils';
import * as NumberUtils from '../../src/libs/NumberUtils';
import CONST from '../../src/CONST';
import ONYXKEYS from '../../src/ONYXKEYS';
import waitForPromisesToResolve from '../utils/waitForPromisesToResolve';
import currencyList from './currencyList.json';
import DateUtils from '../../src/libs/DateUtils';

let iouReport;
let reportActions;
const ownerEmail = '[email protected]';
const managerEmail = '[email protected]';

function createIOUReportAction(type, amount, currency, isOffline = false, IOUTransactionID = NumberUtils.rand64()) {
function createIOUReportAction(type, amount, currency, isOffline = false, isError = false, IOUTransactionID = NumberUtils.rand64()) {
const moneyRequestAction = ReportUtils.buildOptimisticIOUReportAction(type, amount, currency, 'Test comment', [managerEmail], IOUTransactionID, '', iouReport.reportID);

// Default is to create requests online, if `isOffline` is not specified then we need to remove the pendingAction
if (!isOffline) {
moneyRequestAction.pendingAction = null;
}

if (isError) {
moneyRequestAction.errors = {
[DateUtils.getMicroseconds()]: 'Unexpected error requesting money, please try again later',
};
}

reportActions.push(moneyRequestAction);
return moneyRequestAction;
}
Expand All @@ -30,6 +38,7 @@ function deleteMoneyRequest(moneyRequestAction, isOffline = false) {
moneyRequestAction.originalMessage.amount,
moneyRequestAction.originalMessage.currency,
isOffline,
false,
moneyRequestAction.originalMessage.IOUTransactionID,
);
}
Expand Down Expand Up @@ -153,4 +162,32 @@ describe('IOUUtils', () => {
expect(IOUUtils.calculateAmount(participants.length, 2)).toBe(1);
});
});

describe('getDeletableTransactions', () => {
beforeAll(() => {
Onyx.set(ONYXKEYS.SESSION, {email: managerEmail});
});

beforeEach(() => {
reportActions = [];
const chatReportID = ReportUtils.generateReportID();
const amount = 1000;
const currency = 'USD';

iouReport = ReportUtils.buildOptimisticIOUReport(ownerEmail, managerEmail, amount, chatReportID, currency);
createIOUReportAction('create', amount, currency);
});

test('Without failed request]', () => {
createIOUReportAction('create', 200, 'JPY');
const expectedOutput = _.map(reportActions, (action) => action.originalMessage.IOUTransactionID);
expect(IOUUtils.getDeletableTransactions(reportActions, iouReport.reportID, managerEmail)).toStrictEqual(expectedOutput);
});

test('With failed request', () => {
createIOUReportAction('create', 200, 'JPY', false, true);
const expectedOutput = _.map(reportActions.slice(0, 1), (action) => action.originalMessage.IOUTransactionID);
expect(IOUUtils.getDeletableTransactions(reportActions, iouReport.reportID, managerEmail)).toStrictEqual(expectedOutput);
});
});
});