diff --git a/src/languages/en.ts b/src/languages/en.ts index 872e451452ba..8a29cc9cdd97 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -594,11 +594,10 @@ export default { noReimbursableExpenses: 'This report has an invalid amount', pendingConversionMessage: "Total will update when you're back online", changedTheRequest: 'changed the request', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `set the ${valueName} to ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `the ${valueName} to ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `set the distance to ${newDistanceToDisplay}, which set the amount to ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `removed the ${valueName} (previously ${oldValueToDisplay})`, - updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `changed the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `the ${valueName} (previously ${oldValueToDisplay})`, + updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => `the ${valueName} to ${newValueToDisplay} (previously ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `changed the distance to ${newDistanceToDisplay} (previously ${oldDistanceToDisplay}), which updated the amount to ${newAmountToDisplay} (previously ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} request${comment ? ` for ${comment}` : ''}`, @@ -622,6 +621,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', + set: 'set', + changed: 'changed', + removed: 'removed', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 44f75e351437..85f3c4c46703 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -587,13 +587,12 @@ export default { noReimbursableExpenses: 'El importe de este informe no es válido', pendingConversionMessage: 'El total se actualizará cuando estés online', changedTheRequest: 'cambió la solicitud', - setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `estableció ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, + setTheRequest: ({valueName, newValueToDisplay}: SetTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay}`, setTheDistance: ({newDistanceToDisplay, newAmountToDisplay}: SetTheDistanceParams) => `estableció la distancia a ${newDistanceToDisplay}, lo que estableció el importe a ${newAmountToDisplay}`, - removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => - `eliminó ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, + removedTheRequest: ({valueName, oldValueToDisplay}: RemovedTheRequestParams) => `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} (previamente ${oldValueToDisplay})`, updatedTheRequest: ({valueName, newValueToDisplay, oldValueToDisplay}: UpdatedTheRequestParams) => - `cambió ${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, + `${valueName === 'comerciante' ? 'el' : 'la'} ${valueName} a ${newValueToDisplay} (previamente ${oldValueToDisplay})`, updatedTheDistance: ({newDistanceToDisplay, oldDistanceToDisplay, newAmountToDisplay, oldAmountToDisplay}: UpdatedTheDistanceParams) => `cambió la distancia a ${newDistanceToDisplay} (previamente ${oldDistanceToDisplay}), lo que cambió el importe a ${newAmountToDisplay} (previamente ${oldAmountToDisplay})`, threadRequestReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Solicitud de ${formattedAmount}${comment ? ` para ${comment}` : ''}`, @@ -617,6 +616,9 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', + set: 'estableció', + changed: 'cambió', + removed: 'eliminó', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts new file mode 100644 index 000000000000..c3d9b0a85339 --- /dev/null +++ b/src/libs/ModifiedExpenseMessage.ts @@ -0,0 +1,226 @@ +import {format} from 'date-fns'; +import Onyx from 'react-native-onyx'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {PolicyTags, ReportAction} from '@src/types/onyx'; +import * as CurrencyUtils from './CurrencyUtils'; +import * as Localize from './Localize'; +import * as PolicyUtils from './PolicyUtils'; +import * as ReportUtils from './ReportUtils'; +import {ExpenseOriginalMessage} from './ReportUtils'; + +let allPolicyTags: Record = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY_TAGS, + waitForCollectionCallback: true, + callback: (value) => { + if (!value) { + allPolicyTags = {}; + return; + } + allPolicyTags = value; + }, +}); + +/** + * Builds the partial message fragment for a modified field on the expense. + */ +function buildMessageFragmentForValue( + newValue: string, + oldValue: string, + valueName: string, + valueInQuotes: boolean, + setFragments: string[], + removalFragments: string[], + changeFragments: string[], + shouldConvertToLowercase = true, +) { + const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; + const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; + const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; + + if (!oldValue) { + const fragment = Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); + setFragments.push(fragment); + } else if (!newValue) { + const fragment = Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); + removalFragments.push(fragment); + } else { + const fragment = Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); + changeFragments.push(fragment); + } +} + +/** + * Get the message line for a modified expense. + */ +function getMessageLine(prefix: string, messageFragments: string[]): string { + if (messageFragments.length === 0) { + return ''; + } + return messageFragments.reduce((acc, value, index) => { + if (index === messageFragments.length - 1) { + if (messageFragments.length === 1) { + return `${acc} ${value}.`; + } + if (messageFragments.length === 2) { + return `${acc} ${Localize.translateLocal('common.and')} ${value}.`; + } + return `${acc}, ${Localize.translateLocal('common.and')} ${value}.`; + } + if (index === 0) { + return `${acc} ${value}`; + } + return `${acc}, ${value}`; + }, prefix); +} + +function getForDistanceRequest(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { + if (!oldDistance) { + return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); + } + return Localize.translateLocal('iou.updatedTheDistance', { + newDistanceToDisplay: newDistance, + oldDistanceToDisplay: oldDistance, + newAmountToDisplay: newAmount, + oldAmountToDisplay: oldAmount, + }); +} + +/** + * Get the report action message when expense has been modified. + * + * ModifiedExpense::getNewDotComment in Web-Expensify should match this. + * If we change this function be sure to update the backend as well. + */ +function getForReportAction(reportAction: ReportAction): string { + if (reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { + return ''; + } + const reportActionOriginalMessage = reportAction.originalMessage as ExpenseOriginalMessage | undefined; + const policyID = ReportUtils.getReportPolicyID(reportAction.reportID) ?? ''; + const policyTags = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; + const policyTagListName = PolicyUtils.getTagListName(policyTags) || Localize.translateLocal('common.tag'); + + const removalFragments: string[] = []; + const setFragments: string[] = []; + const changeFragments: string[] = []; + + const hasModifiedAmount = + reportActionOriginalMessage && + 'oldAmount' in reportActionOriginalMessage && + 'oldCurrency' in reportActionOriginalMessage && + 'amount' in reportActionOriginalMessage && + 'currency' in reportActionOriginalMessage; + + const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; + if (hasModifiedAmount) { + const oldCurrency = reportActionOriginalMessage?.oldCurrency ?? ''; + const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency); + + const currency = reportActionOriginalMessage?.currency ?? ''; + const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); + + // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. + // We check the merchant is in distance format (includes @) as a sanity check + if (hasModifiedMerchant && (reportActionOriginalMessage?.merchant ?? '').includes('@')) { + return getForDistanceRequest(reportActionOriginalMessage?.merchant ?? '', reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); + } + + buildMessageFragmentForValue(amount, oldAmount, Localize.translateLocal('iou.amount'), false, setFragments, removalFragments, changeFragments); + } + + const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; + if (hasModifiedComment) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', + Localize.translateLocal('common.description'), + true, + setFragments, + removalFragments, + changeFragments, + ); + } + + const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; + if (hasModifiedCreated) { + // Take only the YYYY-MM-DD value as the original date includes timestamp + let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ? reportActionOriginalMessage.oldCreated : 0); + formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); + buildMessageFragmentForValue( + reportActionOriginalMessage?.created ?? '', + formattedOldCreated, + Localize.translateLocal('common.date'), + false, + setFragments, + removalFragments, + changeFragments, + ); + } + + if (hasModifiedMerchant) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.merchant ?? '', + reportActionOriginalMessage?.oldMerchant ?? '', + Localize.translateLocal('common.merchant'), + true, + setFragments, + removalFragments, + changeFragments, + ); + } + + const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; + if (hasModifiedCategory) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.category ?? '', + reportActionOriginalMessage?.oldCategory ?? '', + Localize.translateLocal('common.category'), + true, + setFragments, + removalFragments, + changeFragments, + ); + } + + const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; + if (hasModifiedTag) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.tag ?? '', + reportActionOriginalMessage?.oldTag ?? '', + policyTagListName, + true, + setFragments, + removalFragments, + changeFragments, + policyTagListName === Localize.translateLocal('common.tag'), + ); + } + + const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; + if (hasModifiedBillable) { + buildMessageFragmentForValue( + reportActionOriginalMessage?.billable ?? '', + reportActionOriginalMessage?.oldBillable ?? '', + Localize.translateLocal('iou.request'), + true, + setFragments, + removalFragments, + changeFragments, + ); + } + + const message = + getMessageLine(`\n${Localize.translateLocal('iou.changed')}`, changeFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.set')}`, setFragments) + + getMessageLine(`\n${Localize.translateLocal('iou.removed')}`, removalFragments); + if (message === '') { + return Localize.translateLocal('iou.changedTheRequest'); + } + return `${message.substring(1, message.length)}`; +} + +export default { + getForReportAction, +}; diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 242248b17794..6bd6c73982eb 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -2,6 +2,7 @@ import Str from 'expensify-common/lib/str'; import {ImageSourcePropType} from 'react-native'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import * as ReportUtils from '@libs/ReportUtils'; import * as AppUpdate from '@userActions/AppUpdate'; import {Report, ReportAction} from '@src/types/onyx'; @@ -108,7 +109,7 @@ export default { pushModifiedExpenseNotification(report: Report, reportAction: ReportAction, onClick: LocalNotificationClickHandler, usesIcon = false) { const title = reportAction.person?.map((f) => f.text).join(', ') ?? ''; - const body = ReportUtils.getModifiedExpenseMessage(reportAction); + const body = ModifiedExpenseMessage.getForReportAction(reportAction); const icon = usesIcon ? EXPENSIFY_ICON_URL : ''; const data = { reportID: report.reportID, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index cc7ef66f7a43..5c7356920233 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -13,6 +13,7 @@ import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; +import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; @@ -407,7 +408,7 @@ function getLastMessageTextForReport(report) { } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`; } else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction); + const properSchemaForModifiedExpenseMessage = ModifiedExpenseMessage.getForReportAction(lastReportAction); lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if ( lastActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED || diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 24e795919649..d3dde542ec22 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1,4 +1,3 @@ -import {format} from 'date-fns'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import {isEmpty} from 'lodash'; @@ -16,7 +15,7 @@ import CONST from '@src/CONST'; import {ParentNavigationSummaryParams, TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; +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 {NotificationPreference} from '@src/types/onyx/Report'; @@ -402,21 +401,6 @@ Onyx.connect({ callback: (value) => (loginList = value), }); -let allPolicyTags: Record = {}; - -Onyx.connect({ - key: ONYXKEYS.COLLECTION.POLICY_TAGS, - waitForCollectionCallback: true, - callback: (value) => { - if (!value) { - allPolicyTags = {}; - return; - } - - allPolicyTags = value; - }, -}); - let allTransactions: OnyxCollection = {}; Onyx.connect({ @@ -430,10 +414,6 @@ Onyx.connect({ }, }); -function getPolicyTags(policyID: string) { - return allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {}; -} - function getChatType(report: OnyxEntry): ValueOf | undefined { return report?.chatType; } @@ -2036,140 +2016,6 @@ function getReportPreviewMessage( return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } -/** - * Get the proper message schema for modified expense message. - */ - -function getProperSchemaForModifiedExpenseMessage(newValue: string, oldValue: string, valueName: string, valueInQuotes: boolean, shouldConvertToLowercase = true): string { - const newValueToDisplay = valueInQuotes ? `"${newValue}"` : newValue; - const oldValueToDisplay = valueInQuotes ? `"${oldValue}"` : oldValue; - const displayValueName = shouldConvertToLowercase ? valueName.toLowerCase() : valueName; - - if (!oldValue) { - return Localize.translateLocal('iou.setTheRequest', {valueName: displayValueName, newValueToDisplay}); - } - if (!newValue) { - return Localize.translateLocal('iou.removedTheRequest', {valueName: displayValueName, oldValueToDisplay}); - } - return Localize.translateLocal('iou.updatedTheRequest', {valueName: displayValueName, newValueToDisplay, oldValueToDisplay}); -} - -/** - * Get the proper message schema for modified distance message. - */ -function getProperSchemaForModifiedDistanceMessage(newDistance: string, oldDistance: string, newAmount: string, oldAmount: string): string { - if (!oldDistance) { - return Localize.translateLocal('iou.setTheDistance', {newDistanceToDisplay: newDistance, newAmountToDisplay: newAmount}); - } - return Localize.translateLocal('iou.updatedTheDistance', { - newDistanceToDisplay: newDistance, - oldDistanceToDisplay: oldDistance, - newAmountToDisplay: newAmount, - oldAmountToDisplay: oldAmount, - }); -} - -/** - * Get the report action message when expense has been modified. - * - * ModifiedExpense::getNewDotComment in Web-Expensify should match this. - * If we change this function be sure to update the backend as well. - */ -function getModifiedExpenseMessage(reportAction: OnyxEntry): string | undefined { - const reportActionOriginalMessage = reportAction?.originalMessage as ExpenseOriginalMessage | undefined; - if (isEmptyObject(reportActionOriginalMessage)) { - return Localize.translateLocal('iou.changedTheRequest'); - } - const reportID = reportAction?.reportID ?? ''; - const policyID = getReport(reportID)?.policyID ?? ''; - const policyTags = getPolicyTags(policyID); - const policyTag = PolicyUtils.getTag(policyTags); - const policyTagListName = policyTag?.name ?? Localize.translateLocal('common.tag'); - - const hasModifiedAmount = - reportActionOriginalMessage && - 'oldAmount' in reportActionOriginalMessage && - 'oldCurrency' in reportActionOriginalMessage && - 'amount' in reportActionOriginalMessage && - 'currency' in reportActionOriginalMessage; - - const hasModifiedMerchant = reportActionOriginalMessage && 'oldMerchant' in reportActionOriginalMessage && 'merchant' in reportActionOriginalMessage; - if (hasModifiedAmount) { - const oldCurrency = reportActionOriginalMessage?.oldCurrency; - const oldAmount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.oldAmount ?? 0, oldCurrency ?? ''); - - const currency = reportActionOriginalMessage?.currency; - const amount = CurrencyUtils.convertToDisplayString(reportActionOriginalMessage?.amount ?? 0, currency); - - // Only Distance edits should modify amount and merchant (which stores distance) in a single transaction. - // We check the merchant is in distance format (includes @) as a sanity check - if (hasModifiedMerchant && reportActionOriginalMessage?.merchant?.includes('@')) { - return getProperSchemaForModifiedDistanceMessage(reportActionOriginalMessage?.merchant, reportActionOriginalMessage?.oldMerchant ?? '', amount, oldAmount); - } - - return getProperSchemaForModifiedExpenseMessage(amount, oldAmount, Localize.translateLocal('iou.amount'), false); - } - - const hasModifiedComment = reportActionOriginalMessage && 'oldComment' in reportActionOriginalMessage && 'newComment' in reportActionOriginalMessage; - if (hasModifiedComment) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.newComment ?? '', - reportActionOriginalMessage?.oldComment ?? '', - Localize.translateLocal('common.description'), - true, - ); - } - - const hasModifiedCreated = reportActionOriginalMessage && 'oldCreated' in reportActionOriginalMessage && 'created' in reportActionOriginalMessage; - if (hasModifiedCreated) { - // Take only the YYYY-MM-DD value as the original date includes timestamp - let formattedOldCreated: Date | string = new Date(reportActionOriginalMessage?.oldCreated ? reportActionOriginalMessage.oldCreated : 0); - formattedOldCreated = format(formattedOldCreated, CONST.DATE.FNS_FORMAT_STRING); - - return getProperSchemaForModifiedExpenseMessage(reportActionOriginalMessage?.created ?? '', formattedOldCreated?.toString?.(), Localize.translateLocal('common.date'), false); - } - - if (hasModifiedMerchant) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.merchant ?? '', - reportActionOriginalMessage?.oldMerchant ?? '', - Localize.translateLocal('common.merchant'), - true, - ); - } - - const hasModifiedCategory = reportActionOriginalMessage && 'oldCategory' in reportActionOriginalMessage && 'category' in reportActionOriginalMessage; - if (hasModifiedCategory) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.category ?? '', - reportActionOriginalMessage?.oldCategory ?? '', - Localize.translateLocal('common.category'), - true, - ); - } - - const hasModifiedTag = reportActionOriginalMessage && 'oldTag' in reportActionOriginalMessage && 'tag' in reportActionOriginalMessage; - if (hasModifiedTag) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage.tag ?? '', - reportActionOriginalMessage.oldTag ?? '', - policyTagListName, - true, - policyTagListName === Localize.translateLocal('common.tag'), - ); - } - - const hasModifiedBillable = reportActionOriginalMessage && 'oldBillable' in reportActionOriginalMessage && 'billable' in reportActionOriginalMessage; - if (hasModifiedBillable) { - return getProperSchemaForModifiedExpenseMessage( - reportActionOriginalMessage?.billable ?? '', - reportActionOriginalMessage?.oldBillable ?? '', - Localize.translateLocal('iou.request'), - true, - ); - } -} - /** * Given the updates user made to the request, compose the originalMessage * object of the modified expense action. @@ -3741,6 +3587,13 @@ function getReportIDFromLink(url: string | null): string { return reportID; } +/** + * Get the report policyID given a reportID + */ +function getReportPolicyID(reportID?: string): string | undefined { + return getReport(reportID)?.policyID; +} + /** * Check if the chat report is linked to an iou that is waiting for the current user to add a credit bank account. */ @@ -4373,6 +4226,7 @@ export { getReport, getReportNotificationPreference, getReportIDFromLink, + getReportPolicyID, getRouteFromLink, getDeletedParentActionMessageForChatReport, getLastVisibleMessage, @@ -4448,7 +4302,6 @@ export { getParentReport, getRootParentReport, getReportPreviewMessage, - getModifiedExpenseMessage, canUserPerformWriteAction, getOriginalReportID, canAccessReport, @@ -4492,4 +4345,4 @@ export { shouldAutoFocusOnKeyPress, }; -export type {OptionData, OptimisticChatReport}; +export type {ExpenseOriginalMessage, OptionData, OptimisticChatReport}; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 6f26ce0cba1c..ba006b1a9662 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -11,6 +11,7 @@ import EmailUtils from '@libs/EmailUtils'; import * as Environment from '@libs/Environment/Environment'; import fileDownload from '@libs/fileDownload'; import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails'; +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -281,7 +282,7 @@ export default [ const displayMessage = ReportUtils.getReportPreviewMessage(iouReport, reportAction); Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = ReportUtils.getModifiedExpenseMessage(reportAction); + const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction); Clipboard.setString(modifyExpenseMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index d4731d3b929b..cd454867a208 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -40,6 +40,7 @@ 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'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -427,7 +428,7 @@ function ReportActionItem(props) { children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { - children = ; + children = ; } else { const hasBeenFlagged = !_.contains([CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING], moderationDecision); children = ( diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 6123469aa813..19b3e75ca74c 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -193,7 +193,24 @@ type OriginalMessagePolicyTask = { type OriginalMessageModifiedExpense = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE; - originalMessage: unknown; + originalMessage: { + oldMerchant?: string; + merchant?: string; + oldCurrency?: string; + currency?: string; + oldAmount?: number; + amount?: number; + oldComment?: string; + newComment?: string; + oldCreated?: string; + created?: string; + oldCategory?: string; + category?: string; + oldTag?: string; + tag?: string; + oldBillable?: string; + billable?: string; + }; }; type OriginalMessageReimbursementQueued = { diff --git a/tests/perf-test/ModifiedExpenseMessage.perf-test.ts b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts new file mode 100644 index 000000000000..1997d55d8a05 --- /dev/null +++ b/tests/perf-test/ModifiedExpenseMessage.perf-test.ts @@ -0,0 +1,64 @@ +import {randAmount} from '@ngneat/falso'; +import Onyx from 'react-native-onyx'; +import {measureFunction} from 'reassure'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import {Policy, Report} from '@src/types/onyx'; +import ModifiedExpenseMessage from '../../src/libs/ModifiedExpenseMessage'; +import createCollection from '../utils/collections/createCollection'; +import createRandomPolicy from '../utils/collections/policies'; +import createRandomReportAction from '../utils/collections/reportActions'; +import createRandomReport from '../utils/collections/reports'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const runs = CONST.PERFORMANCE_TESTS.RUNS; + +beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }), +); + +// Clear out Onyx after each test so that each test starts with a clean state +afterEach(() => { + Onyx.clear(); +}); + +const getMockedReports = (length = 500) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (index) => createRandomReport(index), + length, + ); + +const getMockedPolicies = (length = 500) => + createCollection( + (item) => `${ONYXKEYS.COLLECTION.POLICY}${item.id}`, + (index) => createRandomPolicy(index), + length, + ); + +const mockedReportsMap = getMockedReports(5000) as Record<`${typeof ONYXKEYS.COLLECTION.REPORT}`, Report>; +const mockedPoliciesMap = getMockedPolicies(5000) as Record<`${typeof ONYXKEYS.COLLECTION.POLICY}`, Policy>; + +test('[ModifiedExpenseMessage] getForReportAction on 5k reports and policies', async () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + amount: randAmount(), + currency: CONST.CURRENCY.USD, + oldAmount: randAmount(), + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + await Onyx.multiSet({ + ...mockedPoliciesMap, + ...mockedReportsMap, + }); + + await waitForBatchedUpdates(); + await measureFunction(() => ModifiedExpenseMessage.getForReportAction(reportAction), {runs}); +}); diff --git a/tests/perf-test/ReportUtils.perf-test.ts b/tests/perf-test/ReportUtils.perf-test.ts index ab6ee72a0082..b931ae85a7da 100644 --- a/tests/perf-test/ReportUtils.perf-test.ts +++ b/tests/perf-test/ReportUtils.perf-test.ts @@ -1,4 +1,3 @@ -import {randAmount} from '@ngneat/falso'; import Onyx from 'react-native-onyx'; import {measureFunction} from 'reassure'; import * as ReportUtils from '@libs/ReportUtils'; @@ -133,29 +132,6 @@ test('[ReportUtils] getReportPreviewMessage on 5k policies', async () => { await measureFunction(() => ReportUtils.getReportPreviewMessage(report, reportAction, shouldConsiderReceiptBeingScanned, isPreviewMessageForParentChatReport, policy), {runs}); }); -test('[ReportUtils] getModifiedExpenseMessage on 5k reports and policies', async () => { - const reportAction = { - ...createRandomReportAction(1), - actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, - originalMessage: { - originalMessage: { - amount: randAmount(), - currency: CONST.CURRENCY.USD, - oldAmount: randAmount(), - oldCurrency: CONST.CURRENCY.USD, - }, - }, - }; - - await Onyx.multiSet({ - ...mockedPoliciesMap, - ...mockedReportsMap, - }); - - await waitForBatchedUpdates(); - await measureFunction(() => ReportUtils.getModifiedExpenseMessage(reportAction), {runs}); -}); - test('[ReportUtils] getReportName on 1k participants', async () => { const report = {...createRandomReport(1), chatType: undefined, participantAccountIDs}; const policy = createRandomPolicy(1); diff --git a/tests/unit/ModifiedExpenseMessageTest.ts b/tests/unit/ModifiedExpenseMessageTest.ts new file mode 100644 index 000000000000..02990aa5c751 --- /dev/null +++ b/tests/unit/ModifiedExpenseMessageTest.ts @@ -0,0 +1,279 @@ +import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; +import CONST from '@src/CONST'; +import createRandomReportAction from '../utils/collections/reportActions'; + +describe('ModifiedExpenseMessage', () => { + describe('getForAction', () => { + describe('when the amount is changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `changed the amount to $18.00 (previously $12.55).`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount is changed and the description is removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + newComment: '', + oldComment: 'this is for the shuttle', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = 'changed the amount to $18.00 (previously $12.55).\nremoved the description (previously "this is for the shuttle").'; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount is changed, the description is removed, and category is set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + newComment: '', + oldComment: 'this is for the shuttle', + category: 'Benefits', + oldCategory: '', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = 'changed the amount to $18.00 (previously $12.55).\nset the category to "Benefits".\nremoved the description (previously "this is for the shuttle").'; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount and merchant are changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: 'Taco Bell', + oldMerchant: 'Big Belly', + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + }, + }; + + it('returns the correct text message', () => { + const expectedResult = 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly").'; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount and merchant are changed, the description is removed, and category is set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: 'Taco Bell', + oldMerchant: 'Big Belly', + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + newComment: '', + oldComment: 'this is for the shuttle', + category: 'Benefits', + oldCategory: '', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = + 'changed the amount to $18.00 (previously $12.55) and the merchant to "Taco Bell" (previously "Big Belly").\nset the category to "Benefits".\nremoved the description (previously "this is for the shuttle").'; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the amount, comment and merchant are changed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: 'Taco Bell', + oldMerchant: 'Big Belly', + amount: 1800, + currency: CONST.CURRENCY.USD, + oldAmount: 1255, + oldCurrency: CONST.CURRENCY.USD, + newComment: 'I bought it on the way', + oldComment: 'from the business trip', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = + 'changed the amount to $18.00 (previously $12.55), the description to "I bought it on the way" (previously "from the business trip"), and the merchant to "Taco Bell" (previously "Big Belly").'; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant is removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: '', + oldMerchant: 'Big Belly', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `removed the merchant (previously "Big Belly").`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant and the description are removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: '', + oldMerchant: 'Big Belly', + newComment: '', + oldComment: 'minishore', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `removed the description (previously "minishore") and the merchant (previously "Big Belly").`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant, the category and the description are removed', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + merchant: '', + oldMerchant: 'Big Belly', + newComment: '', + oldComment: 'minishore', + category: '', + oldCategory: 'Benefits', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `removed the description (previously "minishore"), the merchant (previously "Big Belly"), and the category (previously "Benefits").`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant is set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + oldMerchant: '', + merchant: 'Big Belly', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `set the merchant to "Big Belly".`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant and the description are set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + oldMerchant: '', + merchant: 'Big Belly', + oldComment: '', + newComment: 'minishore', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `set the description to "minishore" and the merchant to "Big Belly".`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when the merchant, the category and the description are set', () => { + const reportAction = { + ...createRandomReportAction(1), + actionName: CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE, + originalMessage: { + oldMerchant: '', + merchant: 'Big Belly', + oldComment: '', + newComment: 'minishore', + oldCategory: '', + category: 'Benefits', + }, + }; + + it('returns the correct text message', () => { + const expectedResult = `set the description to "minishore", the merchant to "Big Belly", and the category to "Benefits".`; + + const result = ModifiedExpenseMessage.getForReportAction(reportAction); + + expect(result).toEqual(expectedResult); + }); + }); + }); +});