Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: distance - share with accountant #51517

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,8 @@ function MoneyRequestConfirmationList({
}

const defaultRate = defaultMileageRate?.customUnitRateID ?? '';
const lastSelectedRate = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
const rateID = lastSelectedRate;
IOU.setCustomUnitRateID(transactionID, rateID);
const lastSelectedRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? defaultRate;
IOU.setCustomUnitRateID(transactionID, lastSelectedRateID);
}, [defaultMileageRate, customUnitRateID, lastSelectedDistanceRates, policy?.id, transactionID, isDistanceRequest]);

const mileageRate = DistanceRequestUtils.getRate({transaction, policy, policyDraft});
Expand Down Expand Up @@ -278,6 +277,18 @@ function MoneyRequestConfirmationList({
const [didConfirm, setDidConfirm] = useState(isConfirmed);
const [didConfirmSplit, setDidConfirmSplit] = useState(false);

// Clear the form error if it's set to one among the list passed as an argument
const clearFormErrors = useCallback(
(errors: string[]) => {
if (!errors.includes(formError)) {
return;
}

setFormError('');
},
[formError, setFormError],
);

const shouldDisplayFieldError: boolean = useMemo(() => {
if (!isEditingSplitBill) {
return false;
Expand Down Expand Up @@ -305,6 +316,32 @@ function MoneyRequestConfirmationList({

const routeError = Object.values(transaction?.errorFields?.route ?? {}).at(0);

useEffect(() => {
// We want this effect to run only when the transaction is moving from Self DM to a workspace chat
if (!isDistanceRequest || !isMovingTransactionFromTrackExpense || !isPolicyExpenseChat) {
return;
}

const errorKey = 'iou.error.invalidRate';
const policyRates = DistanceRequestUtils.getMileageRates(policy);

// If the selected rate belongs to the policy, clear the error
if (Object.keys(policyRates).includes(customUnitRateID)) {
clearFormErrors([errorKey]);
return;
}

// If there is a distance rate in the policy that matches the rate and unit of the currently selected mileage rate, select it automatically
const matchingRate = Object.values(policyRates).find((policyRate) => policyRate.rate === mileageRate.rate && policyRate.unit === mileageRate.unit);
if (matchingRate?.customUnitRateID) {
IOU.setCustomUnitRateID(transactionID, matchingRate.customUnitRateID);
return;
}

// If none of the above conditions are met, display the rate error
setFormError(errorKey);
}, [isDistanceRequest, isPolicyExpenseChat, transactionID, mileageRate, customUnitRateID, policy, isMovingTransactionFromTrackExpense, setFormError, clearFormErrors]);

useEffect(() => {
if (shouldDisplayFieldError && didConfirmSplit) {
setFormError('iou.error.genericSmartscanFailureMessage');
Expand All @@ -315,7 +352,7 @@ function MoneyRequestConfirmationList({
return;
}
// reset the form error whenever the screen gains or loses focus
setFormError('');
clearFormErrors(['iou.error.genericSmartscanFailureMessage', 'iou.receiptScanningFailed']);

// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps -- we don't want this effect to run if it's just setFormError that changes
}, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit]);
Expand Down Expand Up @@ -470,8 +507,8 @@ function MoneyRequestConfirmationList({
return;
}

setFormError('');
}, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate]);
clearFormErrors(['iou.error.invalidSplit', 'iou.error.invalidSplitParticipants', 'iou.error.invalidSplitYourself']);
}, [isFocused, transaction, isTypeSplit, transaction?.splitShares, currentUserPersonalDetails.accountID, iouAmount, iouCurrencyCode, setFormError, translate, clearFormErrors]);

useEffect(() => {
if (!isTypeSplit || !transaction?.splitShares) {
Expand Down Expand Up @@ -638,7 +675,9 @@ function MoneyRequestConfirmationList({
}, [isTypeSplit, translate, payeePersonalDetails, getSplitSectionHeader, splitParticipants, selectedParticipants]);

useEffect(() => {
if (!isDistanceRequest || isMovingTransactionFromTrackExpense) {
if (!isDistanceRequest || (isMovingTransactionFromTrackExpense && !isPolicyExpenseChat)) {
// We don't want to recalculate the distance merchant when moving a transaction from Track Expense to a 1:1 chat, because the distance rate will be the same default P2P rate.
// When moving to a policy chat (e.g. sharing with an accountant), we should recalculate the distance merchant with the policy's rate.
return;
}

Expand All @@ -661,6 +700,7 @@ function MoneyRequestConfirmationList({
translate,
toLocaleDigit,
isDistanceRequest,
isPolicyExpenseChat,
transaction,
transactionID,
action,
Expand Down
2 changes: 2 additions & 0 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ function MoneyRequestConfirmationListFooter({
const taxRateTitle = TransactionUtils.getTaxName(policy, transaction);
// Determine if the merchant error should be displayed
const shouldDisplayMerchantError = isMerchantRequired && (shouldDisplayFieldError || formError === 'iou.error.invalidMerchant') && isMerchantEmpty;
const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate';
// The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate")
const shouldShowReceiptEmptyState = iouType === CONST.IOU.TYPE.SUBMIT && PolicyUtils.isPaidGroupPolicy(policy);
const {
Expand Down Expand Up @@ -369,6 +370,7 @@ function MoneyRequestConfirmationListFooter({
style={[styles.moneyRequestMenuItem]}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRouteWithoutParams()))}
brickRoadIndicator={shouldDisplayDistanceRateError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
disabled={didConfirm}
interactive={!!rate && !isReadOnly && isPolicyExpenseChat}
/>
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import type {
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
MissingPropertyParams,
MovedFromSelfDMParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
Expand Down Expand Up @@ -979,6 +980,7 @@ const translations = {
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${formattedAmount} ${comment ? `for ${comment}` : 'expense'}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Tracking ${formattedAmount} ${comment ? `for ${comment}` : ''}`,
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} sent${comment ? ` for ${comment}` : ''}`,
movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `moved expense from self DM to ${workspaceName ?? `chat with ${reportName}`}`,
tagSelection: 'Select a tag to better organize your spend.',
categorySelection: 'Select a category to better organize your spend.',
error: {
Expand Down Expand Up @@ -1008,6 +1010,7 @@ const translations = {
splitExpenseMultipleParticipantsErrorMessage: 'An expense cannot be split between a workspace and other members. Please update your selection.',
invalidMerchant: 'Please enter a correct merchant.',
atLeastOneAttendee: 'At least one attendee must be selected',
invalidRate: 'Rate not valid for this workspace. Please select an available rate from the workspace.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up. Payment is on hold until ${submitterDisplayName} enables their wallet.`,
enableWallet: 'Enable wallet',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import type {
MarkedReimbursedParams,
MarkReimbursedFromIntegrationParams,
MissingPropertyParams,
MovedFromSelfDMParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
Expand Down Expand Up @@ -977,6 +978,7 @@ const translations = {
threadExpenseReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `${comment ? `${formattedAmount} para ${comment}` : `Gasto de ${formattedAmount}`}`,
threadTrackReportName: ({formattedAmount, comment}: ThreadRequestReportNameParams) => `Seguimiento ${formattedAmount} ${comment ? `para ${comment}` : ''}`,
threadPaySomeoneReportName: ({formattedAmount, comment}: ThreadSentMoneyReportNameParams) => `${formattedAmount} enviado${comment ? ` para ${comment}` : ''}`,
movedFromSelfDM: ({workspaceName, reportName}: MovedFromSelfDMParams) => `movió el gasto desde su propio mensaje directo a ${workspaceName ?? `un chat con ${reportName}`}`,
tagSelection: 'Selecciona una etiqueta para organizar mejor tus gastos.',
categorySelection: 'Selecciona una categoría para organizar mejor tus gastos.',
error: {
Expand Down Expand Up @@ -1006,6 +1008,7 @@ const translations = {
splitExpenseMultipleParticipantsErrorMessage: 'Solo puedes dividir un gasto entre un único espacio de trabajo o con miembros individuales. Por favor, actualiza tu selección.',
invalidMerchant: 'Por favor, introduce un comerciante correcto.',
atLeastOneAttendee: 'Debe seleccionarse al menos un asistente',
invalidRate: 'Tasa no válida para este espacio de trabajo. Por favor, selecciona una tasa disponible en el espacio de trabajo.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su billetera`,
enableWallet: 'Habilitar billetera',
Expand Down
5 changes: 4 additions & 1 deletion src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ type ThreadRequestReportNameParams = {formattedAmount: string; comment: string};

type ThreadSentMoneyReportNameParams = {formattedAmount: string; comment: string};

type MovedFromSelfDMParams = {workspaceName?: string; reportName?: string};

type SizeExceededParams = {maxUploadSizeInMB: number};

type ResolutionConstraintsParams = {minHeightInPx: number; minWidthInPx: number; maxHeightInPx: number; maxWidthInPx: number};
Expand Down Expand Up @@ -667,7 +669,7 @@ export type {
LoggedInAsParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
SignUpNewFaceCodeParams,
MovedFromSelfDMParams,
NoLongerHaveAccessParams,
NotAllowedExtensionParams,
NotYouParams,
Expand Down Expand Up @@ -701,6 +703,7 @@ export type {
SetTheRequestParams,
SettleExpensifyCardParams,
SettledAfterAddedBankAccountParams,
SignUpNewFaceCodeParams,
SizeExceededParams,
SplitAmountParams,
StepCounterParams,
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/CategorizeTrackedExpenseParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ type CategorizeTrackedExpenseParams = {
taxCode: string;
taxAmount: number;
billable?: boolean;
waypoints?: string;
customUnitRateID?: string;
policyExpenseChatReportID?: string;
policyExpenseCreatedReportActionID?: string;
adminsChatReportID?: string;
adminsCreatedReportActionID?: string;
};

export default CategorizeTrackedExpenseParams;
6 changes: 6 additions & 0 deletions src/libs/API/parameters/ShareTrackedExpenseParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ type ShareTrackedExpenseParams = {
taxCode: string;
taxAmount: number;
billable?: boolean;
waypoints?: string;
customUnitRateID?: string;
policyExpenseChatReportID?: string;
policyExpenseCreatedReportActionID?: string;
adminsChatReportID?: string;
adminsCreatedReportActionID?: string;
};

export default ShareTrackedExpenseParams;
34 changes: 31 additions & 3 deletions src/libs/ModifiedExpenseMessage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import isEmpty from 'lodash/isEmpty';
import Onyx from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PolicyTagLists, ReportAction} from '@src/types/onyx';
import type {PolicyTagLists, Report, ReportAction} from '@src/types/onyx';
import * as CurrencyUtils from './CurrencyUtils';
import DateUtils from './DateUtils';
import * as Localize from './Localize';
import Log from './Log';
import * as PolicyUtils from './PolicyUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import * as ReportConnection from './ReportConnection';
// eslint-disable-next-line import/no-cycle
import {buildReportNameFromParticipantNames, getPolicyExpenseChatName, getPolicyName, getRootParentReport, isPolicyExpenseChat} from './ReportUtils';
import * as TransactionUtils from './TransactionUtils';

let allPolicyTags: OnyxCollection<PolicyTagLists> = {};
Expand All @@ -25,6 +27,13 @@ Onyx.connect({
},
});

let allReports: OnyxCollection<Report>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => (allReports = value),
});
neil-marcellini marked this conversation as resolved.
Show resolved Hide resolved

/**
* Utility to get message based on boolean literal value.
*/
Expand Down Expand Up @@ -126,6 +135,20 @@ function getForDistanceRequest(newMerchant: string, oldMerchant: string, newAmou
});
}

function getForExpenseMovedFromSelfDM(destinationReportID: string) {
const destinationReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${destinationReportID}`];
const rootParentReport = getRootParentReport(destinationReport);

// The "Move report" flow only supports moving expenses to a policy expense chat or a 1:1 DM.
const reportName = isPolicyExpenseChat(rootParentReport) ? getPolicyExpenseChatName(rootParentReport) : buildReportNameFromParticipantNames({report: rootParentReport});
const policyName = getPolicyName(rootParentReport, true);
Comment on lines +139 to +144
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have an issue with the dependency cycle #51517 (comment)

Can't we move this part to ReportUtils to avoid dependency cycle issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. ReportUtils already imports ModifiedExpenseMessage, so the cycle will be present anyway.
The cycle will be resolved holistically after https://expensify.slack.com/archives/C01GTK53T8Q/p1733492380567339

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, it will be the same. 😓


return Localize.translateLocal('iou.movedFromSelfDM', {
reportName,
workspaceName: !isEmpty(policyName) ? policyName : undefined,
});
}

/**
* Get the report action message when expense has been modified.
*
Expand All @@ -136,8 +159,13 @@ function getForReportAction(reportID: string | undefined, reportAction: OnyxEntr
if (!ReportActionsUtils.isModifiedExpenseAction(reportAction)) {
return '';
}

const reportActionOriginalMessage = ReportActionsUtils.getOriginalMessage(reportAction);
const policyID = ReportConnection.getAllReports()?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';
const policyID = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.policyID ?? '-1';

if (reportActionOriginalMessage?.movedToReportID) {
return getForExpenseMovedFromSelfDM(reportActionOriginalMessage.movedToReportID);
}

const removalFragments: string[] = [];
const setFragments: string[] = [];
Expand Down
29 changes: 19 additions & 10 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import * as LocalePhoneNumber from './LocalePhoneNumber';
import * as Localize from './Localize';
import Log from './Log';
import {isEmailPublicDomain} from './LoginUtils';
// eslint-disable-next-line import/no-cycle
import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import linkingConfig from './Navigation/linkingConfig';
import Navigation from './Navigation/Navigation';
Expand Down Expand Up @@ -3915,6 +3916,21 @@ const reportNameCache = new Map<string, {lastVisibleActionCreated: string; repor
*/
const getCacheKey = (report: OnyxEntry<Report>): string => `${report?.reportID}-${report?.lastVisibleActionCreated}-${report?.reportName}`;

/**
* Get the title for a report using only participant names. This may be used for 1:1 DMs and other non-categorized chats.
*/
function buildReportNameFromParticipantNames({report, personalDetails}: {report: OnyxEntry<Report>; personalDetails?: Partial<PersonalDetailsList>}) {
const participantsWithoutCurrentUser: number[] = [];
Object.keys(report?.participants ?? {}).forEach((accountID) => {
const accID = Number(accountID);
if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
participantsWithoutCurrentUser.push(accID);
}
});
const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
return participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
}

/**
* Get the title for a report.
*/
Expand Down Expand Up @@ -4074,16 +4090,7 @@ function getReportName(
}

// Not a room or PolicyExpenseChat, generate title from first 5 other participants
const participantsWithoutCurrentUser: number[] = [];
Object.keys(report?.participants ?? {}).forEach((accountID) => {
const accID = Number(accountID);
if (accID !== currentUserAccountID && participantsWithoutCurrentUser.length < 5) {
participantsWithoutCurrentUser.push(accID);
}
});
const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1;
const participantNames = participantsWithoutCurrentUser.map((accountID) => getDisplayNameForParticipant(accountID, isMultipleParticipantReport, true, false, personalDetails)).join(', ');
formattedName = participantNames;
formattedName = buildReportNameFromParticipantNames({report, personalDetails});

if (reportID) {
reportNameCache.set(cacheKey, {lastVisibleActionCreated: report?.lastVisibleActionCreated ?? '', reportName: formattedName});
Expand Down Expand Up @@ -8519,6 +8526,7 @@ export {
buildOptimisticWorkspaceChats,
buildOptimisticCardAssignedReportAction,
buildParticipantsFromAccountIDs,
buildReportNameFromParticipantNames,
buildTransactionThread,
canAccessReport,
isReportNotFound,
Expand Down Expand Up @@ -8606,6 +8614,7 @@ export {
getPersonalDetailsForAccountID,
getPolicyDescriptionText,
getPolicyExpenseChat,
getPolicyExpenseChatName,
getPolicyName,
getPolicyType,
getReimbursementDeQueuedActionMessage,
Expand Down
Loading
Loading