diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 63181e4aea87..b75f4e2df845 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -267,6 +267,9 @@ function MoneyRequestConfirmationList(props) { return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); + const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; + useEffect(() => { if (shouldDisplayFieldError && props.hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); @@ -500,7 +503,7 @@ function MoneyRequestConfirmationList(props) { } const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; + const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; const button = shouldShowSettlementButton ? ( )} {shouldShowCategories && ( diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 20012bc90ef0..dab34e324ffa 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -97,6 +97,9 @@ const propTypes = { /** Should the list be read only, and not editable? */ isReadOnly: PropTypes.bool, + /** Whether the money request is a scan request */ + isScanRequest: PropTypes.bool, + /** Depending on expense report or personal IOU report, respective bank account route */ bankAccountRoute: PropTypes.string, @@ -211,6 +214,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isEditingSplitBill, isPolicyExpenseChat, isReadOnly, + isScanRequest, listStyles, mileageRate, onConfirm, @@ -281,6 +285,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const [didConfirm, setDidConfirm] = useState(false); const [didConfirmSplit, setDidConfirmSplit] = useState(false); + const [merchantError, setMerchantError] = useState(false); + const shouldDisplayFieldError = useMemo(() => { if (!isEditingSplitBill) { return false; @@ -289,6 +295,21 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return (hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [isEditingSplitBill, hasSmartScanFailed, transaction, didConfirmSplit]); + const isMerchantEmpty = !iouMerchant || iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; + const isMerchantRequired = isPolicyExpenseChat && !isScanRequest && shouldShowMerchant; + + useEffect(() => { + if ((!isMerchantRequired && isMerchantEmpty) || !merchantError) { + return; + } + if (!isMerchantEmpty && merchantError) { + setMerchantError(false); + if (formError === 'iou.error.invalidMerchant') { + setFormError(''); + } + } + }, [formError, isMerchantEmpty, merchantError, isMerchantRequired]); + useEffect(() => { if (shouldDisplayFieldError && hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); @@ -298,9 +319,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ setFormError('iou.error.genericSmartscanFailureMessage'); return; } + if (merchantError) { + setFormError('iou.error.invalidMerchant'); + return; + } // reset the form error whenever the screen gains or loses focus setFormError(''); - }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]); + }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); useEffect(() => { if (!shouldCalculateDistanceAmount) { @@ -470,6 +495,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (_.isEmpty(selectedParticipants)) { return; } + if ((isMerchantRequired && isMerchantEmpty) || (shouldDisplayFieldError && TransactionUtils.isMerchantMissing(transaction))) { + setMerchantError(true); + return; + } if (iouType === CONST.IOU.TYPE.SEND) { if (!paymentMethod) { @@ -498,7 +527,21 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onConfirm(selectedParticipants); } }, - [selectedParticipants, onSendMoney, onConfirm, isEditingSplitBill, iouType, isDistanceRequest, isDistanceRequestWithoutRoute, iouCurrencyCode, iouAmount, transaction], + [ + selectedParticipants, + isMerchantRequired, + isMerchantEmpty, + shouldDisplayFieldError, + transaction, + iouType, + onSendMoney, + iouCurrencyCode, + isDistanceRequest, + isDistanceRequestWithoutRoute, + iouAmount, + isEditingSplitBill, + onConfirm, + ], ); const footerContent = useMemo(() => { @@ -551,7 +594,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ {button} ); - }, [confirm, bankAccountRoute, iouCurrencyCode, iouType, isReadOnly, policyID, selectedParticipants, splitOrRequestOptions, translate, formError, styles.ph1, styles.mb2]); + }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2, translate]); const {image: receiptImage, thumbnail: receiptThumbnail} = receiptPath && receiptFilename ? ReceiptUtils.getThumbnailAndImageURIs(transaction, receiptPath, receiptFilename) : {}; return ( @@ -629,6 +672,26 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ interactive={!isReadOnly} numberOfLinesTitle={2} /> + {isMerchantRequired && ( + { + if (isEditingSplitBill) { + Navigation.navigate(ROUTES.EDIT_SPLIT_BILL.getRoute(reportID, reportActionID, CONST.EDIT_REQUEST_FIELD.MERCHANT)); + return; + } + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_MERCHANT.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} + disabled={didConfirm} + interactive={!isReadOnly} + brickRoadIndicator={merchantError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + error={merchantError ? translate('common.error.fieldRequired') : ''} + /> + )} {!shouldShowAllFields && ( @@ -680,10 +743,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ interactive={!isReadOnly} /> )} - {shouldShowMerchant && ( + {!isMerchantRequired && shouldShowMerchant && ( )} {shouldShowCategories && ( diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 47031bfc164c..514dc71ffe2c 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -240,7 +240,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', diff --git a/src/languages/es.ts b/src/languages/es.ts index 9b424fc48793..8f44c2a24274 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -613,6 +613,7 @@ export default { genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', + invalidMerchant: 'Por favor ingrese un comerciante correcto.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 6905a542fa5b..75815a2448e4 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -107,7 +107,7 @@ function buildOptimisticTransaction( currency, reportID, comment: commentJSON, - merchant: merchant || CONST.TRANSACTION.DEFAULT_MERCHANT, + merchant: merchant || CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, created: created || DateUtils.getDBTime(), pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, receipt, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d43fefca20bc..3355597e8da6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -160,6 +160,7 @@ function startMoneyRequest_temporaryForRefactor(reportID, isFromGlobalCreate, io reportID, transactionID: newTransactionID, isFromGlobalCreate, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, }); } @@ -278,7 +279,7 @@ function resetMoneyRequestInfo(id = '') { currency: lodashGet(currentUserPersonalDetails, 'localCurrencyCode', CONST.CURRENCY.USD), comment: '', participants: [], - merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, + merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT, category: '', tag: '', created, diff --git a/src/pages/EditRequestMerchantPage.js b/src/pages/EditRequestMerchantPage.js index 5fa14d850f45..c8766d9acc67 100644 --- a/src/pages/EditRequestMerchantPage.js +++ b/src/pages/EditRequestMerchantPage.js @@ -18,22 +18,27 @@ const propTypes = { /** Callback to fire when the Save button is pressed */ onSubmit: PropTypes.func.isRequired, + + /** Boolean to enable validation */ + isPolicyExpenseChat: PropTypes.bool.isRequired, }; -function EditRequestMerchantPage({defaultMerchant, onSubmit}) { +function EditRequestMerchantPage({defaultMerchant, onSubmit, isPolicyExpenseChat}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const merchantInputRef = useRef(null); + const isEmptyMerchant = defaultMerchant === '' || defaultMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || defaultMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; - const validate = useCallback((value) => { - const errors = {}; - - if (_.isEmpty(value.merchant)) { - errors.merchant = 'common.error.fieldRequired'; - } - - return errors; - }, []); + const validate = useCallback( + (value) => { + const errors = {}; + if (_.isEmpty(value.merchant) && value.merchant.trim() === '' && isPolicyExpenseChat) { + errors.merchant = 'common.error.fieldRequired'; + } + return errors; + }, + [isPolicyExpenseChat], + ); return ( { // In case the merchant hasn't been changed, do not make the API request. if (transactionChanges.merchant.trim() === transactionMerchant) { Navigation.dismissModal(); return; } + // This is possible only in case of IOU requests. + if (transactionChanges.merchant.trim() === '') { + editMoneyRequest({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}); + return; + } editMoneyRequest({merchant: transactionChanges.merchant.trim()}); }} /> diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index bf799cd0957b..ce96a09446b9 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -53,6 +53,7 @@ function MoneyRequestMerchantPage({iou, route}) { const {inputCallbackRef} = useAutoFocusInput(); const iouType = lodashGet(route, 'params.iouType', ''); const reportID = lodashGet(route, 'params.reportID', ''); + const isEmptyMerchant = iou.merchant === '' || iou.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; useEffect(() => { const moneyRequestId = `${iouType}${reportID}`; @@ -114,7 +115,7 @@ function MoneyRequestMerchantPage({iou, route}) { InputComponent={TextInput} inputID="moneyRequestMerchant" name="moneyRequestMerchant" - defaultValue={iou.merchant} + defaultValue={isEmptyMerchant ? '' : iou.merchant} maxLength={CONST.MERCHANT_NAME_MAX_LENGTH} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} diff --git a/src/pages/iou/request/step/IOURequestStepMerchant.js b/src/pages/iou/request/step/IOURequestStepMerchant.js index 3234b6046f31..355bb76b89b0 100644 --- a/src/pages/iou/request/step/IOURequestStepMerchant.js +++ b/src/pages/iou/request/step/IOURequestStepMerchant.js @@ -41,6 +41,7 @@ function IOURequestStepMerchant({ const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); + const isEmptyMerchant = merchant === '' || merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const navigateBack = () => { Navigation.goBack(backTo || ROUTES.HOME); @@ -89,7 +90,7 @@ function IOURequestStepMerchant({ InputComponent={TextInput} inputID="moneyRequestMerchant" name="moneyRequestMerchant" - defaultValue={merchant} + defaultValue={isEmptyMerchant ? '' : merchant} maxLength={CONST.MERCHANT_NAME_MAX_LENGTH} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index bb7a7c3424d2..4d9ce42a08ce 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -313,7 +313,7 @@ describe('actions/IOU', () => { // The comment should be correct expect(transaction.comment.comment).toBe(comment); - expect(transaction.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); + expect(transaction.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); // It should be pending expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); @@ -497,7 +497,7 @@ describe('actions/IOU', () => { expect(newTransaction.reportID).toBe(iouReportID); expect(newTransaction.amount).toBe(amount); expect(newTransaction.comment.comment).toBe(comment); - expect(newTransaction.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); + expect(newTransaction.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); expect(newTransaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection @@ -642,7 +642,7 @@ describe('actions/IOU', () => { expect(transaction.reportID).toBe(iouReportID); expect(transaction.amount).toBe(amount); expect(transaction.comment.comment).toBe(comment); - expect(transaction.merchant).toBe(CONST.TRANSACTION.DEFAULT_MERCHANT); + expect(transaction.merchant).toBe(CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT); expect(transaction.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD); // The transactionID on the iou action should match the one from the transactions collection