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

[No QA] [Direct Feeds] Broken connection violation #50793

Merged
Merged
Show file tree
Hide file tree
Changes from 7 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
65 changes: 65 additions & 0 deletions src/components/BrokenConnectionDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, Report} from '@src/types/onyx';
import TextLink from './TextLink';

type BrokenConnectionDescriptionProps = {
/** Transaction id of the corresponding report */
transactionID: string;

/** Current report */
report: OnyxEntry<Report>;

/** Policy which the report is tied to */
policy: OnyxEntry<Policy>;
};

function BrokenConnectionDescription({transactionID, policy, report}: BrokenConnectionDescriptionProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

my concern here is that for a fraction of time useOnyx gives undefined value, hope this doesn't affect the violation fetch. We can only test this in production, so lets stay hopeful 🤞


const brokenConnection530Error = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530);
const brokenConnectionError = transactionViolations?.find((violation) => violation.data?.rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION);
const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy);

if (!brokenConnection530Error && !brokenConnectionError) {
return '';
}

if (brokenConnection530Error) {
return translate('violations.brokenConnection530Error');
}

if (isPolicyAdmin) {
Copy link
Contributor

Choose a reason for hiding this comment

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

cool makes sense, according to docs:

Admins can also see violation, but we hide the Mark as cash button (same as the 7-day-hold doc.)

So we match the expected results

Copy link
Contributor

Choose a reason for hiding this comment

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

@VickyStash :

What about the case when the submitter itself is the policy admin ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic for the admin will be applied. Do you have any concerns?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, if i am admin and i submit a request with violation, Now i won't be able to see the mark as cash button even when i submitted the report, is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess so cause you, as admin, can go to Company Cards page and resolve the violation (fix broken connection).
cc @joekaufmanexpensify @mountiny To confirm as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the admin is a submitter on that report, lets show them the Mark as cash button too. So I guess the logic would be - show it to the users who is the owner of the report (submitter) even if they are admin

Copy link
Contributor

@allgandalf allgandalf Oct 16, 2024

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update the check in ~15 mins!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mountiny I've started to have some doubts about this approach as it goes against the docs.
In docs it mentiones that

Admins can’t resolve this violation with the Mark as cash button, but they can fix the broken card connection, which indirectly resolves this violation.

https://docs.google.com/document/d/1rWjtZqMd15ACI5OUBTb_Kpd-1bAfrDGcZZU9jcQztUI/edit#bookmark=id.xzmm2dzeoc1g

Also the message for the admin doesn't mention Mark as cash button. Should we then update the text of the message for the admin?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated! Thank you all for clarifications! ✨

return (
<>
{`${translate('violations.adminBrokenConnectionError')}`}
<TextLink
style={[styles.textLabelSupporting, styles.link]}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id ?? '-1'))}
>{`${translate('workspace.common.companyCards')}`}</TextLink>
.
</>
);
}

if (ReportUtils.isReportApproved(report) || ReportUtils.isReportManuallyReimbursed(report) || (ReportUtils.isProcessingReport(report) && !PolicyUtils.isInstantSubmitEnabled(policy))) {
return translate('violations.memberBrokenConnectionError');
}

return `${translate('violations.memberBrokenConnectionError')} ${translate('violations.markAsCashToIgnore')}`;
}

BrokenConnectionDescription.displayName = 'BrokenConnectionDescription';

export default BrokenConnectionDescription;
30 changes: 24 additions & 6 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import BrokenConnectionDescription from './BrokenConnectionDescription';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import DelegateNoAccessModal from './DelegateNoAccessModal';
Expand Down Expand Up @@ -108,6 +109,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t));
const transactionIDs = allTransactions.map((t) => t.transactionID);
const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']);
const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', moneyRequestReport, policy);
const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID ?? '');
const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction);
const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
Expand All @@ -121,25 +123,29 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea

const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);

const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && !PolicyUtils.isPolicyAdmin(policy));

const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere;

const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy), [moneyRequestReport, policy]);

const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);

const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations;
const shouldShowSubmitButton = !!moneyRequestReport && isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations && !shouldShowBrokenConnectionViolation;

const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport);

