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

Create Cancel Payment command #32674

Merged
merged 40 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
528ff66
show option in menu
Gonals Dec 6, 2023
d1673b7
display menu in the right spot
Gonals Dec 6, 2023
af46937
extra imports
Gonals Dec 6, 2023
a76ca3c
changing branch
Gonals Dec 6, 2023
51b3e82
Merge branch 'main' into alberto-unPay
Gonals Dec 6, 2023
8912f19
Add confirm modal
Gonals Dec 6, 2023
7418583
Right wording
Gonals Dec 6, 2023
f77c18b
spanish
Gonals Dec 6, 2023
1fb616d
add cancel reason
Gonals Dec 7, 2023
f617954
build optimistic report action
Gonals Dec 7, 2023
c454e56
add optimistic dat
Gonals Dec 7, 2023
41b6cef
add success data
Gonals Dec 7, 2023
8cd3e9a
add failureData
Gonals Dec 7, 2023
5ec2dd0
call command
Gonals Dec 7, 2023
ca24ff4
Connect everything
Gonals Dec 7, 2023
4c866cd
couple of fixes
Gonals Dec 7, 2023
d884d83
change branch
Gonals Dec 12, 2023
d0f78fb
show right message
Gonals Dec 13, 2023
09cfe4a
add original message
Gonals Dec 13, 2023
64268f9
conflicts
Gonals Dec 13, 2023
5e4fe74
style
Gonals Dec 13, 2023
76734d6
lint
Gonals Dec 13, 2023
75f13f1
prettier
Gonals Dec 13, 2023
aecf743
spanish
Gonals Dec 13, 2023
e598cdb
typescript BS
Gonals Dec 13, 2023
d5a68ea
more ts bs
Gonals Dec 14, 2023
4e9f72c
conflicts
Gonals Dec 14, 2023
1f66da6
I hate typescript
Gonals Dec 14, 2023
a9af4cc
prettier
Gonals Dec 14, 2023
79db9ba
typescript
Gonals Dec 14, 2023
7e6afba
prettier again
Gonals Dec 14, 2023
7f4bbf8
have I mentioned how much I hate typescript?
Gonals Dec 14, 2023
e6ff6b2
Merge branch 'main' into alberto-unPay
Gonals Dec 18, 2023
dc57239
remove setIsConfirmModalVisible
Gonals Dec 18, 2023
e17f37a
believe it or not, prettier again
Gonals Dec 18, 2023
a720c1d
conflicts
Gonals Dec 20, 2023
063f842
lint
Gonals Dec 20, 2023
7df1541
conflicts
Gonals Dec 20, 2023
4b6e834
3 dots behavior
Gonals Dec 20, 2023
cd94583
Merge branch 'main' of github.com:Expensify/App into alberto-unPay
stitesExpensify Dec 21, 2023
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
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,9 @@ const CONST = {
},
},
},
CANCEL_PAYMENT_REASONS: {
ADMIN: 'CANCEL_REASON_ADMIN',
},
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
Expand Down
30 changes: 28 additions & 2 deletions src/components/MoneyReportHeader.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import _ from 'underscore';
Expand All @@ -24,7 +24,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
import participantPropTypes from './participantPropTypes';
import SettlementButton from './SettlementButton';
Expand Down Expand Up @@ -74,9 +76,9 @@ const defaultProps = {
};

function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) {
const {windowWidth} = useWindowDimensions();
const styles = useThemeStyles();
const {translate} = useLocalize();
const {windowWidth} = useWindowDimensions();
const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport);
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
Expand All @@ -89,6 +91,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);

const cancelPayment = useCallback(() => {
IOU.cancelPayment(moneyRequestReport, chatReport);
setIsConfirmModalVisible(false);
}, [moneyRequestReport, chatReport]);

