From 884a83f725f6e393bee935215302036eea940a7c Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 22 Sep 2023 13:03:59 +0200 Subject: [PATCH 1/6] [TS migration] Migrate 'TransactionUtils.js' lib --- src/libs/TransactionUtils.js | 412 ------------------------------ src/libs/TransactionUtils.ts | 358 ++++++++++++++++++++++++++ src/types/onyx/OriginalMessage.ts | 2 +- src/types/onyx/Transaction.ts | 22 +- 4 files changed, 375 insertions(+), 419 deletions(-) delete mode 100644 src/libs/TransactionUtils.js create mode 100644 src/libs/TransactionUtils.ts diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js deleted file mode 100644 index 5dcfbc467c20..000000000000 --- a/src/libs/TransactionUtils.js +++ /dev/null @@ -1,412 +0,0 @@ -import Onyx from 'react-native-onyx'; -import {format, parseISO, isValid} from 'date-fns'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; -import CONST from '../CONST'; -import ONYXKEYS from '../ONYXKEYS'; -import DateUtils from './DateUtils'; -import * as NumberUtils from './NumberUtils'; - -let allTransactions = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (val) => { - if (!val) { - return; - } - allTransactions = _.pick(val, (transaction) => transaction); - }, -}); - -/** - * Optimistically generate a transaction. - * - * @param {Number} amount – in cents - * @param {String} currency - * @param {String} reportID - * @param {String} [comment] - * @param {String} [created] - * @param {String} [source] - * @param {String} [originalTransactionID] - * @param {String} [merchant] - * @param {Object} [receipt] - * @param {String} [filename] - * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. - * @param {String} [category] - * @param {String} [tag] - * @param {Boolean} [billable] - * @returns {Object} - */ -function buildOptimisticTransaction( - amount, - currency, - reportID, - comment = '', - created = '', - source = '', - originalTransactionID = '', - merchant = '', - receipt = {}, - filename = '', - existingTransactionID = null, - category = '', - tag = '', - billable = false, -) { - // transactionIDs are random, positive, 64-bit numeric strings. - // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) - const transactionID = existingTransactionID || NumberUtils.rand64(); - - const commentJSON = {comment}; - if (source) { - commentJSON.source = source; - } - if (originalTransactionID) { - commentJSON.originalTransactionID = originalTransactionID; - } - - // For the SmartScan to run successfully, we need to pass the merchant field empty to the API - const defaultMerchant = _.isEmpty(receipt) ? CONST.TRANSACTION.DEFAULT_MERCHANT : ''; - - return { - transactionID, - amount, - currency, - reportID, - comment: commentJSON, - merchant: merchant || defaultMerchant, - created: created || DateUtils.getDBTime(), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - receipt, - filename, - category, - tag, - billable, - }; -} - -/** - * @param {Object|null} transaction - * @returns {Boolean} - */ -function hasReceipt(transaction) { - return lodashGet(transaction, 'receipt.state', '') !== ''; -} - -/** - * @param {Object} transaction - * @returns {Boolean} - */ -function areRequiredFieldsEmpty(transaction) { - return ( - transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || - transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || - (transaction.modifiedMerchant === '' && - (transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) || - (transaction.modifiedAmount === 0 && transaction.amount === 0) || - (transaction.modifiedCreated === '' && transaction.created === '') - ); -} - -/** - * Given the edit made to the money request, return an updated transaction object. - * - * @param {Object} transaction - * @param {Object} transactionChanges - * @param {Object} isFromExpenseReport - * @returns {Object} - */ -function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) { - // Only changing the first level fields so no need for deep clone now - const updatedTransaction = _.clone(transaction); - let shouldStopSmartscan = false; - - // The comment property does not have its modifiedComment counterpart - if (_.has(transactionChanges, 'comment')) { - updatedTransaction.comment = { - ...updatedTransaction.comment, - comment: transactionChanges.comment, - }; - } - if (_.has(transactionChanges, 'created')) { - updatedTransaction.modifiedCreated = transactionChanges.created; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'amount')) { - updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'currency')) { - updatedTransaction.modifiedCurrency = transactionChanges.currency; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'merchant')) { - updatedTransaction.modifiedMerchant = transactionChanges.merchant; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'category')) { - updatedTransaction.category = transactionChanges.category; - } - - if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) { - updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; - } - - updatedTransaction.pendingFields = { - ...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'amount') && {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - }; - - return updatedTransaction; -} - -/** - * Retrieve the particular transaction object given its ID. - * - * @param {String} transactionID - * @returns {Object} - * @deprecated Use withOnyx() or Onyx.connect() instead - */ -function getTransaction(transactionID) { - return lodashGet(allTransactions, `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {}); -} - -/** - * Return the comment field (referred to as description in the App) from the transaction. - * The comment does not have its modifiedComment counterpart. - * - * @param {Object} transaction - * @returns {String} - */ -function getDescription(transaction) { - return lodashGet(transaction, 'comment.comment', ''); -} - -/** - * Return the amount field from the transaction, return the modifiedAmount if present. - * - * @param {Object} transaction - * @param {Boolean} isFromExpenseReport - * @returns {Number} - */ -function getAmount(transaction, isFromExpenseReport) { - // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value - if (!isFromExpenseReport) { - const amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return Math.abs(amount); - } - return Math.abs(lodashGet(transaction, 'amount', 0)); - } - - // Expense report case: - // The amounts are stored using an opposite sign and negative values can be set, - // we need to return an opposite sign than is saved in the transaction object - let amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return -amount; - } - - // To avoid -0 being shown, lets only change the sign if the value is other than 0. - amount = lodashGet(transaction, 'amount', 0); - return amount ? -amount : 0; -} - -/** - * Return the currency field from the transaction, return the modifiedCurrency if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCurrency(transaction) { - const currency = lodashGet(transaction, 'modifiedCurrency', ''); - if (currency) { - return currency; - } - return lodashGet(transaction, 'currency', CONST.CURRENCY.USD); -} - -/** - * Return the merchant field from the transaction, return the modifiedMerchant if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getMerchant(transaction) { - return lodashGet(transaction, 'modifiedMerchant', null) || lodashGet(transaction, 'merchant', ''); -} - -/** - * Return the category from the transaction. This "category" field has no "modified" complement. - * - * @param {Object} transaction - * @return {String} - */ -function getCategory(transaction) { - return lodashGet(transaction, 'category', ''); -} - -/** - * Return the created field from the transaction, return the modifiedCreated if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCreated(transaction) { - const created = lodashGet(transaction, 'modifiedCreated', '') || lodashGet(transaction, 'created', ''); - const createdDate = parseISO(created); - if (isValid(createdDate)) { - return format(createdDate, CONST.DATE.FNS_FORMAT_STRING); - } - - return ''; -} - -/* - * @param {Object} transaction - * @param {Object} transaction.comment - * @param {String} transaction.comment.type - * @param {Object} [transaction.comment.customUnit] - * @param {String} [transaction.comment.customUnit.name] - * @returns {Boolean} - */ -function isDistanceRequest(transaction) { - const type = lodashGet(transaction, 'comment.type'); - const customUnitName = lodashGet(transaction, 'comment.customUnit.name'); - return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; -} - -function isReceiptBeingScanned(transaction) { - return _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], transaction.receipt.state); -} - -/** - * Check if the transaction has a non-smartscanning receipt and is missing required fields - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasMissingSmartscanFields(transaction) { - return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); -} - -/** - * Check if the transaction has a defined route - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasRoute(transaction) { - return !!lodashGet(transaction, 'routes.route0.geometry.coordinates'); -} - -/** - * Get the transactions related to a report preview with receipts - * Get the details linked to the IOU reportAction - * - * @param {Object} reportAction - * @returns {Object} - * @deprecated Use Onyx.connect() or withOnyx() instead - */ -function getLinkedTransaction(reportAction = {}) { - const transactionID = lodashGet(reportAction, ['originalMessage', 'IOUTransactionID'], ''); - return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; -} - -function getAllReportTransactions(reportID) { - // `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`. - // For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277. - // We will update this in a follow-up Issue. According to this comment: https://github.com/Expensify/App/pull/26536#issuecomment-1703591019. - return _.filter(allTransactions, (transaction) => `${transaction.reportID}` === `${reportID}`); -} - -/** - * Checks if a waypoint has a valid address - * @param {Object} waypoint - * @returns {Boolean} Returns true if the address is valid - */ -function waypointHasValidAddress(waypoint) { - if (!waypoint || !waypoint.address || typeof waypoint.address !== 'string' || waypoint.address.trim() === '') { - return false; - } - return true; -} - -/** - * Converts the key of a waypoint to its index - * @param {String} key - * @returns {Number} waypoint index - */ -function getWaypointIndex(key) { - return Number(key.replace('waypoint', '')); -} - -/** - * Filters the waypoints which are valid and returns those - * @param {Object} waypoints - * @param {Boolean} reArrangeIndexes - * @returns {Object} validated waypoints - */ -function getValidWaypoints(waypoints, reArrangeIndexes = false) { - const sortedIndexes = _.map(_.keys(waypoints), (key) => getWaypointIndex(key)).sort(); - const waypointValues = _.map(sortedIndexes, (index) => waypoints[`waypoint${index}`]); - // Ensure the number of waypoints is between 2 and 25 - if (waypointValues.length < 2 || waypointValues.length > 25) { - return {}; - } - - let lastWaypointIndex = -1; - - const validWaypoints = _.reduce( - waypointValues, - (acc, currentWaypoint, index) => { - const previousWaypoint = waypointValues[lastWaypointIndex]; - // Check if the waypoint has a valid address - if (!waypointHasValidAddress(currentWaypoint)) { - return acc; - } - - // Check for adjacent waypoints with the same address - if (previousWaypoint && currentWaypoint.address === previousWaypoint.address) { - return acc; - } - - const validatedWaypoints = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; - - lastWaypointIndex += 1; - - return validatedWaypoints; - }, - {}, - ); - return validWaypoints; -} - -export { - buildOptimisticTransaction, - getUpdatedTransaction, - getTransaction, - getDescription, - getAmount, - getCurrency, - getMerchant, - getCreated, - getCategory, - getLinkedTransaction, - getAllReportTransactions, - hasReceipt, - hasRoute, - isReceiptBeingScanned, - getValidWaypoints, - isDistanceRequest, - hasMissingSmartscanFields, - getWaypointIndex, - waypointHasValidAddress, -}; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts new file mode 100644 index 000000000000..f821aada2a5c --- /dev/null +++ b/src/libs/TransactionUtils.ts @@ -0,0 +1,358 @@ +import Onyx, {OnyxCollection} from 'react-native-onyx'; +import {format, parseISO, isValid} from 'date-fns'; +import CONST from '../CONST'; +import ONYXKEYS from '../ONYXKEYS'; +import DateUtils from './DateUtils'; +import * as NumberUtils from './NumberUtils'; +import {RecentWaypoints, ReportAction, Transaction} from '../types/onyx'; +import {Receipt, Comment} from '../types/onyx/Transaction'; +import {OriginalMessageIOU} from '../types/onyx/OriginalMessage'; + +type TransactionChanges = Partial & {comment?: string}; + +type OriginalMessage = OriginalMessageIOU['originalMessage']; + +type Waypoints = Record; + +let allTransactions: OnyxCollection = {}; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.TRANSACTION, + waitForCollectionCallback: true, + callback: (val) => { + if (!val) { + return; + } + allTransactions = {...val}; + }, +}); + +/** + * Optimistically generate a transaction. + * + * @param amount – in cents + * @param currency + * @param reportID + * @param [comment] + * @param [created] + * @param [source] + * @param [originalTransactionID] + * @param [merchant] + * @param [receipt] + * @param [filename] + * @param [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. + * @param [category] + * @param [tag] + * @param [billable] + */ +function buildOptimisticTransaction( + amount: number, + currency: string, + reportID: string, + comment = '', + created = '', + source = '', + originalTransactionID = '', + merchant = '', + receipt: Receipt = {}, + filename = '', + existingTransactionID: string | null = null, + category = '', + tag = '', + billable = false, +): Transaction { + // transactionIDs are random, positive, 64-bit numeric strings. + // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) + const transactionID = existingTransactionID ?? NumberUtils.rand64(); + + const commentJSON: Comment = {comment}; + if (source) { + commentJSON.source = source; + } + if (originalTransactionID) { + commentJSON.originalTransactionID = originalTransactionID; + } + + // For the SmartScan to run successfully, we need to pass the merchant field empty to the API + const defaultMerchant = !receipt || Object.keys(receipt).length === 0 ? CONST.TRANSACTION.DEFAULT_MERCHANT : ''; + + return { + transactionID, + amount, + currency, + reportID, + comment: commentJSON, + merchant: merchant || defaultMerchant, + created: created || DateUtils.getDBTime(), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + receipt, + filename, + category, + tag, + billable, + }; +} + +function hasReceipt(transaction: Transaction | null): boolean { + return !!transaction?.receipt?.state; +} + +function areRequiredFieldsEmpty(transaction: Transaction): boolean { + return ( + transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || + transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || + (transaction.modifiedMerchant === '' && + (transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) || + (transaction.modifiedAmount === 0 && transaction.amount === 0) || + (transaction.modifiedCreated === '' && transaction.created === '') + ); +} + +/** + * Given the edit made to the money request, return an updated transaction object. + */ +function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean): Transaction { + // Only changing the first level fields so no need for deep clone now + const updatedTransaction = {...transaction}; + let shouldStopSmartscan = false; + + // The comment property does not have its modifiedComment counterpart + if (Object.hasOwn(transactionChanges, 'comment')) { + updatedTransaction.comment = { + ...updatedTransaction.comment, + comment: transactionChanges.comment, + }; + } + if (Object.hasOwn(transactionChanges, 'created')) { + updatedTransaction.modifiedCreated = transactionChanges.created; + shouldStopSmartscan = true; + } + if (Object.hasOwn(transactionChanges, 'amount') && typeof transactionChanges.amount === 'number') { + updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount; + shouldStopSmartscan = true; + } + if (Object.hasOwn(transactionChanges, 'currency')) { + updatedTransaction.modifiedCurrency = transactionChanges.currency; + shouldStopSmartscan = true; + } + + if (Object.hasOwn(transactionChanges, 'merchant')) { + updatedTransaction.modifiedMerchant = transactionChanges.merchant; + shouldStopSmartscan = true; + } + + if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { + updatedTransaction.category = transactionChanges.category; + } + + if (shouldStopSmartscan && transaction?.receipt && Object.keys(transaction.receipt).length > 0 && transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN) { + updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; + } + + updatedTransaction.pendingFields = { + ...(Object.hasOwn(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'amount') && {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + }; + + return updatedTransaction; +} + +/** + * Retrieve the particular transaction object given its ID. + * + * @deprecated Use withOnyx() or Onyx.connect() instead + */ +function getTransaction(transactionID: string): Transaction | Record { + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; +} + +/** + * Return the comment field (referred to as description in the App) from the transaction. + * The comment does not have its modifiedComment counterpart. + */ +function getDescription(transaction: Transaction): string { + return transaction?.comment?.comment ?? ''; +} + +/** + * Return the amount field from the transaction, return the modifiedAmount if present. + */ +function getAmount(transaction: Transaction, isFromExpenseReport: boolean): number { + // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value + if (!isFromExpenseReport) { + const amount = transaction?.modifiedAmount ?? 0; + if (amount) { + return Math.abs(amount); + } + return Math.abs(transaction?.amount ?? 0); + } + + // Expense report case: + // The amounts are stored using an opposite sign and negative values can be set, + // we need to return an opposite sign than is saved in the transaction object + let amount = transaction?.modifiedAmount ?? 0; + if (amount) { + return -amount; + } + + // To avoid -0 being shown, lets only change the sign if the value is other than 0. + amount = transaction?.amount ?? 0; + return amount ? -amount : 0; +} + +/** + * Return the currency field from the transaction, return the modifiedCurrency if present. + */ +function getCurrency(transaction: Transaction): string { + const currency = transaction?.modifiedCurrency ?? ''; + if (currency) { + return currency; + } + return transaction?.currency ?? CONST.CURRENCY.USD; +} + +/** + * Return the merchant field from the transaction, return the modifiedMerchant if present. + */ +function getMerchant(transaction: Transaction): string { + return transaction?.modifiedMerchant ?? transaction?.merchant ?? ''; +} + +/** + * Return the category from the transaction. This "category" field has no "modified" complement. + */ +function getCategory(transaction: Transaction): string { + return transaction?.category ?? ''; +} + +/** + * Return the created field from the transaction, return the modifiedCreated if present. + */ +function getCreated(transaction: Transaction): string { + const created = transaction?.modifiedCreated ?? transaction?.created ?? ''; + const createdDate = parseISO(created); + if (isValid(createdDate)) { + return format(createdDate, CONST.DATE.FNS_FORMAT_STRING); + } + + return ''; +} + +function isDistanceRequest(transaction: Transaction): boolean { + const type = transaction?.comment?.type; + const customUnitName = transaction?.comment?.customUnit?.name; + return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; +} + +function isReceiptBeingScanned(transaction: Transaction): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction.receipt.state); +} + +/** + * Check if the transaction has a non-smartscanning receipt and is missing required fields + */ +function hasMissingSmartscanFields(transaction: Transaction): boolean { + return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); +} + +/** + * Check if the transaction has a defined route + */ +function hasRoute(transaction: Transaction): boolean { + return !!transaction?.routes?.route0?.geometry?.coordinates; +} + +/** + * Get the transactions related to a report preview with receipts + * Get the details linked to the IOU reportAction + * + * @deprecated Use Onyx.connect() or withOnyx() instead + */ +function getLinkedTransaction(reportAction: ReportAction): Transaction | Record { + const transactionID = (reportAction?.originalMessage as OriginalMessage)?.IOUTransactionID ?? ''; + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; +} + +function getAllReportTransactions(reportID?: string): Transaction[] { + // `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`. + // For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277. + // We will update this in a follow-up Issue. According to this comment: https://github.com/Expensify/App/pull/26536#issuecomment-1703591019. + const transactions: Transaction[] = Object.values(allTransactions ?? {}).filter((transaction): transaction is Transaction => transaction !== null); + return transactions.filter((transaction) => `${transaction.reportID}` === `${reportID}`); +} + +/** + * Checks if a waypoint has a valid address + */ +function waypointHasValidAddress(waypoint: RecentWaypoints): boolean { + return !!waypoint?.address?.trim(); +} + +/** + * Converts the key of a waypoint to its index + */ +function getWaypointIndex(key: string): number { + return Number(key.replace('waypoint', '')); +} + +/** + * Filters the waypoints which are valid and returns those + */ +function getValidWaypoints(waypoints: Waypoints, reArrangeIndexes = false): Waypoints { + const sortedIndexes = Object.keys(waypoints) + .map((key) => getWaypointIndex(key)) + .sort(); + const waypointValues = sortedIndexes.map((index) => waypoints[`waypoint${index}`]); + // Ensure the number of waypoints is between 2 and 25 + if (waypointValues.length < 2 || waypointValues.length > 25) { + return {}; + } + + let lastWaypointIndex = -1; + + return waypointValues.reduce((acc, currentWaypoint, index) => { + const previousWaypoint = waypointValues[lastWaypointIndex]; + + // Check if the waypoint has a valid address + if (!waypointHasValidAddress(currentWaypoint)) { + return acc; + } + + // Check for adjacent waypoints with the same address + if (previousWaypoint && currentWaypoint.address === previousWaypoint.address) { + return acc; + } + + const validatedWaypoints: Waypoints = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; + + lastWaypointIndex += 1; + + return validatedWaypoints; + }, {}); +} + +export { + buildOptimisticTransaction, + getUpdatedTransaction, + getTransaction, + getDescription, + getAmount, + getCurrency, + getMerchant, + getCreated, + getCategory, + getLinkedTransaction, + getAllReportTransactions, + hasReceipt, + hasRoute, + isReceiptBeingScanned, + getValidWaypoints, + isDistanceRequest, + hasMissingSmartscanFields, + getWaypointIndex, + waypointHasValidAddress, +}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 8ed25cb286b0..5c6c60395569 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -137,4 +137,4 @@ type OriginalMessage = | OriginalMessagePolicyTask; export default OriginalMessage; -export type {Reaction}; +export type {Reaction, OriginalMessageIOU}; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index ea0b178444b5..f58a76aa2e0b 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -4,6 +4,10 @@ import CONST from '../../CONST'; type Comment = { comment?: string; + type?: string; + customUnit?: Record; + source?: string; + originalTransactionID?: string; }; type Geometry = { @@ -11,6 +15,12 @@ type Geometry = { type: 'LineString'; }; +type Receipt = { + receiptID?: number; + source?: string; + state?: ValueOf; +}; + type Route = { distance: number; geometry: Geometry; @@ -24,23 +34,23 @@ type Transaction = { comment: Comment; created: string; currency: string; - errors: OnyxCommon.Errors; + errors?: OnyxCommon.Errors; // The name of the file used for a receipt (formerly receiptFilename) filename?: string; merchant: string; modifiedAmount?: number; modifiedCreated?: string; modifiedCurrency?: string; + modifiedMerchant?: string; pendingAction: OnyxCommon.PendingAction; - receipt: { - receiptID?: number; - source?: string; - state?: ValueOf; - }; + receipt: Receipt; reportID: string; + billable?: boolean; routes?: Routes; transactionID: string; tag: string; + pendingFields?: Partial<{[K in keyof Transaction]: 'update'}>; }; export default Transaction; +export type {Comment, Receipt}; From c94f2ab2a1cca1ee75ed2eaae662df92c30a73b3 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Fri, 22 Sep 2023 18:46:07 +0200 Subject: [PATCH 2/6] Add falsy transactions filtering, code improvements --- src/libs/TransactionUtils.ts | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f821aada2a5c..8fa34b854c2b 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -19,11 +19,11 @@ let allTransactions: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, waitForCollectionCallback: true, - callback: (val) => { - if (!val) { + callback: (value) => { + if (!value) { return; } - allTransactions = {...val}; + allTransactions = Object.fromEntries(Object.entries({...value}).filter(([, transaction]) => !!transaction)); }, }); @@ -31,19 +31,7 @@ Onyx.connect({ * Optimistically generate a transaction. * * @param amount – in cents - * @param currency - * @param reportID - * @param [comment] - * @param [created] - * @param [source] - * @param [originalTransactionID] - * @param [merchant] - * @param [receipt] - * @param [filename] * @param [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. - * @param [category] - * @param [tag] - * @param [billable] */ function buildOptimisticTransaction( amount: number, @@ -93,7 +81,7 @@ function buildOptimisticTransaction( }; } -function hasReceipt(transaction: Transaction | null): boolean { +function hasReceipt(transaction: Transaction | undefined | null): boolean { return !!transaction?.receipt?.state; } @@ -303,9 +291,7 @@ function getWaypointIndex(key: string): number { * Filters the waypoints which are valid and returns those */ function getValidWaypoints(waypoints: Waypoints, reArrangeIndexes = false): Waypoints { - const sortedIndexes = Object.keys(waypoints) - .map((key) => getWaypointIndex(key)) - .sort(); + const sortedIndexes = Object.keys(waypoints).map(getWaypointIndex).sort(); const waypointValues = sortedIndexes.map((index) => waypoints[`waypoint${index}`]); // Ensure the number of waypoints is between 2 and 25 if (waypointValues.length < 2 || waypointValues.length > 25) { From 50640976ca743a7fdbcd288c0618d2d9d54d2360 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 25 Sep 2023 13:58:53 +0200 Subject: [PATCH 3/6] Resolve conflicts --- src/libs/TransactionUtils.js | 428 -------------------------------- src/libs/TransactionUtils.ts | 27 +- src/libs/actions/Transaction.ts | 2 +- 3 files changed, 20 insertions(+), 437 deletions(-) delete mode 100644 src/libs/TransactionUtils.js diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js deleted file mode 100644 index 58fb23a8811a..000000000000 --- a/src/libs/TransactionUtils.js +++ /dev/null @@ -1,428 +0,0 @@ -import Onyx from 'react-native-onyx'; -import {format, parseISO, isValid} from 'date-fns'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; -import CONST from '../CONST'; -import ONYXKEYS from '../ONYXKEYS'; -import DateUtils from './DateUtils'; -import * as NumberUtils from './NumberUtils'; - -let allTransactions = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (val) => { - if (!val) { - return; - } - allTransactions = _.pick(val, (transaction) => transaction); - }, -}); - -/** - * Optimistically generate a transaction. - * - * @param {Number} amount – in cents - * @param {String} currency - * @param {String} reportID - * @param {String} [comment] - * @param {String} [created] - * @param {String} [source] - * @param {String} [originalTransactionID] - * @param {String} [merchant] - * @param {Object} [receipt] - * @param {String} [filename] - * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. - * @param {String} [category] - * @param {String} [tag] - * @param {Boolean} [billable] - * @returns {Object} - */ -function buildOptimisticTransaction( - amount, - currency, - reportID, - comment = '', - created = '', - source = '', - originalTransactionID = '', - merchant = '', - receipt = {}, - filename = '', - existingTransactionID = null, - category = '', - tag = '', - billable = false, -) { - // transactionIDs are random, positive, 64-bit numeric strings. - // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) - const transactionID = existingTransactionID || NumberUtils.rand64(); - - const commentJSON = {comment}; - if (source) { - commentJSON.source = source; - } - if (originalTransactionID) { - commentJSON.originalTransactionID = originalTransactionID; - } - - // For the SmartScan to run successfully, we need to pass the merchant field empty to the API - const defaultMerchant = _.isEmpty(receipt) ? CONST.TRANSACTION.DEFAULT_MERCHANT : ''; - - return { - transactionID, - amount, - currency, - reportID, - comment: commentJSON, - merchant: merchant || defaultMerchant, - created: created || DateUtils.getDBTime(), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - receipt, - filename, - category, - tag, - billable, - }; -} - -/** - * @param {Object|null} transaction - * @returns {Boolean} - */ -function hasReceipt(transaction) { - return lodashGet(transaction, 'receipt.state', '') !== ''; -} - -/** - * @param {Object} transaction - * @returns {Boolean} - */ -function areRequiredFieldsEmpty(transaction) { - return ( - transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || - transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || - (transaction.modifiedMerchant === '' && - (transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) || - (transaction.modifiedAmount === 0 && transaction.amount === 0) || - (transaction.modifiedCreated === '' && transaction.created === '') - ); -} - -/** - * Given the edit made to the money request, return an updated transaction object. - * - * @param {Object} transaction - * @param {Object} transactionChanges - * @param {Object} isFromExpenseReport - * @returns {Object} - */ -function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) { - // Only changing the first level fields so no need for deep clone now - const updatedTransaction = _.clone(transaction); - let shouldStopSmartscan = false; - - // The comment property does not have its modifiedComment counterpart - if (_.has(transactionChanges, 'comment')) { - updatedTransaction.comment = { - ...updatedTransaction.comment, - comment: transactionChanges.comment, - }; - } - if (_.has(transactionChanges, 'created')) { - updatedTransaction.modifiedCreated = transactionChanges.created; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'amount')) { - updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'currency')) { - updatedTransaction.modifiedCurrency = transactionChanges.currency; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'merchant')) { - updatedTransaction.modifiedMerchant = transactionChanges.merchant; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'category')) { - updatedTransaction.category = transactionChanges.category; - } - - if (_.has(transactionChanges, 'tag')) { - updatedTransaction.tag = transactionChanges.tag; - } - - if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) { - updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; - } - - updatedTransaction.pendingFields = { - ...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'amount') && {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - }; - - return updatedTransaction; -} - -/** - * Retrieve the particular transaction object given its ID. - * - * @param {String} transactionID - * @returns {Object} - * @deprecated Use withOnyx() or Onyx.connect() instead - */ -function getTransaction(transactionID) { - return lodashGet(allTransactions, `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {}); -} - -/** - * Return the comment field (referred to as description in the App) from the transaction. - * The comment does not have its modifiedComment counterpart. - * - * @param {Object} transaction - * @returns {String} - */ -function getDescription(transaction) { - return lodashGet(transaction, 'comment.comment', ''); -} - -/** - * Return the amount field from the transaction, return the modifiedAmount if present. - * - * @param {Object} transaction - * @param {Boolean} isFromExpenseReport - * @returns {Number} - */ -function getAmount(transaction, isFromExpenseReport) { - // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value - if (!isFromExpenseReport) { - const amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return Math.abs(amount); - } - return Math.abs(lodashGet(transaction, 'amount', 0)); - } - - // Expense report case: - // The amounts are stored using an opposite sign and negative values can be set, - // we need to return an opposite sign than is saved in the transaction object - let amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return -amount; - } - - // To avoid -0 being shown, lets only change the sign if the value is other than 0. - amount = lodashGet(transaction, 'amount', 0); - return amount ? -amount : 0; -} - -/** - * Return the currency field from the transaction, return the modifiedCurrency if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCurrency(transaction) { - const currency = lodashGet(transaction, 'modifiedCurrency', ''); - if (currency) { - return currency; - } - return lodashGet(transaction, 'currency', CONST.CURRENCY.USD); -} - -/** - * Return the merchant field from the transaction, return the modifiedMerchant if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getMerchant(transaction) { - return lodashGet(transaction, 'modifiedMerchant', null) || lodashGet(transaction, 'merchant', ''); -} - -/** - * Return the category from the transaction. This "category" field has no "modified" complement. - * - * @param {Object} transaction - * @return {String} - */ -function getCategory(transaction) { - return lodashGet(transaction, 'category', ''); -} - -/** - * Return the tag from the transaction. This "tag" field has no "modified" complement. - * - * @param {Object} transaction - * @return {String} - */ -function getTag(transaction) { - return lodashGet(transaction, 'tag', ''); -} - -/** - * Return the created field from the transaction, return the modifiedCreated if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCreated(transaction) { - const created = lodashGet(transaction, 'modifiedCreated', '') || lodashGet(transaction, 'created', ''); - const createdDate = parseISO(created); - if (isValid(createdDate)) { - return format(createdDate, CONST.DATE.FNS_FORMAT_STRING); - } - - return ''; -} - -/* - * @param {Object} transaction - * @param {Object} transaction.comment - * @param {String} transaction.comment.type - * @param {Object} [transaction.comment.customUnit] - * @param {String} [transaction.comment.customUnit.name] - * @returns {Boolean} - */ -function isDistanceRequest(transaction) { - const type = lodashGet(transaction, 'comment.type'); - const customUnitName = lodashGet(transaction, 'comment.customUnit.name'); - return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; -} - -function isReceiptBeingScanned(transaction) { - return _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], transaction.receipt.state); -} - -/** - * Check if the transaction has a non-smartscanning receipt and is missing required fields - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasMissingSmartscanFields(transaction) { - return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); -} - -/** - * Check if the transaction has a defined route - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasRoute(transaction) { - return !!lodashGet(transaction, 'routes.route0.geometry.coordinates'); -} - -/** - * Get the transactions related to a report preview with receipts - * Get the details linked to the IOU reportAction - * - * @param {Object} reportAction - * @returns {Object} - * @deprecated Use Onyx.connect() or withOnyx() instead - */ -function getLinkedTransaction(reportAction = {}) { - const transactionID = lodashGet(reportAction, ['originalMessage', 'IOUTransactionID'], ''); - return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; -} - -function getAllReportTransactions(reportID) { - // `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`. - // For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277. - // We will update this in a follow-up Issue. According to this comment: https://github.com/Expensify/App/pull/26536#issuecomment-1703591019. - return _.filter(allTransactions, (transaction) => `${transaction.reportID}` === `${reportID}`); -} - -/** - * Checks if a waypoint has a valid address - * @param {Object} waypoint - * @returns {Boolean} Returns true if the address is valid - */ -function waypointHasValidAddress(waypoint) { - if (!waypoint || !waypoint.address || typeof waypoint.address !== 'string' || waypoint.address.trim() === '') { - return false; - } - return true; -} - -/** - * Converts the key of a waypoint to its index - * @param {String} key - * @returns {Number} waypoint index - */ -function getWaypointIndex(key) { - return Number(key.replace('waypoint', '')); -} - -/** - * Filters the waypoints which are valid and returns those - * @param {Object} waypoints - * @param {Boolean} reArrangeIndexes - * @returns {Object} validated waypoints - */ -function getValidWaypoints(waypoints, reArrangeIndexes = false) { - const sortedIndexes = _.map(_.keys(waypoints), (key) => getWaypointIndex(key)).sort(); - const waypointValues = _.map(sortedIndexes, (index) => waypoints[`waypoint${index}`]); - // Ensure the number of waypoints is between 2 and 25 - if (waypointValues.length < 2 || waypointValues.length > 25) { - return {}; - } - - let lastWaypointIndex = -1; - - const validWaypoints = _.reduce( - waypointValues, - (acc, currentWaypoint, index) => { - const previousWaypoint = waypointValues[lastWaypointIndex]; - // Check if the waypoint has a valid address - if (!waypointHasValidAddress(currentWaypoint)) { - return acc; - } - - // Check for adjacent waypoints with the same address - if (previousWaypoint && currentWaypoint.address === previousWaypoint.address) { - return acc; - } - - const validatedWaypoints = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; - - lastWaypointIndex += 1; - - return validatedWaypoints; - }, - {}, - ); - return validWaypoints; -} - -export { - buildOptimisticTransaction, - getUpdatedTransaction, - getTransaction, - getDescription, - getAmount, - getCurrency, - getMerchant, - getCreated, - getCategory, - getTag, - getLinkedTransaction, - getAllReportTransactions, - hasReceipt, - hasRoute, - isReceiptBeingScanned, - getValidWaypoints, - isDistanceRequest, - hasMissingSmartscanFields, - getWaypointIndex, - waypointHasValidAddress, -}; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 8fa34b854c2b..86b868baf24f 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -4,16 +4,14 @@ import CONST from '../CONST'; import ONYXKEYS from '../ONYXKEYS'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; -import {RecentWaypoints, ReportAction, Transaction} from '../types/onyx'; -import {Receipt, Comment} from '../types/onyx/Transaction'; +import {RecentWaypoint, ReportAction, Transaction} from '../types/onyx'; +import {Receipt, Comment, WaypointCollection} from '../types/onyx/Transaction'; import {OriginalMessageIOU} from '../types/onyx/OriginalMessage'; type TransactionChanges = Partial & {comment?: string}; type OriginalMessage = OriginalMessageIOU['originalMessage']; -type Waypoints = Record; - let allTransactions: OnyxCollection = {}; Onyx.connect({ @@ -133,6 +131,10 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra updatedTransaction.category = transactionChanges.category; } + if (Object.hasOwn(transactionChanges, 'tag') && typeof transactionChanges.tag === 'string') { + updatedTransaction.tag = transactionChanges.tag; + } + if (shouldStopSmartscan && transaction?.receipt && Object.keys(transaction.receipt).length > 0 && transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN) { updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; } @@ -144,6 +146,7 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra ...(Object.hasOwn(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }; return updatedTransaction; @@ -217,6 +220,13 @@ function getCategory(transaction: Transaction): string { return transaction?.category ?? ''; } +/** + * Return the tag from the transaction. This "tag" field has no "modified" complement. + */ +function getTag(transaction: Transaction): string { + return transaction?.tag ?? ''; +} + /** * Return the created field from the transaction, return the modifiedCreated if present. */ @@ -276,7 +286,7 @@ function getAllReportTransactions(reportID?: string): Transaction[] { /** * Checks if a waypoint has a valid address */ -function waypointHasValidAddress(waypoint: RecentWaypoints): boolean { +function waypointHasValidAddress(waypoint: RecentWaypoint | null): boolean { return !!waypoint?.address?.trim(); } @@ -290,7 +300,7 @@ function getWaypointIndex(key: string): number { /** * Filters the waypoints which are valid and returns those */ -function getValidWaypoints(waypoints: Waypoints, reArrangeIndexes = false): Waypoints { +function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = false): WaypointCollection { const sortedIndexes = Object.keys(waypoints).map(getWaypointIndex).sort(); const waypointValues = sortedIndexes.map((index) => waypoints[`waypoint${index}`]); // Ensure the number of waypoints is between 2 and 25 @@ -309,11 +319,11 @@ function getValidWaypoints(waypoints: Waypoints, reArrangeIndexes = false): Wayp } // Check for adjacent waypoints with the same address - if (previousWaypoint && currentWaypoint.address === previousWaypoint.address) { + if (previousWaypoint && currentWaypoint?.address === previousWaypoint.address) { return acc; } - const validatedWaypoints: Waypoints = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; + const validatedWaypoints: WaypointCollection = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; lastWaypointIndex += 1; @@ -331,6 +341,7 @@ export { getMerchant, getCreated, getCategory, + getTag, getLinkedTransaction, getAllReportTransactions, hasReceipt, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index cc26dccc25b6..05d0cf75af5e 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -107,7 +107,7 @@ function removeWaypoint(transactionID: string, currentIndex: string) { const waypointValues = Object.values(existingWaypoints); const removed = waypointValues.splice(index, 1); - const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {}); + const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? null); const reIndexedWaypoints: WaypointCollection = {}; waypointValues.forEach((waypoint, idx) => { From 4e27e2927edbfdd5fe93edeb15da89ffeae7a4d4 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 25 Sep 2023 15:25:32 +0200 Subject: [PATCH 4/6] Update types --- src/libs/TransactionUtils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 86b868baf24f..c75a814ffdc4 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -6,11 +6,10 @@ import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; import {RecentWaypoint, ReportAction, Transaction} from '../types/onyx'; import {Receipt, Comment, WaypointCollection} from '../types/onyx/Transaction'; -import {OriginalMessageIOU} from '../types/onyx/OriginalMessage'; -type TransactionChanges = Partial & {comment?: string}; +type AdditionalTransactionChanges = {comment?: string}; -type OriginalMessage = OriginalMessageIOU['originalMessage']; +type TransactionChanges = Partial & AdditionalTransactionChanges; let allTransactions: OnyxCollection = {}; @@ -271,7 +270,12 @@ function hasRoute(transaction: Transaction): boolean { * @deprecated Use Onyx.connect() or withOnyx() instead */ function getLinkedTransaction(reportAction: ReportAction): Transaction | Record { - const transactionID = (reportAction?.originalMessage as OriginalMessage)?.IOUTransactionID ?? ''; + let transactionID = ''; + + if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + transactionID = reportAction.originalMessage?.IOUTransactionID ?? ''; + } + return allTransactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? {}; } From 8e776e67fc5fc8708aace5a71a814ee2bd89cd09 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Mon, 25 Sep 2023 16:30:02 +0200 Subject: [PATCH 5/6] Code improvement --- src/libs/TransactionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c75a814ffdc4..3429a25a9268 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -20,7 +20,7 @@ Onyx.connect({ if (!value) { return; } - allTransactions = Object.fromEntries(Object.entries({...value}).filter(([, transaction]) => !!transaction)); + allTransactions = Object.fromEntries(Object.entries(value).filter(([, transaction]) => !!transaction)); }, }); From 15aed9b78d441cd4f3a4b7c7d14c13ddca4e649e Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 28 Sep 2023 16:34:15 +0200 Subject: [PATCH 6/6] Resolve conflicts --- src/libs/TransactionUtils.js | 461 ---------------------------------- src/libs/TransactionUtils.ts | 29 ++- src/types/onyx/Transaction.ts | 6 +- 3 files changed, 30 insertions(+), 466 deletions(-) delete mode 100644 src/libs/TransactionUtils.js diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js deleted file mode 100644 index aff1068546d1..000000000000 --- a/src/libs/TransactionUtils.js +++ /dev/null @@ -1,461 +0,0 @@ -import Onyx from 'react-native-onyx'; -import {format, parseISO, isValid} from 'date-fns'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; -import CONST from '../CONST'; -import ONYXKEYS from '../ONYXKEYS'; -import DateUtils from './DateUtils'; -import * as NumberUtils from './NumberUtils'; - -let allTransactions = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.TRANSACTION, - waitForCollectionCallback: true, - callback: (val) => { - if (!val) { - return; - } - allTransactions = _.pick(val, (transaction) => transaction); - }, -}); - -/** - * Optimistically generate a transaction. - * - * @param {Number} amount – in cents - * @param {String} currency - * @param {String} reportID - * @param {String} [comment] - * @param {String} [created] - * @param {String} [source] - * @param {String} [originalTransactionID] - * @param {String} [merchant] - * @param {Object} [receipt] - * @param {String} [filename] - * @param {String} [existingTransactionID] When creating a distance request, an empty transaction has already been created with a transactionID. In that case, the transaction here needs to have it's transactionID match what was already generated. - * @param {String} [category] - * @param {String} [tag] - * @param {Boolean} [billable] - * @returns {Object} - */ -function buildOptimisticTransaction( - amount, - currency, - reportID, - comment = '', - created = '', - source = '', - originalTransactionID = '', - merchant = '', - receipt = {}, - filename = '', - existingTransactionID = null, - category = '', - tag = '', - billable = false, -) { - // transactionIDs are random, positive, 64-bit numeric strings. - // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) - const transactionID = existingTransactionID || NumberUtils.rand64(); - - const commentJSON = {comment}; - if (source) { - commentJSON.source = source; - } - if (originalTransactionID) { - commentJSON.originalTransactionID = originalTransactionID; - } - - // For the SmartScan to run successfully, we need to pass the merchant field empty to the API - const defaultMerchant = _.isEmpty(receipt) ? CONST.TRANSACTION.DEFAULT_MERCHANT : ''; - - return { - transactionID, - amount, - currency, - reportID, - comment: commentJSON, - merchant: merchant || defaultMerchant, - created: created || DateUtils.getDBTime(), - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - receipt, - filename, - category, - tag, - billable, - }; -} - -/** - * @param {Object|null} transaction - * @returns {Boolean} - */ -function hasReceipt(transaction) { - return lodashGet(transaction, 'receipt.state', '') !== ''; -} - -/** - * @param {Object} transaction - * @returns {Boolean} - */ -function areRequiredFieldsEmpty(transaction) { - return ( - transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || - transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || - (transaction.modifiedMerchant === '' && - (transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) || - (transaction.modifiedAmount === 0 && transaction.amount === 0) || - (transaction.modifiedCreated === '' && transaction.created === '') - ); -} - -/** - * Given the edit made to the money request, return an updated transaction object. - * - * @param {Object} transaction - * @param {Object} transactionChanges - * @param {Object} isFromExpenseReport - * @returns {Object} - */ -function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport) { - // Only changing the first level fields so no need for deep clone now - const updatedTransaction = _.clone(transaction); - let shouldStopSmartscan = false; - - // The comment property does not have its modifiedComment counterpart - if (_.has(transactionChanges, 'comment')) { - updatedTransaction.comment = { - ...updatedTransaction.comment, - comment: transactionChanges.comment, - }; - } - if (_.has(transactionChanges, 'created')) { - updatedTransaction.modifiedCreated = transactionChanges.created; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'amount')) { - updatedTransaction.modifiedAmount = isFromExpenseReport ? -transactionChanges.amount : transactionChanges.amount; - shouldStopSmartscan = true; - } - if (_.has(transactionChanges, 'currency')) { - updatedTransaction.modifiedCurrency = transactionChanges.currency; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'merchant')) { - updatedTransaction.modifiedMerchant = transactionChanges.merchant; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'waypoints')) { - updatedTransaction.modifiedWaypoints = transactionChanges.waypoints; - shouldStopSmartscan = true; - } - - if (_.has(transactionChanges, 'billable')) { - updatedTransaction.billable = transactionChanges.billable; - } - - if (_.has(transactionChanges, 'category')) { - updatedTransaction.category = transactionChanges.category; - } - - if (_.has(transactionChanges, 'tag')) { - updatedTransaction.tag = transactionChanges.tag; - } - - if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) { - updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; - } - - updatedTransaction.pendingFields = { - ...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'amount') && {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'waypoints') && {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - ...(_.has(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), - }; - - return updatedTransaction; -} - -/** - * Retrieve the particular transaction object given its ID. - * - * @param {String} transactionID - * @returns {Object} - * @deprecated Use withOnyx() or Onyx.connect() instead - */ -function getTransaction(transactionID) { - return lodashGet(allTransactions, `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {}); -} - -/** - * Return the comment field (referred to as description in the App) from the transaction. - * The comment does not have its modifiedComment counterpart. - * - * @param {Object} transaction - * @returns {String} - */ -function getDescription(transaction) { - return lodashGet(transaction, 'comment.comment', ''); -} - -/** - * Return the amount field from the transaction, return the modifiedAmount if present. - * - * @param {Object} transaction - * @param {Boolean} isFromExpenseReport - * @returns {Number} - */ -function getAmount(transaction, isFromExpenseReport) { - // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value - if (!isFromExpenseReport) { - const amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return Math.abs(amount); - } - return Math.abs(lodashGet(transaction, 'amount', 0)); - } - - // Expense report case: - // The amounts are stored using an opposite sign and negative values can be set, - // we need to return an opposite sign than is saved in the transaction object - let amount = lodashGet(transaction, 'modifiedAmount', 0); - if (amount) { - return -amount; - } - - // To avoid -0 being shown, lets only change the sign if the value is other than 0. - amount = lodashGet(transaction, 'amount', 0); - return amount ? -amount : 0; -} - -/** - * Return the currency field from the transaction, return the modifiedCurrency if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCurrency(transaction) { - const currency = lodashGet(transaction, 'modifiedCurrency', ''); - if (currency) { - return currency; - } - return lodashGet(transaction, 'currency', CONST.CURRENCY.USD); -} - -/** - * Return the merchant field from the transaction, return the modifiedMerchant if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getMerchant(transaction) { - return lodashGet(transaction, 'modifiedMerchant', null) || lodashGet(transaction, 'merchant', ''); -} - -/** - * Return the waypoints field from the transaction, return the modifiedWaypoints if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getWaypoints(transaction) { - return lodashGet(transaction, 'modifiedWaypoints', null) || lodashGet(transaction, ['comment', 'waypoints']); -} - -/** - * Return the category from the transaction. This "category" field has no "modified" complement. - * - * @param {Object} transaction - * @return {String} - */ -function getCategory(transaction) { - return lodashGet(transaction, 'category', ''); -} - -/** - * Return the billable field from the transaction. This "billable" field has no "modified" complement. - * - * @param {Object} transaction - * @return {Boolean} - */ -function getBillable(transaction) { - return lodashGet(transaction, 'billable', false); -} - -/** - * Return the tag from the transaction. This "tag" field has no "modified" complement. - * - * @param {Object} transaction - * @return {String} - */ -function getTag(transaction) { - return lodashGet(transaction, 'tag', ''); -} - -/** - * Return the created field from the transaction, return the modifiedCreated if present. - * - * @param {Object} transaction - * @returns {String} - */ -function getCreated(transaction) { - const created = lodashGet(transaction, 'modifiedCreated', '') || lodashGet(transaction, 'created', ''); - const createdDate = parseISO(created); - if (isValid(createdDate)) { - return format(createdDate, CONST.DATE.FNS_FORMAT_STRING); - } - - return ''; -} - -/* - * @param {Object} transaction - * @param {Object} transaction.comment - * @param {String} transaction.comment.type - * @param {Object} [transaction.comment.customUnit] - * @param {String} [transaction.comment.customUnit.name] - * @returns {Boolean} - */ -function isDistanceRequest(transaction) { - const type = lodashGet(transaction, 'comment.type'); - const customUnitName = lodashGet(transaction, 'comment.customUnit.name'); - return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; -} - -function isReceiptBeingScanned(transaction) { - return _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], transaction.receipt.state); -} - -/** - * Check if the transaction has a non-smartscanning receipt and is missing required fields - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasMissingSmartscanFields(transaction) { - return hasReceipt(transaction) && !isDistanceRequest(transaction) && !isReceiptBeingScanned(transaction) && areRequiredFieldsEmpty(transaction); -} - -/** - * Check if the transaction has a defined route - * - * @param {Object} transaction - * @returns {Boolean} - */ -function hasRoute(transaction) { - return !!lodashGet(transaction, 'routes.route0.geometry.coordinates'); -} - -/** - * Get the transactions related to a report preview with receipts - * Get the details linked to the IOU reportAction - * - * @param {Object} reportAction - * @returns {Object} - * @deprecated Use Onyx.connect() or withOnyx() instead - */ -function getLinkedTransaction(reportAction = {}) { - const transactionID = lodashGet(reportAction, ['originalMessage', 'IOUTransactionID'], ''); - return allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; -} - -function getAllReportTransactions(reportID) { - // `reportID` from the `/CreateDistanceRequest` endpoint return's number instead of string for created `transaction`. - // For reference, https://github.com/Expensify/App/pull/26536#issuecomment-1703573277. - // We will update this in a follow-up Issue. According to this comment: https://github.com/Expensify/App/pull/26536#issuecomment-1703591019. - return _.filter(allTransactions, (transaction) => `${transaction.reportID}` === `${reportID}`); -} - -/** - * Checks if a waypoint has a valid address - * @param {Object} waypoint - * @returns {Boolean} Returns true if the address is valid - */ -function waypointHasValidAddress(waypoint) { - if (!waypoint || !waypoint.address || typeof waypoint.address !== 'string' || waypoint.address.trim() === '') { - return false; - } - return true; -} - -/** - * Converts the key of a waypoint to its index - * @param {String} key - * @returns {Number} waypoint index - */ -function getWaypointIndex(key) { - return Number(key.replace('waypoint', '')); -} - -/** - * Filters the waypoints which are valid and returns those - * @param {Object} waypoints - * @param {Boolean} reArrangeIndexes - * @returns {Object} validated waypoints - */ -function getValidWaypoints(waypoints, reArrangeIndexes = false) { - const sortedIndexes = _.map(_.keys(waypoints), (key) => getWaypointIndex(key)).sort(); - const waypointValues = _.map(sortedIndexes, (index) => waypoints[`waypoint${index}`]); - // Ensure the number of waypoints is between 2 and 25 - if (waypointValues.length < 2 || waypointValues.length > 25) { - return {}; - } - - let lastWaypointIndex = -1; - - const validWaypoints = _.reduce( - waypointValues, - (acc, currentWaypoint, index) => { - const previousWaypoint = waypointValues[lastWaypointIndex]; - // Check if the waypoint has a valid address - if (!waypointHasValidAddress(currentWaypoint)) { - return acc; - } - - // Check for adjacent waypoints with the same address - if (previousWaypoint && currentWaypoint.address === previousWaypoint.address) { - return acc; - } - - const validatedWaypoints = {...acc, [`waypoint${reArrangeIndexes ? lastWaypointIndex + 1 : index}`]: currentWaypoint}; - - lastWaypointIndex += 1; - - return validatedWaypoints; - }, - {}, - ); - return validWaypoints; -} - -export { - buildOptimisticTransaction, - getUpdatedTransaction, - getTransaction, - getDescription, - getAmount, - getCurrency, - getMerchant, - getCreated, - getCategory, - getBillable, - getTag, - getLinkedTransaction, - getAllReportTransactions, - hasReceipt, - hasRoute, - isReceiptBeingScanned, - getValidWaypoints, - isDistanceRequest, - getWaypoints, - hasMissingSmartscanFields, - getWaypointIndex, - waypointHasValidAddress, -}; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 3429a25a9268..4a9ab448546a 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -7,7 +7,7 @@ import * as NumberUtils from './NumberUtils'; import {RecentWaypoint, ReportAction, Transaction} from '../types/onyx'; import {Receipt, Comment, WaypointCollection} from '../types/onyx/Transaction'; -type AdditionalTransactionChanges = {comment?: string}; +type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; type TransactionChanges = Partial & AdditionalTransactionChanges; @@ -126,6 +126,15 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra shouldStopSmartscan = true; } + if (Object.hasOwn(transactionChanges, 'waypoints')) { + updatedTransaction.modifiedWaypoints = transactionChanges.waypoints; + shouldStopSmartscan = true; + } + + if (Object.hasOwn(transactionChanges, 'billable') && typeof transactionChanges.billable === 'boolean') { + updatedTransaction.billable = transactionChanges.billable; + } + if (Object.hasOwn(transactionChanges, 'category') && typeof transactionChanges.category === 'string') { updatedTransaction.category = transactionChanges.category; } @@ -144,6 +153,8 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra ...(Object.hasOwn(transactionChanges, 'amount') && {amount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'currency') && {currency: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'merchant') && {merchant: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'waypoints') && {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), + ...(Object.hasOwn(transactionChanges, 'billable') && {billable: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'category') && {category: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(Object.hasOwn(transactionChanges, 'tag') && {tag: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), }; @@ -212,6 +223,13 @@ function getMerchant(transaction: Transaction): string { return transaction?.modifiedMerchant ?? transaction?.merchant ?? ''; } +/** + * Return the waypoints field from the transaction, return the modifiedWaypoints if present. + */ +function getWaypoints(transaction: Transaction): WaypointCollection | undefined { + return transaction?.modifiedWaypoints ?? transaction?.comment?.waypoints; +} + /** * Return the category from the transaction. This "category" field has no "modified" complement. */ @@ -219,6 +237,13 @@ function getCategory(transaction: Transaction): string { return transaction?.category ?? ''; } +/** + * Return the billable field from the transaction. This "billable" field has no "modified" complement. + */ +function getBillable(transaction: Transaction): boolean { + return transaction?.billable ?? false; +} + /** * Return the tag from the transaction. This "tag" field has no "modified" complement. */ @@ -345,6 +370,7 @@ export { getMerchant, getCreated, getCategory, + getBillable, getTag, getLinkedTransaction, getAllReportTransactions, @@ -353,6 +379,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, + getWaypoints, hasMissingSmartscanFields, getWaypointIndex, waypointHasValidAddress, diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 6967c5cc2f63..18e296e9ea60 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -49,16 +49,14 @@ type Transaction = { modifiedCreated?: string; modifiedCurrency?: string; modifiedMerchant?: string; + modifiedWaypoints?: WaypointCollection; pendingAction: OnyxCommon.PendingAction; - pendingFields: { - comment: string; - }; receipt: Receipt; reportID: string; routes?: Routes; transactionID: string; tag: string; - pendingFields?: Partial<{[K in keyof Transaction]: 'update'}>; + pendingFields?: Partial<{[K in keyof Transaction]: ValueOf}>; }; export default Transaction;