const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton;
const shouldShowSettlementButton =
(shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton && !shouldShowBrokenConnectionViolation;

const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowStatusBar = hasAllPendingRTERViolations || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
const shouldShowStatusBar =
hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
const shouldShowAnyButton =
shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || hasAllPendingRTERViolations || shouldShowExportIntegrationButton;
shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || shouldShowMarkAsCashButton || shouldShowExportIntegrationButton;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
Expand Down Expand Up @@ -228,6 +234,18 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
if (hasOnlyHeldExpenses) {
return {icon: getStatusIcon(Expensicons.Stopwatch), description: translate('iou.expensesOnHold')};
}
if (shouldShowBrokenConnectionViolation) {
return {
icon: getStatusIcon(Expensicons.Hourglass),
description: (
<BrokenConnectionDescription
transactionID={transaction?.transactionID ?? '-1'}
report={moneyRequestReport}
policy={policy}
/>
),
};
}
if (hasAllPendingRTERViolations) {
return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')};
}
Expand Down Expand Up @@ -336,7 +354,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
/>
</View>
)}
{hasAllPendingRTERViolations && !shouldUseNarrowLayout && (
{shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (
<View style={[styles.pv2]}>
<Button
success
Expand Down Expand Up @@ -384,7 +402,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
isDisabled={shouldDisableSubmitButton}
/>
)}
{hasAllPendingRTERViolations && shouldUseNarrowLayout && (
{shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
<Button
success
text={translate('iou.markAsCash')}
Expand Down
22 changes: 20 additions & 2 deletions src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import variables from '@styles/variables';
Expand All @@ -18,6 +19,7 @@ import ROUTES from '@src/ROUTES';
import type {Policy, Report, ReportAction} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import BrokenConnectionDescription from './BrokenConnectionDescription';
import Button from './Button';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
Expand Down Expand Up @@ -61,6 +63,10 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre

const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID ?? '-1']);

const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', parentReport, policy);

const shouldShowMarkAsCashButton = hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && !PolicyUtils.isPolicyAdmin(policy));

const markAsCash = useCallback(() => {
TransactionActions.markAsCash(transaction?.transactionID ?? '-1', reportID ?? '');
}, [reportID, transaction?.transactionID]);
Expand All @@ -84,6 +90,18 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
if (TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction)) {
return {icon: getStatusIcon(Expensicons.CreditCardHourglass), description: translate('iou.transactionPendingDescription')};
}
if (shouldShowBrokenConnectionViolation) {
return {
icon: getStatusIcon(Expensicons.Hourglass),
description: (
<BrokenConnectionDescription
transactionID={transaction?.transactionID ?? '-1'}
report={report}
policy={policy}
/>
),
};
}
if (TransactionUtils.hasPendingRTERViolation(TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) {
return {icon: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')};
}
Expand Down Expand Up @@ -137,7 +155,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
shouldDisplaySearchRouter
onBackButtonPress={onBackButtonPress}
>
{hasAllPendingRTERViolations && !shouldUseNarrowLayout && (
{shouldShowMarkAsCashButton && !shouldUseNarrowLayout && (
<Button
success
text={translate('iou.markAsCash')}
Expand All @@ -156,7 +174,7 @@ function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPre
/>
)}
</HeaderWithBackButton>
{hasAllPendingRTERViolations && shouldUseNarrowLayout && (
{shouldShowMarkAsCashButton && shouldUseNarrowLayout && (
<View style={[styles.ph5, styles.pb3]}>
<Button
success
Expand Down
4 changes: 2 additions & 2 deletions src/components/MoneyRequestHeaderStatusBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ReactNode} from 'react';
import type {ReactElement, ReactNode} from 'react';
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
Expand All @@ -9,7 +9,7 @@ type MoneyRequestHeaderStatusBarProps = {
icon: ReactNode;

/** Banner Description */
description: string;
description: string | ReactElement;

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we save the description as string itself, we can convert the prop into string before passing ? (saying cause we don't want the BE to throw some weird error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@allgandalf could you clarify what BE errors do you mean?
This component just renders the description display.
Since for the admin, the broken connection violation text can include TextLink (link to company cards), it returned as a component.

function MoneyRequestHeaderStatusBar({icon, description, shouldStyleFlexGrow = true}: MoneyRequestHeaderStatusBarProps) {
const styles = useThemeStyles();
return (
<View style={[styles.dFlex, styles.flexRow, styles.alignItemsCenter, shouldStyleFlexGrow && styles.flexGrow1, styles.overflowHidden, styles.headerStatusBarContainer]}>
<View style={styles.mr2}>{icon}</View>
<View style={[styles.flexShrink1]}>
<Text style={[styles.textLabelSupporting]}>{description}</Text>
</View>
</View>
);
}

/** Whether we style flex grow */
shouldStyleFlexGrow?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import * as IOUUtils from '@libs/IOUUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {TransactionDuplicateNavigatorParamList} from '@libs/Navigation/types';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReceiptUtils from '@libs/ReceiptUtils';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
Expand Down Expand Up @@ -77,6 +78,7 @@ function MoneyRequestPreviewContent({
const [session] = useOnyx(ONYXKEYS.SESSION);
const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`);

const policy = PolicyUtils.getPolicy(iouReport?.policyID);
const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
const transactionID = isMoneyRequestAction ? ReportActionsUtils.getOriginalMessage(action)?.IOUTransactionID : '-1';
const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`);
Expand Down Expand Up @@ -248,6 +250,9 @@ function MoneyRequestPreviewContent({
if (TransactionUtils.isPending(transaction)) {
return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')};
}
if (TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', iouReport, policy)) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')};
}
if (TransactionUtils.hasPendingUI(transaction, TransactionUtils.getTransactionViolations(transaction?.transactionID ?? '-1', transactionViolations))) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')};
}
Expand Down
10 changes: 7 additions & 3 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,16 @@ function ReportPreview({
const showRTERViolationMessage =
numberOfRequests === 1 &&
TransactionUtils.hasPendingUI(allTransactions.at(0), TransactionUtils.getTransactionViolations(allTransactions.at(0)?.transactionID ?? '-1', transactionViolations));

const shouldShowBrokenConnectionViolation =
numberOfRequests === 1 && TransactionUtils.shouldShowBrokenConnectionViolation(allTransactions.at(0)?.transactionID ?? '-1', iouReport, policy);
let formattedMerchant = numberOfRequests === 1 ? TransactionUtils.getMerchant(allTransactions.at(0)) : null;
const formattedDescription = numberOfRequests === 1 ? TransactionUtils.getDescription(allTransactions.at(0)) : null;

if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) {
formattedMerchant = null;
}

const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0 && !showRTERViolationMessage;
const shouldShowSubmitButton = isOpenExpenseReport && reimbursableSpend !== 0 && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(iouReport);

// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
Expand Down Expand Up @@ -335,7 +336,7 @@ function ReportPreview({

const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(iouReport);

const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage;
const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;

const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID) || ReportUtils.hasMissingInvoiceBankAccount(iouReportID);
const shouldShowRBR = hasErrors;
Expand Down Expand Up @@ -377,6 +378,9 @@ function ReportPreview({
if (shouldShowPendingSubtitle) {
return {shouldShow: true, messageIcon: Expensicons.CreditCardHourglass, messageDescription: translate('iou.transactionPending')};
}
if (shouldShowBrokenConnectionViolation) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('violations.brokenConnection530Error')};
}
if (showRTERViolationMessage) {
return {shouldShow: true, messageIcon: Expensicons.Hourglass, messageDescription: translate('iou.pendingMatchWithCreditCard')};
}
Expand Down
9 changes: 8 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4688,7 +4688,10 @@ const translations = {
return message;
},
reviewRequired: 'Review required',
rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member, rterType}: ViolationsRterParams) => {
if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530 || rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION) {
return '';
}
if (brokenBankConnection) {
return isAdmin
? `Can't auto-match receipt due to broken bank connection which ${email} needs to fix`
Copy link
Contributor

Choose a reason for hiding this comment

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

When will we use these translations? Should we remove then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The updates were implemented in this PR.
It looks like it makes the error to be shown next to the merchant field on the Money Request View for rter violation.

Expand All @@ -4700,6 +4703,10 @@ const translations = {

return '';
},
brokenConnection530Error: 'Receipt pending due to broken bank connection.',
adminBrokenConnectionError: 'Receipt pending due to broken bank connection. Please resolve in ',
memberBrokenConnectionError: 'Receipt pending due to broken bank connection. Please ask a workspace admin to resolve.',
markAsCashToIgnore: 'Mark as cash to ignore and request payment.',
smartscanFailed: 'Receipt scanning failed. Enter details manually.',
someTagLevelsRequired: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `Missing ${tagName ?? 'Tag'}`,
tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams = {}) => `${tagName ?? 'Tag'} no longer valid`,
Expand Down
Loading
Loading