const shouldShowPayButton = useMemo(
() => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
[isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
Expand All @@ -109,6 +118,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
const isMoreContentShown = shouldShowNextSteps || (shouldShowAnyButton && isSmallScreenWidth);

const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
if (isPayer && isSettled) {
threeDotsMenuItems.push({
icon: Expensicons.Trashcan,
text: 'Cancel payment',
onSelected: () => setIsConfirmModalVisible(true),
});
}
if (!ReportUtils.isArchivedRoom(chatReport)) {
threeDotsMenuItems.push({
icon: ZoomIcon,
Expand Down Expand Up @@ -206,6 +222,16 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
</View>
)}
</View>
<ConfirmModal
title={translate('iou.cancelPayment')}
isVisible={isConfirmModalVisible}
onConfirm={cancelPayment}
onCancel={() => setIsConfirmModalVisible(false)}
prompt={translate('iou.cancelPaymentConfirmation')}
confirmText={translate('iou.cancelPayment')}
cancelText={translate('common.dismiss')}
danger
/>
</View>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import CONST from '@src/CONST';
import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
Expand Down Expand Up @@ -96,6 +97,7 @@ type AllCountries = Record<keyof typeof CONST.ALL_COUNTRIES, string>;
export default {
common: {
cancel: 'Cancel',
dismiss: 'Dismiss',
yes: 'Yes',
no: 'No',
ok: 'OK',
Expand Down Expand Up @@ -549,6 +551,8 @@ export default {
requestMoney: 'Request money',
sendMoney: 'Send money',
pay: 'Pay',
cancelPayment: 'Cancel payment',
cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?',
viewDetails: 'View details',
pending: 'Pending',
canceled: 'Canceled',
Expand Down Expand Up @@ -585,6 +589,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`,
adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `The ${amount} payment has been cancelled by the admin.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CONST from '@src/CONST';
import type {
AddressLineParams,
AdminCanceledRequestParams,
AlreadySignedInParams,
AmountEachParams,
ApprovedAmountParams,
Expand Down Expand Up @@ -86,6 +87,7 @@ import type {
export default {
common: {
cancel: 'Cancelar',
dismiss: 'Descartar',
yes: 'Sí',
no: 'No',
ok: 'OK',
Expand Down Expand Up @@ -542,6 +544,8 @@ export default {
requestMoney: 'Pedir dinero',
sendMoney: 'Enviar dinero',
pay: 'Pagar',
cancelPayment: 'Cancelar el pago',
cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?',
viewDetails: 'Ver detalles',
pending: 'Pendiente',
canceled: 'Canceló',
Expand Down Expand Up @@ -578,6 +582,7 @@ export default {
payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`,
approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`,
waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`,
adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `El pago de ${amount} ha sido cancelado por el administrador.`,
canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) =>
`Canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`,
settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) =>
Expand Down
3 changes: 3 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ type WaitingOnBankAccountParams = {submitterDisplayName: string};

type CanceledRequestParams = {amount: string; submitterDisplayName: string};

type AdminCanceledRequestParams = {amount: string};

type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string};

type PaidElsewhereWithAmountParams = {payer: string; amount: string};
Expand Down Expand Up @@ -293,6 +295,7 @@ export type {
PayerSettledParams,
WaitingOnBankAccountParams,
CanceledRequestParams,
AdminCanceledRequestParams,
SettledAfterAddedBankAccountParams,
PaidElsewhereWithAmountParams,
PaidWithExpensifyWithAmountParams,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/OptionsListUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ function getLastMessageTextForReport(report) {
} else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) {
lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report);
lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, report);
} else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) {
Expand Down
49 changes: 45 additions & 4 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage';
import {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage';
import {NotificationPreference} from '@src/types/onyx/Report';
import {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction';
import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction';
Expand Down Expand Up @@ -177,6 +177,11 @@ type OptimisticSubmittedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticCancelPaymentReportAction = Pick<
ReportAction,
'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction'
>;

type OptimisticEditedTaskReportAction = Pick<
ReportAction,
'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person'
Expand Down Expand Up @@ -1517,10 +1522,13 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry<ReportActio
/**
* Returns the preview message for `REIMBURSEMENTDEQUEUED` action
*/
function getReimbursementDeQueuedActionMessage(report: OnyxEntry<Report>): string {
function getReimbursementDeQueuedActionMessage(reportAction: OnyxEntry<ReportAction>, report: OnyxEntry<Report>): string {
const amount = CurrencyUtils.convertToDisplayString(Math.abs(report?.total ?? 0), report?.currency);
const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined;
if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) {
return Localize.translateLocal('iou.adminCanceledRequest', {amount});
}
const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? '';
const amount = CurrencyUtils.convertToDisplayString(report?.total ?? 0, report?.currency);

return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount});
}

Expand Down Expand Up @@ -2769,6 +2777,38 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string,
};
}

/**
* Builds an optimistic REIMBURSEMENTDEQUEUED report action with a randomly generated reportActionID.
*
*/
function buildOptimisticCancelPaymentReportAction(): OptimisticCancelPaymentReportAction {
return {
actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED,
actorAccountID: currentUserAccountID,
message: [
{
cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
text: '',
},
],
originalMessage: {
cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN,
},
person: [
{
style: 'strong',
text: currentUserPersonalDetails?.displayName ?? currentUserEmail,
type: 'TEXT',
},
],
reportActionID: NumberUtils.rand64(),
shouldShow: true,
created: DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
};
}

/**
* Builds an optimistic report preview action with a randomly generated reportActionID.
*
Expand Down Expand Up @@ -4245,6 +4285,7 @@ export {
buildOptimisticApprovedReportAction,
buildOptimisticMovedReportAction,
buildOptimisticSubmittedReportAction,
buildOptimisticCancelPaymentReportAction,
buildOptimisticExpenseReport,
buildOptimisticIOUReportAction,
buildOptimisticReportPreview,
Expand Down
103 changes: 103 additions & 0 deletions src/libs/actions/IOU.js
Original file line number Diff line number Diff line change
Expand Up @@ -3091,6 +3091,108 @@ function submitReport(expenseReport) {
);
}

/**
* @param {Object} expenseReport
* @param {Object} chatReport
*/
function cancelPayment(expenseReport, chatReport) {
const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction();
const policy = ReportUtils.getPolicy(chatReport.policyID);
const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE;
const optimisticData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
value: {
[optimisticReportAction.reportActionID]: {
...optimisticReportAction,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
...expenseReport,
lastMessageText: lodashGet(optimisticReportAction, 'message.0.text', ''),
lastMessageHtml: lodashGet(optimisticReportAction, 'message.0.html', ''),
state: isFree ? CONST.REPORT.STATE.SUBMITTED : CONST.REPORT.STATE.OPEN,
stateNum: isFree ? CONST.REPORT.STATE_NUM.PROCESSING : CONST.REPORT.STATE.OPEN,
statusNum: isFree ? CONST.REPORT.STATUS.SUBMITTED : CONST.REPORT.STATE.OPEN,
},
},
...(chatReport.reportID
? [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
value: {
...chatReport,
hasOutstandingIOU: true,
hasOutstandingChildRequest: true,
iouReportID: expenseReport.reportID,
},
},
]
: []),
];

const successData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
value: {
[optimisticReportAction.reportActionID]: {
pendingAction: null,
},
},
},
];

const failureData = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`,
value: {
[expenseReport.reportActionID]: {
errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'),
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
statusNum: CONST.REPORT.STATUS.REIMBURSED,
},
},
...(chatReport.reportID
? [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`,
value: {
hasOutstandingIOU: false,
hasOutstandingChildRequest: false,
iouReportID: 0,
},
},
]
: []),
];

API.write(
'CancelPayment',
{
reportID: expenseReport.reportID,
managerAccountID: expenseReport.managerID,
reportActionID: optimisticReportAction.reportActionID,
},
{optimisticData, successData, failureData},
);
}

/**
* @param {String} paymentType
* @param {Object} chatReport
Expand Down Expand Up @@ -3407,4 +3509,5 @@ export {
detachReceipt,
getIOUReportID,
editMoneyRequest,
cancelPayment,
};
6 changes: 1 addition & 5 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import ControlSelection from '@libs/ControlSelection';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
Expand Down Expand Up @@ -423,10 +422,7 @@ function ReportActionItem(props) {
</ReportActionItemBasicMessage>
);
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) {
const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, [props.report.ownerAccountID, 'displayName']));
const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency);

children = <ReportActionItemBasicMessage message={props.translate('iou.canceledRequest', {submitterDisplayName, amount})} />;
children = <ReportActionItemBasicMessage message={ReportUtils.getReimbursementDeQueuedActionMessage(props.action, props.report)} />;
} else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) {
children = <ReportActionItemBasicMessage message={ModifiedExpenseMessage.getForReportAction(props.action)} />;
} else {
Expand Down
Loading
Loading