diff --git a/src/CONST.ts b/src/CONST.ts
index 0c98645511d4..15b89a8e9940 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -3055,6 +3055,42 @@ const CONST = {
CAROUSEL: 3,
},
+ VIOLATIONS: {
+ ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired',
+ AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense',
+ BILLABLE_EXPENSE: 'billableExpense',
+ CASH_EXPENSE_WITH_NO_RECEIPT: 'cashExpenseWithNoReceipt',
+ CATEGORY_OUT_OF_POLICY: 'categoryOutOfPolicy',
+ CONVERSION_SURCHARGE: 'conversionSurcharge',
+ CUSTOM_UNIT_OUT_OF_POLICY: 'customUnitOutOfPolicy',
+ DUPLICATED_TRANSACTION: 'duplicatedTransaction',
+ FIELD_REQUIRED: 'fieldRequired',
+ FUTURE_DATE: 'futureDate',
+ INVOICE_MARKUP: 'invoiceMarkup',
+ MAX_AGE: 'maxAge',
+ MISSING_CATEGORY: 'missingCategory',
+ MISSING_COMMENT: 'missingComment',
+ MISSING_TAG: 'missingTag',
+ MODIFIED_AMOUNT: 'modifiedAmount',
+ MODIFIED_DATE: 'modifiedDate',
+ NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense',
+ OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit',
+ OVER_CATEGORY_LIMIT: 'overCategoryLimit',
+ OVER_LIMIT: 'overLimit',
+ OVER_LIMIT_ATTENDEE: 'overLimitAttendee',
+ PER_DAY_LIMIT: 'perDayLimit',
+ RECEIPT_NOT_SMART_SCANNED: 'receiptNotSmartScanned',
+ RECEIPT_REQUIRED: 'receiptRequired',
+ RTER: 'rter',
+ SMARTSCAN_FAILED: 'smartscanFailed',
+ SOME_TAG_LEVELS_REQUIRED: 'someTagLevelsRequired',
+ TAG_OUT_OF_POLICY: 'tagOutOfPolicy',
+ TAX_AMOUNT_CHANGED: 'taxAmountChanged',
+ TAX_OUT_OF_POLICY: 'taxOutOfPolicy',
+ TAX_RATE_CHANGED: 'taxRateChanged',
+ TAX_REQUIRED: 'taxRequired',
+ },
+
/** Context menu types */
CONTEXT_MENU_TYPES: {
LINK: 'LINK',
diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js
index 514dc71ffe2c..f5b3353b2f5f 100644
--- a/src/components/ReportActionItem/MoneyRequestView.js
+++ b/src/components/ReportActionItem/MoneyRequestView.js
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import lodashValues from 'lodash/values';
import PropTypes from 'prop-types';
-import React, {useMemo} from 'react';
+import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import categoryPropTypes from '@components/categoryPropTypes';
@@ -14,12 +14,14 @@ import Switch from '@components/Switch';
import tagPropTypes from '@components/tagPropTypes';
import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
+import ViolationMessages from '@components/ViolationMessages';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
+import useViolations from '@hooks/useViolations';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CardUtils from '@libs/CardUtils';
import compose from '@libs/compose';
@@ -41,6 +43,32 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import ReportActionItemImage from './ReportActionItemImage';
+const violationNames = lodashValues(CONST.VIOLATIONS);
+
+const transactionViolationPropType = PropTypes.shape({
+ type: PropTypes.string.isRequired,
+ name: PropTypes.oneOf(violationNames).isRequired,
+ data: PropTypes.shape({
+ rejectedBy: PropTypes.string,
+ rejectReason: PropTypes.string,
+ amount: PropTypes.string,
+ surcharge: PropTypes.number,
+ invoiceMarkup: PropTypes.number,
+ maxAge: PropTypes.number,
+ tagName: PropTypes.string,
+ formattedLimitAmount: PropTypes.string,
+ categoryLimit: PropTypes.string,
+ limit: PropTypes.string,
+ category: PropTypes.string,
+ brokenBankConnection: PropTypes.bool,
+ isAdmin: PropTypes.bool,
+ email: PropTypes.string,
+ isTransactionOlderThan7Days: PropTypes.bool,
+ member: PropTypes.string,
+ taxName: PropTypes.string,
+ }),
+});
+
const propTypes = {
/** The report currently being looked at */
report: reportPropTypes.isRequired,
@@ -61,6 +89,9 @@ const propTypes = {
/** The transaction associated with the transactionThread */
transaction: transactionPropTypes,
+ /** Violations detected in this transaction */
+ transactionViolations: PropTypes.arrayOf(transactionViolationPropType),
+
/** Collection of tags attached to a policy */
policyTags: tagPropTypes,
@@ -76,10 +107,11 @@ const defaultProps = {
currency: CONST.CURRENCY.USD,
comment: {comment: ''},
},
+ transactionViolations: [],
policyTags: {},
};
-function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) {
+function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy, transactionViolations}) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
@@ -131,6 +163,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList)));
const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true));
+ const {getViolationsForField} = useViolations(transactionViolations);
+ const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]);
+
let amountDescription = `${translate('iou.amount')}`;
if (isCardTransaction) {
@@ -198,6 +233,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT))}
/>
)}
+ {canUseViolations && }
Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
- brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={hasViolations('amount') || (hasErrors && transactionAmount === 0) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
/>
+ {canUseViolations && }
Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
+ brickRoadIndicator={hasViolations('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
numberOfLinesTitle={0}
/>
+ {canUseViolations && }
{isDistanceRequest ? (
@@ -245,9 +284,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))}
- brickRoadIndicator={hasErrors && isEmptyMerchant ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && isEmptyMerchant ? translate('common.error.enterMerchant') : ''}
/>
+ {canUseViolations && }
)}
@@ -258,9 +298,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit && !isSettled}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
- brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
+ brickRoadIndicator={hasViolations('date') || (hasErrors && transactionDate === '') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''}
/>
+ {canUseViolations && }
{shouldShowCategory && (
@@ -271,7 +312,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))}
+ brickRoadIndicator={hasViolations('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
/>
+ {canUseViolations && }
)}
{shouldShowTag && (
@@ -283,7 +326,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAG))}
+ brickRoadIndicator={hasViolations('tag') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
/>
+ {canUseViolations && }
)}
{isCardTransaction && (
@@ -295,15 +340,24 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
/>
)}
+
{shouldShowBillable && (
-
- {translate('common.billable')}
- IOU.editMoneyRequest(transaction, report.reportID, {billable: value})}
- />
-
+ <>
+
+ {translate('common.billable')}
+ IOU.editMoneyRequest(transaction, report.reportID, {billable: value})}
+ />
+
+ {hasViolations('billable') && (
+
+ )}
+ >
)}
{
+ const parentReportAction = ReportActionsUtils.getParentReportAction(report);
+ const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0);
+ return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`;
+ },
+ },
+ policyTags: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`,
+ },
}),
)(MoneyRequestView);
diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx
new file mode 100644
index 000000000000..8eb555184596
--- /dev/null
+++ b/src/components/ViolationMessages.tsx
@@ -0,0 +1,26 @@
+import React, {useMemo} from 'react';
+import {View} from 'react-native';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import ViolationsUtils from '@libs/ViolationsUtils';
+import type {TransactionViolation} from '@src/types/onyx';
+import Text from './Text';
+
+export default function ViolationMessages({violations, isLast}: {violations: TransactionViolation[]; isLast?: boolean}) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const violationMessages = useMemo(() => violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate)]), [translate, violations]);
+
+ return (
+
+ {violationMessages.map(([name, message]) => (
+
+ {message}
+
+ ))}
+
+ );
+}
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index 0f43abdff6e2..76d48158237b 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -2,12 +2,12 @@ import {useCallback, useMemo} from 'react';
import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
- * Names of Fields where violations can occur
+ * Names of Fields where violations can occur.
*/
type ViolationField = 'amount' | 'billable' | 'category' | 'comment' | 'date' | 'merchant' | 'receipt' | 'tag' | 'tax';
/**
- * Map from Violation Names to the field where that violation can occur
+ * Map from Violation Names to the field where that violation can occur.
*/
const violationFields: Record = {
allTagLevelsRequired: 'tag',
@@ -60,13 +60,12 @@ function useViolations(violations: TransactionViolation[]) {
return violationGroups ?? new Map();
}, [violations]);
- const hasViolations = useCallback((field: ViolationField) => Boolean(violationsByField.get(field)?.length), [violationsByField]);
const getViolationsForField = useCallback((field: ViolationField) => violationsByField.get(field) ?? [], [violationsByField]);
return {
- hasViolations,
getViolationsForField,
};
}
export default useViolations;
+export type {ViolationField};
diff --git a/src/languages/en.ts b/src/languages/en.ts
index e223dd0a9aaf..6e177c1df141 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -74,6 +74,20 @@ import type {
UpdatedTheDistanceParams,
UpdatedTheRequestParams,
UserIsAlreadyMemberParams,
+ ViolationsAutoReportedRejectedExpenseParams,
+ ViolationsCashExpenseWithNoReceiptParams,
+ ViolationsConversionSurchargeParams,
+ ViolationsInvoiceMarkupParams,
+ ViolationsMaxAgeParams,
+ ViolationsMissingTagParams,
+ ViolationsOverAutoApprovalLimitParams,
+ ViolationsOverCategoryLimitParams,
+ ViolationsOverLimitParams,
+ ViolationsPerDayLimitParams,
+ ViolationsReceiptRequiredParams,
+ ViolationsRterParams,
+ ViolationsTagOutOfPolicyParams,
+ ViolationsTaxOutOfPolicyParams,
WaitingOnBankAccountParams,
WalletProgramParams,
WelcomeEnterMagicCodeParams,
@@ -2035,38 +2049,49 @@ export default {
copyReferralLink: 'Copy invite link',
},
violations: {
- allTagLevelsRequired: 'dummy.violations.allTagLevelsRequired',
- autoReportedRejectedExpense: 'dummy.violations.autoReportedRejectedExpense',
- billableExpense: 'dummy.violations.billableExpense',
- cashExpenseWithNoReceipt: 'dummy.violations.cashExpenseWithNoReceipt',
- categoryOutOfPolicy: 'dummy.violations.categoryOutOfPolicy',
- conversionSurcharge: 'dummy.violations.conversionSurcharge',
- customUnitOutOfPolicy: 'dummy.violations.customUnitOutOfPolicy',
- duplicatedTransaction: 'dummy.violations.duplicatedTransaction',
- fieldRequired: 'dummy.violations.fieldRequired',
- futureDate: 'dummy.violations.futureDate',
- invoiceMarkup: 'dummy.violations.invoiceMarkup',
- maxAge: 'dummy.violations.maxAge',
- missingCategory: 'dummy.violations.missingCategory',
- missingComment: 'dummy.violations.missingComment',
- missingTag: 'dummy.violations.missingTag',
- modifiedAmount: 'dummy.violations.modifiedAmount',
- modifiedDate: 'dummy.violations.modifiedDate',
- nonExpensiworksExpense: 'dummy.violations.nonExpensiworksExpense',
- overAutoApprovalLimit: 'dummy.violations.overAutoApprovalLimit',
- overCategoryLimit: 'dummy.violations.overCategoryLimit',
- overLimit: 'dummy.violations.overLimit',
- overLimitAttendee: 'dummy.violations.overLimitAttendee',
- perDayLimit: 'dummy.violations.perDayLimit',
- receiptNotSmartScanned: 'dummy.violations.receiptNotSmartScanned',
- receiptRequired: 'dummy.violations.receiptRequired',
- rter: 'dummy.violations.rter',
- smartscanFailed: 'dummy.violations.smartscanFailed',
- someTagLevelsRequired: 'dummy.violations.someTagLevelsRequired',
- tagOutOfPolicy: 'dummy.violations.tagOutOfPolicy',
- taxAmountChanged: 'dummy.violations.taxAmountChanged',
- taxOutOfPolicy: 'dummy.violations.taxOutOfPolicy',
- taxRateChanged: 'dummy.violations.taxRateChanged',
- taxRequired: 'dummy.violations.taxRequired',
+ allTagLevelsRequired: 'All tags required',
+ autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`,
+ billableExpense: 'Billable no longer valid',
+ cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Receipt required over ${amount}`,
+ categoryOutOfPolicy: 'Category no longer valid',
+ conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `Applied ${surcharge}% conversion surcharge`,
+ customUnitOutOfPolicy: 'Unit no longer valid',
+ duplicatedTransaction: 'Potential duplicate',
+ fieldRequired: 'Report fields are required',
+ futureDate: 'Future date not allowed',
+ invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Marked up by ${invoiceMarkup}%`,
+ maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`,
+ missingCategory: 'Missing category',
+ missingComment: 'Description required for selected category',
+ missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`,
+ modifiedAmount: 'Amount greater than scanned receipt',
+ modifiedDate: 'Date differs from scanned receipt',
+ nonExpensiworksExpense: 'Non-Expensiworks expense',
+ overAutoApprovalLimit: ({formattedLimitAmount}: ViolationsOverAutoApprovalLimitParams) => `Expense exceeds auto approval limit of ${formattedLimitAmount}`,
+ overCategoryLimit: ({categoryLimit}: ViolationsOverCategoryLimitParams) => `Amount over ${categoryLimit}/person category limit`,
+ overLimit: ({amount}: ViolationsOverLimitParams) => `Amount over ${amount}/person limit`,
+ overLimitAttendee: ({amount}: ViolationsOverLimitParams) => `Amount over ${amount}/person limit`,
+ perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Amount over daily ${limit}/person category limit`,
+ receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.',
+ receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Receipt required over ${amount} ${category ? ' category limit' : ''}`,
+ rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
+ if (brokenBankConnection) {
+ return isAdmin
+ ? `Can't auto-match receipt due to broken bank connection which ${email} needs to fix`
+ : "Can't auto-match receipt due to broken bank connection which you need to fix";
+ }
+ if (!isTransactionOlderThan7Days) {
+ return isAdmin ? `Ask ${member} to mark as a cash or wait 7 days and try again` : 'Awaiting merge with card transaction.';
+ }
+
+ return '';
+ },
+ smartscanFailed: 'Receipt scanning failed. Enter details manually.',
+ someTagLevelsRequired: 'Missing tag',
+ tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `${tagName ?? ''} no longer valid`,
+ taxAmountChanged: 'Tax amount was modified',
+ taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName ?? ''} no longer valid`,
+ taxRateChanged: 'Tax rate was modified',
+ taxRequired: 'Missing tax rate',
},
} satisfies TranslationBase;
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 42743f43a098..990554b0b502 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -73,6 +73,20 @@ import type {
UpdatedTheDistanceParams,
UpdatedTheRequestParams,
UserIsAlreadyMemberParams,
+ ViolationsAutoReportedRejectedExpenseParams,
+ ViolationsCashExpenseWithNoReceiptParams,
+ ViolationsConversionSurchargeParams,
+ ViolationsInvoiceMarkupParams,
+ ViolationsMaxAgeParams,
+ ViolationsMissingTagParams,
+ ViolationsOverAutoApprovalLimitParams,
+ ViolationsOverCategoryLimitParams,
+ ViolationsOverLimitParams,
+ ViolationsPerDayLimitParams,
+ ViolationsReceiptRequiredParams,
+ ViolationsRterParams,
+ ViolationsTagOutOfPolicyParams,
+ ViolationsTaxOutOfPolicyParams,
WaitingOnBankAccountParams,
WalletProgramParams,
WelcomeEnterMagicCodeParams,
@@ -2522,38 +2536,50 @@ export default {
copyReferralLink: 'Copiar enlace de invitación',
},
violations: {
- allTagLevelsRequired: 'dummy.violations.allTagLevelsRequired',
- autoReportedRejectedExpense: 'dummy.violations.autoReportedRejectedExpense',
- billableExpense: 'dummy.violations.billableExpense',
- cashExpenseWithNoReceipt: 'dummy.violations.cashExpenseWithNoReceipt',
- categoryOutOfPolicy: 'dummy.violations.categoryOutOfPolicy',
- conversionSurcharge: 'dummy.violations.conversionSurcharge',
- customUnitOutOfPolicy: 'dummy.violations.customUnitOutOfPolicy',
- duplicatedTransaction: 'dummy.violations.duplicatedTransaction',
- fieldRequired: 'dummy.violations.fieldRequired',
- futureDate: 'dummy.violations.futureDate',
- invoiceMarkup: 'dummy.violations.invoiceMarkup',
- maxAge: 'dummy.violations.maxAge',
- missingCategory: 'dummy.violations.missingCategory',
- missingComment: 'dummy.violations.missingComment',
- missingTag: 'dummy.violations.missingTag',
- modifiedAmount: 'dummy.violations.modifiedAmount',
- modifiedDate: 'dummy.violations.modifiedDate',
- nonExpensiworksExpense: 'dummy.violations.nonExpensiworksExpense',
- overAutoApprovalLimit: 'dummy.violations.overAutoApprovalLimit',
- overCategoryLimit: 'dummy.violations.overCategoryLimit',
- overLimit: 'dummy.violations.overLimit',
- overLimitAttendee: 'dummy.violations.overLimitAttendee',
- perDayLimit: 'dummy.violations.perDayLimit',
- receiptNotSmartScanned: 'dummy.violations.receiptNotSmartScanned',
- receiptRequired: 'dummy.violations.receiptRequired',
- rter: 'dummy.violations.rter',
- smartscanFailed: 'dummy.violations.smartscanFailed',
- someTagLevelsRequired: 'dummy.violations.someTagLevelsRequired',
- tagOutOfPolicy: 'dummy.violations.tagOutOfPolicy',
- taxAmountChanged: 'dummy.violations.taxAmountChanged',
- taxOutOfPolicy: 'dummy.violations.taxOutOfPolicy',
- taxRateChanged: 'dummy.violations.taxRateChanged',
- taxRequired: 'dummy.violations.taxRequired',
+ allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
+ autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
+ billableExpense: 'La opción facturable ya no es válida',
+ cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para montos mayores a ${amount}`,
+ categoryOutOfPolicy: 'La categoría ya no es válida',
+ conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`,
+ customUnitOutOfPolicy: 'Unidad ya no es válida',
+ duplicatedTransaction: 'Potencial duplicado',
+ fieldRequired: 'Los campos del informe son obligatorios',
+ futureDate: 'Fecha futura no permitida',
+ invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Incrementado un ${invoiceMarkup}%`,
+ maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`,
+ missingCategory: 'Falta categoría',
+ missingComment: 'Descripción obligatoria para categoría seleccionada',
+ missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName}`,
+ modifiedAmount: 'Importe superior al del recibo escaneado',
+ modifiedDate: 'Fecha difiere del recibo escaneado',
+ nonExpensiworksExpense: 'Gasto no es de Expensiworks',
+ overAutoApprovalLimit: ({formattedLimitAmount}: ViolationsOverAutoApprovalLimitParams) => `Importe supera el límite de aprobación automática de ${formattedLimitAmount}`,
+ overCategoryLimit: ({categoryLimit}: ViolationsOverCategoryLimitParams) => `Importe supera el límite para la categoría de ${categoryLimit}/persona`,
+ overLimit: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`,
+ overLimitAttendee: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`,
+ perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría de ${limit}/persona`,
+ receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud',
+ receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Recibo obligatorio para importes sobre ${category ? 'el limite de la categoría de ' : ''}${amount}`,
+ rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
+ if (brokenBankConnection) {
+ return isAdmin
+ ? `No se puede adjuntar recibo debido a una conexión con su banco que ${email} necesita arreglar`
+ : 'No se puede adjuntar recibo debido a una conexión con su banco que necesitas arreglar';
+ }
+ if (!isTransactionOlderThan7Days) {
+ return isAdmin
+ ? `Pídele a ${member} que marque la transacción como efectivo o espera 7 días e intenta de nuevo`
+ : 'Esperando adjuntar automáticamente a transacción de tarjeta de crédito';
+ }
+ return '';
+ },
+ smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente',
+ someTagLevelsRequired: 'Falta etiqueta',
+ tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `Le etiqueta ${tagName} ya no es válida`,
+ taxAmountChanged: 'El importe del impuesto fue modificado',
+ taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName} ya no es válido`,
+ taxRateChanged: 'La tasa de impuesto fue modificada',
+ taxRequired: 'Falta tasa de impuesto',
},
} satisfies EnglishTranslation;
diff --git a/src/languages/types.ts b/src/languages/types.ts
index dd2d339858b0..5b6e56a38689 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -209,6 +209,40 @@ type TagSelectionParams = {tagName: string};
type WalletProgramParams = {walletProgram: string};
+type ViolationsAutoReportedRejectedExpenseParams = {rejectedBy: string; rejectReason: string};
+
+type ViolationsCashExpenseWithNoReceiptParams = {amount: string};
+
+type ViolationsConversionSurchargeParams = {surcharge?: number};
+
+type ViolationsInvoiceMarkupParams = {invoiceMarkup?: number};
+
+type ViolationsMaxAgeParams = {maxAge: number};
+
+type ViolationsMissingTagParams = {tagName?: string};
+
+type ViolationsOverAutoApprovalLimitParams = {formattedLimitAmount: string};
+
+type ViolationsOverCategoryLimitParams = {categoryLimit: string};
+
+type ViolationsOverLimitParams = {amount: string};
+
+type ViolationsPerDayLimitParams = {limit: string};
+
+type ViolationsReceiptRequiredParams = {amount: string; category?: string};
+
+type ViolationsRterParams = {
+ brokenBankConnection: boolean;
+ isAdmin: boolean;
+ email?: string;
+ isTransactionOlderThan7Days: boolean;
+ member?: string;
+};
+
+type ViolationsTagOutOfPolicyParams = {tagName?: string};
+
+type ViolationsTaxOutOfPolicyParams = {taxName?: string};
+
type TaskCreatedActionParams = {title: string};
/* Translation Object types */
@@ -250,87 +284,101 @@ type TranslationFlatObject = {
};
export type {
- TranslationBase,
- TranslationPaths,
- EnglishTranslation,
- TranslationFlatObject,
+ ApprovedAmountParams,
AddressLineParams,
- CharacterLimitParams,
- MaxParticipantsReachedParams,
- ZipCodeExampleFormatParams,
- LoggedInAsParams,
- NewFaceEnterMagicCodeParams,
- WelcomeEnterMagicCodeParams,
AlreadySignedInParams,
- GoBackMessageParams,
- LocalTimeParams,
- EditActionParams,
- DeleteActionParams,
- DeleteConfirmationParams,
- BeginningOfChatHistoryDomainRoomPartOneParams,
+ AmountEachParams,
BeginningOfChatHistoryAdminRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartOneParams,
BeginningOfChatHistoryAnnounceRoomPartTwo,
- WelcomeToRoomParams,
- ReportArchiveReasonsClosedParams,
- ReportArchiveReasonsMergedParams,
- ReportArchiveReasonsRemovedFromPolicyParams,
- ReportArchiveReasonsPolicyDeletedParams,
- RequestCountParams,
- SettleExpensifyCardParams,
- RequestAmountParams,
- RequestedAmountMessageParams,
- SplitAmountParams,
+ BeginningOfChatHistoryDomainRoomPartOneParams,
+ CanceledRequestParams,
+ CharacterLimitParams,
+ ConfirmThatParams,
+ DateShouldBeAfterParams,
+ DateShouldBeBeforeParams,
+ DeleteActionParams,
+ DeleteConfirmationParams,
DidSplitAmountMessageParams,
- AmountEachParams,
+ EditActionParams,
+ EnglishTranslation,
+ EnterMagicCodeParams,
+ FormattedMaxLengthParams,
+ GoBackMessageParams,
+ GoToRoomParams,
+ IncorrectZipFormatParams,
+ InstantSummaryParams,
+ LocalTimeParams,
+ LoggedInAsParams,
+ ManagerApprovedAmountParams,
+ ManagerApprovedParams,
+ MaxParticipantsReachedParams,
+ NewFaceEnterMagicCodeParams,
+ NoLongerHaveAccessParams,
+ NotAllowedExtensionParams,
+ NotYouParams,
+ OOOEventSummaryFullDayParams,
+ OOOEventSummaryPartialDayParams,
+ OurEmailProviderParams,
+ PaidElsewhereWithAmountParams,
+ PaidWithExpensifyWithAmountParams,
+ ParentNavigationSummaryParams,
PayerOwesAmountParams,
PayerOwesParams,
PayerPaidAmountParams,
PayerPaidParams,
- ApprovedAmountParams,
- ManagerApprovedParams,
- ManagerApprovedAmountParams,
PayerSettledParams,
- WaitingOnBankAccountParams,
- CanceledRequestParams,
- SettledAfterAddedBankAccountParams,
- PaidElsewhereWithAmountParams,
- PaidWithExpensifyWithAmountParams,
- ThreadRequestReportNameParams,
- ThreadSentMoneyReportNameParams,
- SizeExceededParams,
+ RemovedTheRequestParams,
+ RenamedRoomActionParams,
+ ReportArchiveReasonsClosedParams,
+ ReportArchiveReasonsMergedParams,
+ ReportArchiveReasonsPolicyDeletedParams,
+ ReportArchiveReasonsRemovedFromPolicyParams,
+ RequestAmountParams,
+ RequestCountParams,
+ RequestedAmountMessageParams,
ResolutionConstraintsParams,
- NotAllowedExtensionParams,
- EnterMagicCodeParams,
- TransferParams,
- InstantSummaryParams,
- NotYouParams,
- DateShouldBeBeforeParams,
- DateShouldBeAfterParams,
- IncorrectZipFormatParams,
- WeSentYouMagicSignInLinkParams,
- ToValidateLoginParams,
- NoLongerHaveAccessParams,
- OurEmailProviderParams,
- ConfirmThatParams,
- UntilTimeParams,
- StepCounterParams,
- UserIsAlreadyMemberParams,
- GoToRoomParams,
- WelcomeNoteParams,
RoomNameReservedErrorParams,
- RenamedRoomActionParams,
RoomRenamedToParams,
- OOOEventSummaryFullDayParams,
- OOOEventSummaryPartialDayParams,
- ParentNavigationSummaryParams,
+ SetTheDistanceParams,
SetTheRequestParams,
- UpdatedTheRequestParams,
- RemovedTheRequestParams,
- FormattedMaxLengthParams,
+ SettleExpensifyCardParams,
+ SettledAfterAddedBankAccountParams,
+ SizeExceededParams,
+ SplitAmountParams,
+ StepCounterParams,
TagSelectionParams,
- SetTheDistanceParams,
+ TaskCreatedActionParams,
+ ThreadRequestReportNameParams,
+ ThreadSentMoneyReportNameParams,
+ ToValidateLoginParams,
+ TransferParams,
+ TranslationBase,
+ TranslationFlatObject,
+ TranslationPaths,
+ UntilTimeParams,
UpdatedTheDistanceParams,
+ UpdatedTheRequestParams,
+ UserIsAlreadyMemberParams,
+ ViolationsAutoReportedRejectedExpenseParams,
+ ViolationsCashExpenseWithNoReceiptParams,
+ ViolationsConversionSurchargeParams,
+ ViolationsInvoiceMarkupParams,
+ ViolationsMaxAgeParams,
+ ViolationsMissingTagParams,
+ ViolationsOverAutoApprovalLimitParams,
+ ViolationsOverCategoryLimitParams,
+ ViolationsOverLimitParams,
+ ViolationsPerDayLimitParams,
+ ViolationsReceiptRequiredParams,
+ ViolationsRterParams,
+ ViolationsTagOutOfPolicyParams,
+ ViolationsTaxOutOfPolicyParams,
+ WaitingOnBankAccountParams,
WalletProgramParams,
- TaskCreatedActionParams,
+ WeSentYouMagicSignInLinkParams,
+ WelcomeEnterMagicCodeParams,
+ WelcomeNoteParams,
+ WelcomeToRoomParams,
+ ZipCodeExampleFormatParams,
};
diff --git a/src/libs/ViolationsUtils.ts b/src/libs/ViolationsUtils.ts
index 748f0ed86b7f..2637686e726b 100644
--- a/src/libs/ViolationsUtils.ts
+++ b/src/libs/ViolationsUtils.ts
@@ -1,7 +1,9 @@
import reject from 'lodash/reject';
import Onyx from 'react-native-onyx';
+import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx';
+import type {Phrase, PhraseParameters} from './Localize';
const ViolationsUtils = {
/**
@@ -80,6 +82,104 @@ const ViolationsUtils = {
value: newTransactionViolations,
};
},
+ /**
+ * Gets the translated message for each violation type.
+ *
+ * Necessary because `translate` throws a type error if you attempt to pass it a template strings, when the
+ * possible values could be either translation keys that resolve to strings or translation keys that resolve to
+ * functions.
+ */
+ getViolationTranslation(
+ violation: TransactionViolation,
+ translate: (phraseKey: TKey, ...phraseParameters: PhraseParameters>) => string,
+ ): string {
+ switch (violation.name) {
+ case 'allTagLevelsRequired':
+ return translate('violations.allTagLevelsRequired');
+ case 'autoReportedRejectedExpense':
+ return translate('violations.autoReportedRejectedExpense', {
+ rejectedBy: violation.data?.rejectedBy ?? '',
+ rejectReason: violation.data?.rejectReason ?? '',
+ });
+ case 'billableExpense':
+ return translate('violations.billableExpense');
+ case 'cashExpenseWithNoReceipt':
+ return translate('violations.cashExpenseWithNoReceipt', {amount: violation.data?.amount ?? ''});
+ case 'categoryOutOfPolicy':
+ return translate('violations.categoryOutOfPolicy');
+ case 'conversionSurcharge':
+ return translate('violations.conversionSurcharge', {surcharge: violation.data?.surcharge});
+ case 'customUnitOutOfPolicy':
+ return translate('violations.customUnitOutOfPolicy');
+ case 'duplicatedTransaction':
+ return translate('violations.duplicatedTransaction');
+ case 'fieldRequired':
+ return translate('violations.fieldRequired');
+ case 'futureDate':
+ return translate('violations.futureDate');
+ case 'invoiceMarkup':
+ return translate('violations.invoiceMarkup', {invoiceMarkup: violation.data?.invoiceMarkup});
+ case 'maxAge':
+ return translate('violations.maxAge', {maxAge: violation.data?.maxAge ?? 0});
+ case 'missingCategory':
+ return translate('violations.missingCategory');
+ case 'missingComment':
+ return translate('violations.missingComment');
+ case 'missingTag':
+ return translate('violations.missingTag', {tagName: violation.data?.tagName});
+ case 'modifiedAmount':
+ return translate('violations.modifiedAmount');
+ case 'modifiedDate':
+ return translate('violations.modifiedDate');
+ case 'nonExpensiworksExpense':
+ return translate('violations.nonExpensiworksExpense');
+ case 'overAutoApprovalLimit':
+ return translate('violations.overAutoApprovalLimit', {formattedLimitAmount: violation.data?.formattedLimitAmount ?? ''});
+ case 'overCategoryLimit':
+ return translate('violations.overCategoryLimit', {categoryLimit: violation.data?.categoryLimit ?? ''});
+ case 'overLimit':
+ return translate('violations.overLimit', {amount: violation.data?.amount ?? ''});
+ case 'overLimitAttendee':
+ return translate('violations.overLimitAttendee', {amount: violation.data?.amount ?? ''});
+ case 'perDayLimit':
+ return translate('violations.perDayLimit', {limit: violation.data?.limit ?? ''});
+ case 'receiptNotSmartScanned':
+ return translate('violations.receiptNotSmartScanned');
+ case 'receiptRequired':
+ return translate('violations.receiptRequired', {
+ amount: violation.data?.amount ?? '0',
+ category: violation.data?.category ?? '',
+ });
+ case 'rter':
+ return translate('violations.rter', {
+ brokenBankConnection: violation.data?.brokenBankConnection ?? false,
+ isAdmin: violation.data?.isAdmin ?? false,
+ email: violation.data?.email,
+ isTransactionOlderThan7Days: Boolean(violation.data?.isTransactionOlderThan7Days),
+ member: violation.data?.member,
+ });
+ case 'smartscanFailed':
+ return translate('violations.smartscanFailed');
+ case 'someTagLevelsRequired':
+ return translate('violations.someTagLevelsRequired');
+ case 'tagOutOfPolicy':
+ return translate('violations.tagOutOfPolicy', {tagName: violation.data?.tagName});
+ case 'taxAmountChanged':
+ return translate('violations.taxAmountChanged');
+ case 'taxOutOfPolicy':
+ return translate('violations.taxOutOfPolicy', {taxName: violation.data?.taxName});
+ case 'taxRateChanged':
+ return translate('violations.taxRateChanged');
+ case 'taxRequired':
+ return translate('violations.taxRequired');
+ default:
+ // The interpreter should never get here because the switch cases should be exhaustive.
+ // If typescript is showing an error on the assertion below it means the switch statement is out of
+ // sync with the `ViolationNames` type, and one or the other needs to be updated.
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ return violation.name as never;
+ }
+ },
};
export default ViolationsUtils;
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index 27d70529dd8a..6def4858229f 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -223,6 +223,10 @@ export default {
marginTop: 'auto',
},
+ mtn2: {
+ marginTop: -8,
+ },
+
mtn6: {
marginTop: -24,
},
diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts
index b6dfb7bbab9a..03d5877bc5b5 100644
--- a/src/types/onyx/PolicyCategory.ts
+++ b/src/types/onyx/PolicyCategory.ts
@@ -20,5 +20,5 @@ type PolicyCategory = {
};
type PolicyCategories = Record;
-export default PolicyCategory;
-export type {PolicyCategories};
+
+export type {PolicyCategory, PolicyCategories};
diff --git a/src/types/onyx/PolicyTag.ts b/src/types/onyx/PolicyTag.ts
index 7807dcc00433..58a21dcf4df5 100644
--- a/src/types/onyx/PolicyTag.ts
+++ b/src/types/onyx/PolicyTag.ts
@@ -12,5 +12,4 @@ type PolicyTag = {
type PolicyTags = Record;
-export default PolicyTag;
-export type {PolicyTags};
+export type {PolicyTag, PolicyTags};
diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts
index f7bc5ea1ee8b..dd7a9ef65746 100644
--- a/src/types/onyx/TransactionViolation.ts
+++ b/src/types/onyx/TransactionViolation.ts
@@ -1,46 +1,34 @@
+import type CONST from '@src/CONST';
+
/**
- * Names of transaction violations
+ * Names of violations.
+ * Derived from `CONST.VIOLATIONS` to maintain a single source of truth.
*/
-type ViolationName =
- | 'allTagLevelsRequired'
- | 'autoReportedRejectedExpense'
- | 'billableExpense'
- | 'cashExpenseWithNoReceipt'
- | 'categoryOutOfPolicy'
- | 'conversionSurcharge'
- | 'customUnitOutOfPolicy'
- | 'duplicatedTransaction'
- | 'fieldRequired'
- | 'futureDate'
- | 'invoiceMarkup'
- | 'maxAge'
- | 'missingCategory'
- | 'missingComment'
- | 'missingTag'
- | 'modifiedAmount'
- | 'modifiedDate'
- | 'nonExpensiworksExpense'
- | 'overAutoApprovalLimit'
- | 'overCategoryLimit'
- | 'overLimit'
- | 'overLimitAttendee'
- | 'perDayLimit'
- | 'receiptNotSmartScanned'
- | 'receiptRequired'
- | 'rter'
- | 'smartscanFailed'
- | 'someTagLevelsRequired'
- | 'tagOutOfPolicy'
- | 'taxAmountChanged'
- | 'taxOutOfPolicy'
- | 'taxRateChanged'
- | 'taxRequired';
+type ViolationName = (typeof CONST.VIOLATIONS)[keyof typeof CONST.VIOLATIONS];
type TransactionViolation = {
type: string;
name: ViolationName;
userMessage: string;
- data?: Record;
+ data?: {
+ rejectedBy?: string;
+ rejectReason?: string;
+ amount?: string;
+ surcharge?: number;
+ invoiceMarkup?: number;
+ maxAge?: number;
+ tagName?: string;
+ formattedLimitAmount?: string;
+ categoryLimit?: string;
+ limit?: string;
+ category?: string;
+ brokenBankConnection?: boolean;
+ isAdmin?: boolean;
+ email?: string;
+ isTransactionOlderThan7Days?: boolean;
+ member?: string;
+ taxName?: string;
+ };
};
export type {TransactionViolation, ViolationName};
diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts
index de71202dcc2a..7bd9c321be5e 100644
--- a/src/types/onyx/index.ts
+++ b/src/types/onyx/index.ts
@@ -27,13 +27,11 @@ import type {PersonalDetailsList} from './PersonalDetails';
import type PersonalDetails from './PersonalDetails';
import type PlaidData from './PlaidData';
import type Policy from './Policy';
-import type {PolicyCategories} from './PolicyCategory';
-import type PolicyCategory from './PolicyCategory';
+import type {PolicyCategories, PolicyCategory} from './PolicyCategory';
import type {PolicyMembers} from './PolicyMember';
import type PolicyMember from './PolicyMember';
import type PolicyReportField from './PolicyReportField';
-import type {PolicyTags} from './PolicyTag';
-import type PolicyTag from './PolicyTag';
+import type {PolicyTag, PolicyTags} from './PolicyTag';
import type PrivatePersonalDetails from './PrivatePersonalDetails';
import type RecentlyUsedCategories from './RecentlyUsedCategories';
import type RecentlyUsedReportFields from './RecentlyUsedReportFields';