From 6079046458dcb63c4f5d1a22be026d78aace6d4a Mon Sep 17 00:00:00 2001 From: Jack Senyitko Date: Wed, 7 Jan 2026 11:26:01 -0500 Subject: [PATCH] revert 6020367 --- src/CONST/index.ts | 1 - src/ROUTES.ts | 4 - src/SCREENS.ts | 1 - .../MoneyRequestConfirmationList.tsx | 13 -- .../MoneyRequestConfirmationListFooter.tsx | 7 +- .../ReportActionItem/MoneyRequestView.tsx | 25 +-- .../Search/TransactionListItem.tsx | 20 +- src/hooks/useViolations.ts | 3 +- src/languages/de.ts | 5 - src/languages/en.ts | 5 - src/languages/es.ts | 5 - src/languages/fr.ts | 5 - src/languages/it.ts | 5 - src/languages/ja.ts | 5 - src/languages/nl.ts | 5 - src/languages/pl.ts | 5 - src/languages/pt-BR.ts | 5 - src/languages/zh-hans.ts | 5 - ...etPolicyCategoryAttendeesRequiredParams.ts | 7 - src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 - src/libs/AttendeeUtils.ts | 92 -------- .../ModalStackNavigators/index.tsx | 1 - .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 3 - src/libs/Navigation/types.ts | 4 - src/libs/TransactionUtils/index.ts | 7 +- src/libs/Violations/ViolationsUtils.ts | 50 ----- src/libs/actions/IOU/index.ts | 4 +- src/libs/actions/Policy/Category.ts | 64 ------ .../request/step/IOURequestStepAttendees.tsx | 23 +- .../categories/CategoryRequiredFieldsPage.tsx | 103 --------- .../categories/CategorySettingsPage.tsx | 103 +++++---- .../rules/IndividualExpenseRulesSection.tsx | 27 +-- src/types/onyx/PolicyCategory.ts | 3 - tests/unit/ViolationUtilsTest.ts | 203 ------------------ 36 files changed, 81 insertions(+), 741 deletions(-) delete mode 100644 src/libs/API/parameters/SetPolicyCategoryAttendeesRequiredParams.ts delete mode 100644 src/libs/AttendeeUtils.ts delete mode 100644 src/pages/workspace/categories/CategoryRequiredFieldsPage.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 4d7031ce388d..525b18be51a3 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5768,7 +5768,6 @@ const CONST = { RECEIPT_GENERATED_WITH_AI: 'receiptGeneratedWithAI', OVER_TRIP_LIMIT: 'overTripLimit', COMPANY_CARD_REQUIRED: 'companyCardRequired', - MISSING_ATTENDEES: 'missingAttendees', }, RTER_VIOLATION_TYPES: { BROKEN_CARD_CONNECTION: 'brokenCardConnection', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9da4f186d1c7..29776e8738ae 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1977,10 +1977,6 @@ const ROUTES = { route: 'workspaces/:policyID/category/:categoryName/require-receipts-over', getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/require-receipts-over` as const, }, - WORKSPACE_CATEGORY_REQUIRED_FIELDS: { - route: 'workspaces/:policyID/category/:categoryName/required-fields', - getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/required-fields` as const, - }, WORKSPACE_CATEGORY_APPROVER: { route: 'workspaces/:policyID/category/:categoryName/approver', getRoute: (policyID: string, categoryName: string) => `workspaces/${policyID}/category/${encodeURIComponent(categoryName)}/approver` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ef2ba954b794..8f763eda3ce5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -690,7 +690,6 @@ const SCREENS = { CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint', CATEGORY_APPROVER: 'Category_Approver', CATEGORY_REQUIRE_RECEIPTS_OVER: 'Category_Require_Receipts_Over', - CATEGORY_REQUIRED_FIELDS: 'Category_Required_Fields', CATEGORIES_SETTINGS: 'Categories_Settings', CATEGORIES_IMPORT: 'Categories_Import', CATEGORIES_IMPORTED: 'Categories_Imported', diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index d11f607158c4..303955628739 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -30,7 +30,6 @@ import { setMoneyRequestTaxRate, setSplitShares, } from '@libs/actions/IOU'; -import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; import {isCategoryDescriptionRequired} from '@libs/CategoryUtils'; import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; @@ -921,12 +920,6 @@ function MoneyRequestConfirmationList({ return; } - const isMissingAttendeesViolation = getIsMissingAttendeesViolation(policyCategories, iouCategory, iouAttendees, currentUserPersonalDetails, policy?.isAttendeeTrackingEnabled); - if (isMissingAttendeesViolation) { - setFormError('violations.missingAttendees'); - return; - } - if (isPerDiemRequest && (transaction.comment?.customUnit?.subRates ?? []).length === 0) { setFormError('iou.error.invalidSubrateLength'); return; @@ -1000,8 +993,6 @@ function MoneyRequestConfirmationList({ showDelegateNoAccessModal, iouCategory, policyCategories, - iouAttendees, - currentUserPersonalDetails, ], ); @@ -1025,10 +1016,6 @@ function MoneyRequestConfirmationList({ if (isTypeSplit && !shouldShowReadOnlySplits) { return debouncedFormError && translate(debouncedFormError); } - // Don't show error at the bottom of the form for missing attendees - if (formError === 'violations.missingAttendees') { - return; - } return formError && translate(formError); }, [routeError, isTypeSplit, shouldShowReadOnlySplits, debouncedFormError, formError, translate]); diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx index da70f62205cd..4e4acba9a7df 100644 --- a/src/components/MoneyRequestConfirmationListFooter.tsx +++ b/src/components/MoneyRequestConfirmationListFooter.tsx @@ -380,7 +380,6 @@ function MoneyRequestConfirmationListFooter({ const shouldDisplayDistanceRateError = formError === 'iou.error.invalidRate'; const shouldDisplayTagError = formError === 'violations.tagOutOfPolicy'; const shouldDisplayCategoryError = formError === 'violations.categoryOutOfPolicy'; - const shouldDisplayAttendeesError = formError === 'violations.missingAttendees'; const showReceiptEmptyState = shouldShowReceiptEmptyState(iouType, action, policy, isPerDiemRequest); // The per diem custom unit @@ -728,7 +727,7 @@ function MoneyRequestConfirmationListFooter({ item: ( item?.displayName ?? item?.login).join(', ')} description={`${translate('iou.attendees')} ${ iouAttendees?.length && iouAttendees.length > 1 && formattedAmountPerAttendee ? `\u00B7 ${formattedAmountPerAttendee} ${translate('common.perPerson')}` : '' @@ -742,10 +741,8 @@ function MoneyRequestConfirmationListFooter({ Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute())); }} - interactive={!isReadOnly} + interactive shouldRenderAsHTML - brickRoadIndicator={shouldDisplayAttendeesError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={shouldDisplayAttendeesError ? translate(formError) : ''} /> ), shouldShow: shouldShowAttendees, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 70fee416f672..432fa62a66f7 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -32,8 +32,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useTransactionViolations from '@hooks/useTransactionViolations'; import type {ViolationField} from '@hooks/useViolations'; import useViolations from '@hooks/useViolations'; -import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@libs/actions/IOU/index'; -import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; import {filterPersonalCards, getCompanyCardDescription} from '@libs/CardUtils'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; @@ -53,12 +51,13 @@ import { isTaxTrackingEnabled, } from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; -import {computeReportName} from '@libs/ReportNameUtils'; import {isSplitAction} from '@libs/ReportSecondaryActionUtils'; import { canEditFieldOfMoneyRequest, canEditMoneyRequest, canUserPerformWriteAction as canUserPerformWriteActionReportUtils, + // eslint-disable-next-line @typescript-eslint/no-deprecated + getReportName, getTransactionDetails, getTripIDFromTransactionParentReportID, isExpenseReport, @@ -98,6 +97,7 @@ import { import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import Navigation from '@navigation/Navigation'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; +import {initSplitExpense, updateMoneyRequestBillable, updateMoneyRequestReimbursable} from '@userActions/IOU'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -167,7 +167,6 @@ function MoneyRequestView({ const {getReportRHPActiveRoute} = useActiveRoute(); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH, {canBeMissing: true}); - const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: false}); const searchContext = useSearchContext(); @@ -473,13 +472,6 @@ function MoneyRequestView({ const hasErrors = hasMissingSmartscanFields(transaction); // Need to return undefined when we have pendingAction to avoid the duplicate pending action const getPendingFieldAction = (fieldPath: TransactionPendingFieldsKey) => (pendingAction ? undefined : transaction?.pendingFields?.[fieldPath]); - const isMissingAttendeesViolation = getIsMissingAttendeesViolation( - policyCategories, - updatedTransaction?.category ?? categoryForDisplay, - actualAttendees, - currentUserPersonalDetails, - policy?.isAttendeeTrackingEnabled, - ); const getErrorForField = (field: ViolationField, data?: OnyxTypes.TransactionViolation['data'], policyHasDependentTags = false, tagValue?: string) => { // Checks applied when creating a new expense @@ -520,10 +512,6 @@ function MoneyRequestView({ return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)).join('. ')}.`; } - if (field === 'attendees' && isMissingAttendeesViolation) { - return translate('violations.missingAttendees'); - } - return ''; }; @@ -732,9 +720,8 @@ function MoneyRequestView({ ); }); - const reportNameToDisplay = isFromMergeTransaction - ? (updatedTransaction?.reportName ?? translate('common.none')) - : (parentReport?.reportName ?? computeReportName(parentReport, allReports, allPolicies, allTransactions)); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const reportNameToDisplay = isFromMergeTransaction ? (updatedTransaction?.reportName ?? translate('common.none')) : (parentReport?.reportName ?? getReportName(parentReport)); const shouldShowReport = !!parentReportID || (isFromMergeTransaction && !!reportNameToDisplay); const reportCopyValue = !canEditReport && reportNameToDisplay !== translate('common.none') ? reportNameToDisplay : undefined; const shouldShowCategoryAnalyzing = isCategoryBeingAnalyzed(updatedTransaction ?? transaction); @@ -1025,8 +1012,6 @@ function MoneyRequestView({ onPress={() => { Navigation.navigate(ROUTES.MONEY_REQUEST_ATTENDEE.getRoute(CONST.IOU.ACTION.EDIT, iouType, transaction.transactionID, transactionThreadReport?.reportID)); }} - brickRoadIndicator={getErrorForField('attendees') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} - errorText={getErrorForField('attendees')} interactive={canEdit} shouldShowRightIcon={canEdit} shouldRenderAsHTML diff --git a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx index 30c9da177f41..ef398b51994a 100644 --- a/src/components/SelectionListWithSections/Search/TransactionListItem.tsx +++ b/src/components/SelectionListWithSections/Search/TransactionListItem.tsx @@ -21,7 +21,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import type {TransactionPreviewData} from '@libs/actions/Search'; import {handleActionButtonPress as handleActionButtonPressUtil} from '@libs/actions/Search'; -import {syncMissingAttendeesViolation} from '@libs/AttendeeUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import variables from '@styles/variables'; @@ -66,8 +65,6 @@ function TransactionListItem({ const snapshotPolicy = useMemo(() => { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${transactionItem.policyID}`] ?? {}) as Policy; }, [snapshot, transactionItem.policyID]); - // Fetch policy categories directly from Onyx since they are not included in the search snapshot - const [policyCategories] = originalUseOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${transactionItem.policyID}`, {canBeMissing: true}); const [lastPaymentMethod] = useOnyx(`${ONYXKEYS.NVP_LAST_PAYMENT_METHOD}`, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); @@ -124,25 +121,12 @@ function TransactionListItem({ ]); const transactionViolations = useMemo(() => { - const onyxViolations = (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( + return (violations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionItem.transactionID}`] ?? []).filter( (violation: TransactionViolation) => !isViolationDismissed(transactionItem, violation, currentUserDetails.email ?? '', currentUserDetails.accountID, snapshotReport, snapshotPolicy) && shouldShowViolation(snapshotReport, snapshotPolicy, violation.name, currentUserDetails.email ?? '', false, transactionItem), ); - - // Sync missingAttendees violation with current policy category settings (can be removed later when BE handles this) - const attendeeOnyxViolations = syncMissingAttendeesViolation( - onyxViolations, - policyCategories, - transactionItem.category ?? '', - transactionItem.attendees, - currentUserDetails, - snapshotPolicy?.isAttendeeTrackingEnabled ?? false, - snapshotPolicy?.type === CONST.POLICY.TYPE.CORPORATE, - ); - - return [...onyxViolations, ...attendeeOnyxViolations]; - }, [snapshotPolicy, policyCategories, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID, currentUserDetails]); + }, [snapshotPolicy, snapshotReport, transactionItem, violations, currentUserDetails.email, currentUserDetails.accountID]); const {isDelegateAccessRestricted, showDelegateNoAccessModal} = useContext(DelegateNoAccessContext); diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts index acd96ca0987a..f86a875b35ed 100644 --- a/src/hooks/useViolations.ts +++ b/src/hooks/useViolations.ts @@ -6,7 +6,7 @@ import type {TransactionViolation, ViolationName} from '@src/types/onyx'; /** * Names of Fields where violations can occur. */ -const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'none'] as const; +const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'customUnitRateID', 'none'] as const; type ViolationField = TupleToUnion; @@ -28,7 +28,6 @@ const violationNameToField: Record 'date', missingCategory: () => 'category', missingComment: () => 'comment', - missingAttendees: () => 'attendees', missingTag: () => 'tag', modifiedAmount: () => 'amount', modifiedDate: () => 'date', diff --git a/src/languages/de.ts b/src/languages/de.ts index 655b2ef688ae..37b4d4951931 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6237,10 +6237,6 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard title: 'Kategorierichtlinien', approver: 'Genehmiger', requireDescription: 'Beschreibung erforderlich', - requireFields: 'Felder verpflichtend machen', - requiredFieldsTitle: 'Pflichtfelder', - requiredFieldsDescription: (categoryName: string) => `Dies gilt für alle Ausgaben, die als ${categoryName} kategorisiert sind.`, - requireAttendees: 'Teilnehmer erforderlich machen', descriptionHint: 'Hinweis zur Beschreibung', descriptionHintDescription: (categoryName: string) => `Mitarbeitende daran erinnern, zusätzliche Informationen für Ausgaben der Kategorie „${categoryName}“ anzugeben. Dieser Hinweis erscheint im Beschreibungsfeld von Ausgaben.`, @@ -7293,7 +7289,6 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Datum älter als ${maxAge} Tage`, missingCategory: 'Fehlende Kategorie', missingComment: 'Beschreibung für ausgewählte Kategorie erforderlich', - missingAttendees: 'Für diese Kategorie sind mehrere Teilnehmer erforderlich', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Fehlende ${tagName ?? 'Tag'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/en.ts b/src/languages/en.ts index f53a0ff4a13c..9e06efe60f42 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6097,10 +6097,6 @@ const translations = { title: 'Category rules', approver: 'Approver', requireDescription: 'Require description', - requireFields: 'Require fields', - requiredFieldsTitle: 'Required fields', - requiredFieldsDescription: (categoryName: string) => `This will apply to all expenses categorized as ${categoryName}.`, - requireAttendees: 'Require attendees', descriptionHint: 'Description hint', descriptionHintDescription: (categoryName: string) => `Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`, @@ -7185,7 +7181,6 @@ const translations = { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, missingCategory: 'Missing category', missingComment: 'Description required for selected category', - missingAttendees: 'Multiple attendees required for this category', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Missing ${tagName ?? 'tag'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/es.ts b/src/languages/es.ts index b64f827a0369..188aa4a71bc2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5834,10 +5834,6 @@ ${amount} para ${merchant} - ${date}`, title: 'Reglas de categoría', approver: 'Aprobador', requireDescription: 'Requerir descripción', - requireFields: 'Requerir campos', - requiredFieldsTitle: 'Campos obligatorios', - requiredFieldsDescription: (categoryName) => `Esto se aplicará a todos los gastos categorizados como ${categoryName}.`, - requireAttendees: 'Requerir asistentes', descriptionHint: 'Sugerencia de descripción', descriptionHintDescription: (categoryName) => `Recuerda a los empleados que deben proporcionar información adicional para los gastos de “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`, @@ -7325,7 +7321,6 @@ ${amount} para ${merchant} - ${date}`, maxAge: ({maxAge}) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', missingComment: 'Descripción obligatoria para la categoría seleccionada', - missingAttendees: 'Se requieren múltiples asistentes para esta categoría', missingTag: ({tagName} = {}) => `Falta ${tagName ?? 'etiqueta'}`, modifiedAmount: ({type, displayPercentVariance}) => { switch (type) { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index e9028ea3f833..d71c334aca3c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6246,10 +6246,6 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin title: 'Règles de catégorie', approver: 'Approbateur', requireDescription: 'Description requise', - requireFields: 'Rendre les champs obligatoires', - requiredFieldsTitle: 'Champs obligatoires', - requiredFieldsDescription: (categoryName: string) => `Cela s’appliquera à toutes les dépenses classées dans la catégorie ${categoryName}.`, - requireAttendees: 'Exiger des participants', descriptionHint: 'Indice de description', descriptionHintDescription: (categoryName: string) => `Rappelez aux employés de fournir des informations supplémentaires pour les dépenses « ${categoryName} ». Cet indice apparaît dans le champ de description des dépenses.`, @@ -7304,7 +7300,6 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date de plus de ${maxAge} jours`, missingCategory: 'Catégorie manquante', missingComment: 'Description requise pour la catégorie sélectionnée', - missingAttendees: 'Plusieurs participants sont requis pour cette catégorie', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Manquant ${tagName ?? 'étiquette'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/it.ts b/src/languages/it.ts index f41517a1c156..f1c2c9e0d2af 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6219,10 +6219,6 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori title: 'Regole di categoria', approver: 'Approvatore', requireDescription: 'Richiedi descrizione', - requireFields: 'Rendi obbligatori i campi', - requiredFieldsTitle: 'Campi obbligatori', - requiredFieldsDescription: (categoryName: string) => `Questo si applicherà a tutte le spese classificate come ${categoryName}.`, - requireAttendees: 'Richiedi partecipanti', descriptionHint: 'Suggerimento per la descrizione', descriptionHintDescription: (categoryName: string) => `Ricorda ai dipendenti di fornire informazioni aggiuntive per la spesa in “${categoryName}”. Questo suggerimento appare nel campo descrizione sulle spese.`, @@ -7279,7 +7275,6 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Data precedente a ${maxAge} giorni`, missingCategory: 'Categoria mancante', missingComment: 'Descrizione richiesta per la categoria selezionata', - missingAttendees: 'Più partecipanti obbligatori per questa categoria', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Manca ${tagName ?? 'etichetta'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4df06c43524a..51c8e375b647 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6176,10 +6176,6 @@ ${reportName} title: 'カテゴリルール', approver: '承認者', requireDescription: '説明を必須にする', - requireFields: 'フィールドを必須にする', - requiredFieldsTitle: '必須項目', - requiredFieldsDescription: (categoryName: string) => `これは${categoryName}として分類されたすべての経費に適用されます。`, - requireAttendees: '参加者の入力を必須にする', descriptionHint: '説明のヒント', descriptionHintDescription: (categoryName: string) => `従業員に「${categoryName}」での支出について追加情報を提供するよう促します。このヒントは経費の説明欄に表示されます。`, descriptionHintLabel: 'ヒント', @@ -7221,7 +7217,6 @@ ${reportName} maxAge: ({maxAge}: ViolationsMaxAgeParams) => `${maxAge}日より前の日付`, missingCategory: 'カテゴリ未設定', missingComment: '選択したカテゴリーには説明が必要です', - missingAttendees: 'このカテゴリには複数の参加者が必要です', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `${tagName ?? 'タグ'} が見つかりません`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c3304f1df68b..a0b615c23bc5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6208,10 +6208,6 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten title: 'Categorisatieregels', approver: 'Fiatteur', requireDescription: 'Beschrijving vereist', - requireFields: 'Velden verplicht stellen', - requiredFieldsTitle: 'Verplichte velden', - requiredFieldsDescription: (categoryName: string) => `Dit is van toepassing op alle uitgaven die zijn gecategoriseerd als ${categoryName}.`, - requireAttendees: 'Aanwezigen verplicht stellen', descriptionHint: 'Beschrijvingstip', descriptionHintDescription: (categoryName: string) => `Herinner medewerkers eraan om extra informatie te geven voor uitgaven in de categorie “${categoryName}”. Deze tip verschijnt in het omschrijvingsveld van uitgaven.`, @@ -7265,7 +7261,6 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Datum ouder dan ${maxAge} dagen`, missingCategory: 'Ontbrekende categorie', missingComment: 'Beschrijving vereist voor geselecteerde categorie', - missingAttendees: 'Meerdere deelnemers vereist voor deze categorie', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Ontbreekt ${tagName ?? 'label'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0ebc19d0da7b..f4c74cf922d2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6199,10 +6199,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i title: 'Zasady kategorii', approver: 'Akceptujący', requireDescription: 'Wymagaj opisu', - requireFields: 'Wymagaj pól', - requiredFieldsTitle: 'Wymagane pola', - requiredFieldsDescription: (categoryName: string) => `To będzie miało zastosowanie do wszystkich wydatków skategoryzowanych jako ${categoryName}.`, - requireAttendees: 'Wymagaj uczestników', descriptionHint: 'Podpowiedź opisu', descriptionHintDescription: (categoryName: string) => `Przypominaj pracownikom o podaniu dodatkowych informacji dotyczących wydatków w kategorii „${categoryName}”. Ta podpowiedź pojawia się w polu opisu przy wydatkach.`, @@ -7253,7 +7249,6 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Data starsza niż ${maxAge} dni`, missingCategory: 'Brak kategorii', missingComment: 'Opis jest wymagany dla wybranej kategorii', - missingAttendees: 'Wymaganych jest wielu uczestników dla tej kategorii', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Brakujące ${tagName ?? 'tag'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index b8e5ee9eb465..ffbf6b1ed20e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6201,10 +6201,6 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe title: 'Regras de categoria', approver: 'Aprovador', requireDescription: 'Exigir descrição', - requireFields: 'Exigir campos', - requiredFieldsTitle: 'Campos obrigatórios', - requiredFieldsDescription: (categoryName: string) => `Isso será aplicado a todas as despesas categorizadas como ${categoryName}.`, - requireAttendees: 'Exigir participantes', descriptionHint: 'Dica de descrição', descriptionHintDescription: (categoryName: string) => `Lembre os funcionários de fornecer informações adicionais para gastos em “${categoryName}”. Essa dica aparece no campo de descrição das despesas.`, @@ -7257,7 +7253,6 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Data anterior a ${maxAge} dias`, missingCategory: 'Categoria ausente', missingComment: 'Descrição obrigatória para a categoria selecionada', - missingAttendees: 'Vários participantes são obrigatórios para esta categoria', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `Faltando ${tagName ?? 'Tag'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5e338a1dca40..df5b8f8724ab 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6078,10 +6078,6 @@ ${reportName} title: '类别规则', approver: '审批人', requireDescription: '要求描述', - requireFields: '必填字段', - requiredFieldsTitle: '必填项', - requiredFieldsDescription: (categoryName: string) => `这将适用于所有被归类为 ${categoryName} 的费用。`, - requireAttendees: '要求与会者', descriptionHint: '描述提示', descriptionHintDescription: (categoryName: string) => `提醒员工为“${categoryName}”支出提供更多信息。此提示将显示在报销单的描述字段中。`, descriptionHintLabel: '提示', @@ -7105,7 +7101,6 @@ ${reportName} maxAge: ({maxAge}: ViolationsMaxAgeParams) => `日期早于 ${maxAge} 天`, missingCategory: '缺少类别', missingComment: '所选类别需要填写描述', - missingAttendees: '此类别需要多个参与者', missingTag: ({tagName}: ViolationsMissingTagParams = {}) => `缺少 ${tagName ?? '标签'}`, modifiedAmount: ({type, displayPercentVariance}: ViolationsModifiedAmountParams) => { switch (type) { diff --git a/src/libs/API/parameters/SetPolicyCategoryAttendeesRequiredParams.ts b/src/libs/API/parameters/SetPolicyCategoryAttendeesRequiredParams.ts deleted file mode 100644 index abaa94f241cc..000000000000 --- a/src/libs/API/parameters/SetPolicyCategoryAttendeesRequiredParams.ts +++ /dev/null @@ -1,7 +0,0 @@ -type SetPolicyCategoryAttendeesRequiredParams = { - policyID: string; - categoryName: string; - areAttendeesRequired: boolean; -}; - -export default SetPolicyCategoryAttendeesRequiredParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 2738d1fda1e9..bece054a0237 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -345,7 +345,6 @@ export type {default as AddDelegateParams} from './AddDelegateParams'; export type {default as UpdateDelegateRoleParams} from './UpdateDelegateRoleParams'; export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams'; export type {default as SetPolicyCategoryDescriptionRequiredParams} from './SetPolicyCategoryDescriptionRequiredParams'; -export type {default as SetPolicyCategoryAttendeesRequiredParams} from './SetPolicyCategoryAttendeesRequiredParams'; export type {default as SetPolicyCategoryApproverParams} from './SetPolicyCategoryApproverParams'; export type {default as SetWorkspaceCategoryDescriptionHintParams} from './SetWorkspaceCategoryDescriptionHintParams'; export type {default as SetPolicyCategoryTaxParams} from './SetPolicyCategoryTaxParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index aa3b948ae878..0e2fca6d06cd 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -263,7 +263,6 @@ const WRITE_COMMANDS = { SET_POLICY_ATTENDEE_TRACKING_ENABLED: 'SetPolicyAttendeeTrackingEnabled', SET_POLICY_REQUIRE_COMPANY_CARDS_ENABLED: 'SetPolicyRequireCompanyCardsEnabled', SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED: 'SetPolicyCategoryDescriptionRequired', - SET_POLICY_CATEGORY_ATTENDEES_REQUIRED: 'SetPolicyCategoryAttendeesRequired', SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT: 'SetWorkspaceCategoryDescriptionHint', SET_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'SetPolicyCategoryReceiptsRequired', REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'RemoveWorkspaceCategoryReceiptsRequired', @@ -790,7 +789,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; [WRITE_COMMANDS.SET_POLICY_REQUIRE_COMPANY_CARDS_ENABLED]: Parameters.SetPolicyRequireCompanyCardsEnabledParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED]: Parameters.SetPolicyCategoryDescriptionRequiredParams; - [WRITE_COMMANDS.SET_POLICY_CATEGORY_ATTENDEES_REQUIRED]: Parameters.SetPolicyCategoryAttendeesRequiredParams; [WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT]: Parameters.SetWorkspaceCategoryDescriptionHintParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.SetPolicyCategoryReceiptsRequiredParams; [WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams; diff --git a/src/libs/AttendeeUtils.ts b/src/libs/AttendeeUtils.ts deleted file mode 100644 index a3ada3de4fe4..000000000000 --- a/src/libs/AttendeeUtils.ts +++ /dev/null @@ -1,92 +0,0 @@ -import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import CONST from '@src/CONST'; -import type {PolicyCategories, PolicyCategory} from '@src/types/onyx'; -import type {Attendee} from '@src/types/onyx/IOU'; -import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; - -/** Formats the title for requiredFields menu item based on which fields are enabled in the policy category */ -function formatRequiredFieldsTitle(translate: LocaleContextProps['translate'], policyCategory: PolicyCategory, isAttendeeTrackingEnabled = false): string { - const enabledFields: string[] = []; - - // Attendees field should show first when both are selected and attendee tracking is enabled - if (isAttendeeTrackingEnabled && policyCategory.areAttendeesRequired) { - enabledFields.push(translate('iou.attendees')); - } - - if (policyCategory.areCommentsRequired) { - enabledFields.push(translate('common.description')); - } - - if (enabledFields.length === 0) { - return ''; - } - - const [first, ...rest] = enabledFields; - const capitalizedFirst = first.charAt(0).toUpperCase() + first.slice(1); - const lowercasedRest = rest.map((field) => field.charAt(0).toLowerCase() + field.slice(1)); - return [capitalizedFirst, ...lowercasedRest].join(', '); -} - -/** Returns whether there are missing attendees for the given category */ -function getIsMissingAttendeesViolation( - policyCategories: PolicyCategories | undefined, - category: string, - iouAttendees: Attendee[] | string | undefined, - userPersonalDetails: CurrentUserPersonalDetails, - isAttendeeTrackingEnabled = false, -) { - const areAttendeesRequired = !!policyCategories?.[category ?? '']?.areAttendeesRequired; - // If attendee tracking is disabled at the policy level, don't enforce attendee requirement - if (!isAttendeeTrackingEnabled || !areAttendeesRequired) { - return false; - } - - const creatorLogin = userPersonalDetails.login ?? ''; - const attendees = Array.isArray(iouAttendees) ? iouAttendees : []; - const attendeesMinusCreatorCount = attendees.filter((a) => a?.login !== creatorLogin).length; - - if (attendees.length === 0 || attendeesMinusCreatorCount === 0) { - return true; - } - - return false; -} - -/** - * Syncs the missingAttendees violation with current policy settings. - * - Adds the violation when it should show but isn't present from BE - * - Removes stale BE violation when policy settings changed (e.g., category no longer requires attendees) - */ -function syncMissingAttendeesViolation( - violations: T[], - policyCategories: PolicyCategories | undefined, - category: string, - attendees: Attendee[] | undefined, - userPersonalDetails: CurrentUserPersonalDetails, - isAttendeeTrackingEnabled: boolean, - isControlPolicy: boolean, -): T[] { - const hasMissingAttendeesViolation = violations.some((v) => v.name === CONST.VIOLATIONS.MISSING_ATTENDEES); - const shouldShowMissingAttendees = - isControlPolicy && getIsMissingAttendeesViolation(policyCategories ?? {}, category ?? '', attendees ?? [], userPersonalDetails, isAttendeeTrackingEnabled); - - if (!hasMissingAttendeesViolation && shouldShowMissingAttendees) { - // Add violation when it should show but isn't present from BE - return [ - ...violations, - { - name: CONST.VIOLATIONS.MISSING_ATTENDEES, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - } as unknown as T, - ]; - } - if (hasMissingAttendeesViolation && !shouldShowMissingAttendees) { - // Remove stale BE violation when policy settings changed - return violations.filter((v) => v.name !== CONST.VIOLATIONS.MISSING_ATTENDEES); - } - - return violations; -} - -export {formatRequiredFieldsTitle, getIsMissingAttendeesViolation, syncMissingAttendeesViolation}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 5b6efd0a59ac..59e3251ff905 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -430,7 +430,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceOverviewSharePage').default, [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../../pages/workspace/WorkspaceOverviewCurrencyPage').default, [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: () => require('../../../../pages/workspace/categories/CategorySettingsPage').default, - [SCREENS.WORKSPACE.CATEGORY_REQUIRED_FIELDS]: () => require('../../../../pages/workspace/categories/CategoryRequiredFieldsPage').default, [SCREENS.WORKSPACE.ADDRESS]: () => require('../../../../pages/workspace/WorkspaceOverviewAddressPage').default, [SCREENS.WORKSPACE.PLAN]: () => require('../../../../pages/workspace/WorkspaceOverviewPlanTypePage').default, [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: () => require('../../../../pages/workspace/categories/WorkspaceCategoriesSettingsPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index e762868d3488..82742ec57e4f 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -213,7 +213,6 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.CATEGORY_REQUIRE_RECEIPTS_OVER]: { path: ROUTES.WORKSPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.route, }, - [SCREENS.WORKSPACE.CATEGORY_REQUIRED_FIELDS]: { - path: ROUTES.WORKSPACE_CATEGORY_REQUIRED_FIELDS.route, - }, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0aa2c14b6735..19aee4008e02 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -335,10 +335,6 @@ type SettingsNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; }; - [SCREENS.WORKSPACE.CATEGORY_REQUIRED_FIELDS]: { - policyID: string; - categoryName: string; - }; [SCREENS.WORKSPACE.UPGRADE]: { policyID?: string; featureName?: string; diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 0e333b1fdfbc..95a20b642728 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -462,7 +462,7 @@ function shouldShowAttendees(iouType: IOUType, policy: OnyxEntry): boole // For backwards compatibility with Expensify Classic, we assume that Attendee Tracking is enabled by default on // Control policies if the policy does not contain the attribute - return policy?.isAttendeeTrackingEnabled ?? false; + return policy?.isAttendeeTrackingEnabled ?? true; } /** @@ -1443,7 +1443,6 @@ function shouldShowViolation( const isSubmitter = isCurrentUserSubmitter(iouReport); const isPolicyMember = isPolicyMemberPolicyUtils(policy, currentUserEmail); const isReportOpen = isOpenExpenseReport(iouReport); - const isAttendeeTrackingEnabled = policy?.isAttendeeTrackingEnabled ?? false; if (violationName === CONST.VIOLATIONS.AUTO_REPORTED_REJECTED_EXPENSE) { return isSubmitter || isPolicyAdmin(policy); @@ -1461,10 +1460,6 @@ function shouldShowViolation( return isPolicyMember && !isSubmitter && !isReportOpen; } - if (violationName === CONST.VIOLATIONS.MISSING_ATTENDEES) { - return isAttendeeTrackingEnabled; - } - if (violationName === CONST.VIOLATIONS.MISSING_CATEGORY && isCategoryBeingAnalyzed(transaction)) { return false; } diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 5967c56828a7..77a1151fa3aa 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -4,7 +4,6 @@ import reject from 'lodash/reject'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {getCurrentUserEmail} from '@libs/actions/Report'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; @@ -336,7 +335,6 @@ const ViolationsUtils = { // const hasOverTripLimitViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.OVER_TRIP_LIMIT); const hasCategoryOverLimitViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.OVER_CATEGORY_LIMIT); const hasMissingCommentViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_COMMENT); - const hasMissingAttendeesViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.MISSING_ATTENDEES); const hasTaxOutOfPolicyViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.TAX_OUT_OF_POLICY); const isDistanceRequest = TransactionUtils.isDistanceRequest(updatedTransaction); const isPerDiemRequest = TransactionUtils.isPerDiemRequest(updatedTransaction); @@ -384,40 +382,6 @@ const ViolationsUtils = { const shouldCategoryShowOverLimitViolation = canCalculateAmountViolations && !isInvoiceTransaction && typeof categoryOverLimit === 'number' && expenseAmount > categoryOverLimit && isControlPolicy; const shouldShowMissingComment = !isInvoiceTransaction && policyCategories?.[categoryName ?? '']?.areCommentsRequired && !updatedTransaction.comment?.comment && isControlPolicy; - const attendees = updatedTransaction.modifiedAttendees ?? updatedTransaction.comment?.attendees ?? []; - const isAttendeeTrackingEnabled = policy.isAttendeeTrackingEnabled ?? false; - // Filter out the owner/creator when checking attendance count - expense is valid if at least one non-owner attendee is present - const ownerAccountID = iouReport?.ownerAccountID; - // Calculate attendees minus owner. When ownerAccountID is known, filter by accountID. - // When ownerAccountID is undefined (offline split where iouReport is unavailable), - // fallback to using login/email to identify the owner (similar to AttendeeUtils approach). - let attendeesMinusOwnerCount: number; - if (ownerAccountID !== undefined) { - // Normal case: filter by accountID - attendeesMinusOwnerCount = attendees.filter((a) => a?.accountID !== ownerAccountID).length; - } else { - // Offline scenario: ownerAccountID unavailable, use login/email as fallback - const currentUserEmail = getCurrentUserEmail(); - if (currentUserEmail) { - // Filter by login or email to identify owner - attendeesMinusOwnerCount = attendees.filter((a) => { - const attendeeIdentifier = a?.login ?? a?.email; - return attendeeIdentifier !== currentUserEmail; - }).length; - } else { - // Can't identify owner at all - if there are attendees, assume owner is one of them - // This means we need at least 2 attendees to have a non-owner attendee - attendeesMinusOwnerCount = Math.max(0, attendees.length - 1); - } - } - - const shouldShowMissingAttendees = - !isInvoiceTransaction && - isAttendeeTrackingEnabled && - policyCategories?.[categoryName ?? '']?.areAttendeesRequired && - isControlPolicy && - (attendees.length === 0 || attendeesMinusOwnerCount === 0); - const hasFutureDateViolation = transactionViolations.some((violation) => violation.name === 'futureDate'); // Add 'futureDate' violation if transaction date is in the future and policy type is corporate if (!hasFutureDateViolation && shouldDisplayFutureDateViolation) { @@ -500,18 +464,6 @@ const ViolationsUtils = { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.MISSING_COMMENT}); } - if (!hasMissingAttendeesViolation && shouldShowMissingAttendees) { - newTransactionViolations.push({ - name: CONST.VIOLATIONS.MISSING_ATTENDEES, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }); - } - - if (hasMissingAttendeesViolation && !shouldShowMissingAttendees) { - newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.MISSING_ATTENDEES}); - } - if (isPolicyTrackTaxEnabled && !hasTaxOutOfPolicyViolation && !isTaxInPolicy) { newTransactionViolations.push({name: CONST.VIOLATIONS.TAX_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true}); } @@ -581,8 +533,6 @@ const ViolationsUtils = { return translate('violations.missingCategory'); case 'missingComment': return translate('violations.missingComment'); - case 'missingAttendees': - return translate('violations.missingAttendees'); case 'missingTag': return translate('violations.missingTag', {tagName}); case 'modifiedAmount': diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 0a95d5e19da5..7b7f72e34adc 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -4306,7 +4306,6 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U const hasModifiedTaxCode = 'taxCode' in transactionChanges; const hasModifiedDate = 'date' in transactionChanges; const hasModifiedMerchant = 'merchant' in transactionChanges; - const hasModifiedAttendees = 'attendees' in transactionChanges; const isInvoice = isInvoiceReportReportUtils(iouReport); if ( @@ -4325,8 +4324,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U hasModifiedAmount || hasModifiedCreated || hasModifiedReimbursable || - hasModifiedTaxCode || - hasModifiedAttendees) + hasModifiedTaxCode) ) { const currentTransactionViolations = allTransactionViolations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? []; // If the amount, currency or date have been modified, we remove the duplicate violations since they would be out of date as the transaction has changed diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 27b700b2970c..a404d2854d4a 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -11,7 +11,6 @@ import type { OpenPolicyCategoriesPageParams, RemovePolicyCategoryReceiptsRequiredParams, SetPolicyCategoryApproverParams, - SetPolicyCategoryAttendeesRequiredParams, SetPolicyCategoryDescriptionRequiredParams, SetPolicyCategoryMaxAmountParams, SetPolicyCategoryReceiptsRequiredParams, @@ -1521,68 +1520,6 @@ function setPolicyCategoryTax(policy: OnyxEntry, categoryName: string, t API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX, parameters, onyxData); } -function setPolicyCategoryAttendeesRequired(policyID: string, categoryName: string, areAttendeesRequired: boolean, policyCategories: PolicyCategories = {}) { - const policyCategoryToUpdate = policyCategories?.[categoryName]; - const originalAreAttendeesRequired = policyCategoryToUpdate?.areAttendeesRequired; - - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - areAttendeesRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - areAttendeesRequired, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - pendingAction: null, - pendingFields: { - areAttendeesRequired: null, - }, - areAttendeesRequired, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - pendingAction: null, - pendingFields: { - areAttendeesRequired: null, - }, - areAttendeesRequired: originalAreAttendeesRequired, - }, - }, - }, - ], - }; - - const parameters: SetPolicyCategoryAttendeesRequiredParams = { - policyID, - categoryName, - areAttendeesRequired, - }; - - API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_ATTENDEES_REQUIRED, parameters, onyxData); -} - export { buildOptimisticPolicyCategories, buildOptimisticMccGroup, @@ -1597,7 +1534,6 @@ export { removePolicyCategoryReceiptsRequired, renamePolicyCategory, setPolicyCategoryApprover, - setPolicyCategoryAttendeesRequired, setPolicyCategoryDescriptionRequired, buildOptimisticPolicyWithExistingCategories, setPolicyCategoryGLCode, diff --git a/src/pages/iou/request/step/IOURequestStepAttendees.tsx b/src/pages/iou/request/step/IOURequestStepAttendees.tsx index 5258f40e7b89..e2f4c024fb57 100644 --- a/src/pages/iou/request/step/IOURequestStepAttendees.tsx +++ b/src/pages/iou/request/step/IOURequestStepAttendees.tsx @@ -1,5 +1,6 @@ import {deepEqual} from 'fast-equals'; import React, {useCallback, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -14,27 +15,37 @@ import MoneyRequestAttendeeSelector from '@pages/iou/request/MoneyRequestAttende import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import StepScreenWrapper from './StepScreenWrapper'; import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -type IOURequestStepAttendeesProps = WithWritableReportOrNotFoundProps; +type IOURequestStepAttendeesOnyxProps = { + /** The policy of the report */ + policy: OnyxEntry; + + /** Collection of categories attached to a policy */ + policyCategories: OnyxEntry; + + /** Collection of tags attached to a policy */ + policyTags: OnyxEntry; +}; + +type IOURequestStepAttendeesProps = IOURequestStepAttendeesOnyxProps & WithWritableReportOrNotFoundProps; function IOURequestStepAttendees({ route: { params: {transactionID, reportID, iouType, backTo, action}, }, + policy, + policyTags, + policyCategories, }: IOURequestStepAttendeesProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isEditing = action === CONST.IOU.ACTION.EDIT; // eslint-disable-next-line rulesdir/no-default-id-values const [transaction] = useOnyx(`${isEditing ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID || CONST.DEFAULT_NUMBER_ID}`, {canBeMissing: true}); - const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: true}); - const policyID = report?.policyID; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: true}); const [attendees, setAttendees] = useState(() => getOriginalAttendees(transaction, currentUserPersonalDetails)); const previousAttendees = usePrevious(attendees); const {translate} = useLocalize(); diff --git a/src/pages/workspace/categories/CategoryRequiredFieldsPage.tsx b/src/pages/workspace/categories/CategoryRequiredFieldsPage.tsx deleted file mode 100644 index 728e9792dd38..000000000000 --- a/src/pages/workspace/categories/CategoryRequiredFieldsPage.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import RenderHTML from '@components/RenderHTML'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Switch from '@components/Switch'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getDecodedCategoryName} from '@libs/CategoryUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SettingsNavigatorParamList} from '@navigation/types'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {setPolicyCategoryAttendeesRequired, setPolicyCategoryDescriptionRequired} from '@userActions/Policy/Category'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; - -type CategoryRequiredFieldsPageProps = PlatformStackScreenProps; - -function CategoryRequiredFieldsPage({ - route: { - params: {policyID, categoryName}, - }, -}: CategoryRequiredFieldsPageProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: true}); - const decodedCategoryName = getDecodedCategoryName(categoryName); - - const policyCategory = policyCategories?.[categoryName]; - const areCommentsRequired = policyCategory?.areCommentsRequired ?? false; - const areAttendeesRequired = policyCategory?.areAttendeesRequired ?? false; - const isAttendeeTrackingEnabled = policy?.isAttendeeTrackingEnabled ?? false; - - return ( - - - Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} - /> - - - - - - - - {translate('workspace.rules.categoryRules.requireDescription')} - { - setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired, policyCategories); - }} - /> - - - - {isAttendeeTrackingEnabled && ( - - - - {translate('workspace.rules.categoryRules.requireAttendees')} - { - setPolicyCategoryAttendeesRequired(policyID, categoryName, !areAttendeesRequired, policyCategories); - }} - /> - - - - )} - - - - ); -} - -CategoryRequiredFieldsPage.displayName = 'CategoryRequiredFieldsPage'; - -export default CategoryRequiredFieldsPage; diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 1eb16f3d9977..e22c8bc46aa1 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -17,7 +17,6 @@ import useLocalize from '@hooks/useLocalize'; import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation'; import usePolicyData from '@hooks/usePolicyData'; import useThemeStyles from '@hooks/useThemeStyles'; -import {formatRequiredFieldsTitle} from '@libs/AttendeeUtils'; import {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApproverRule, getCategoryDefaultTaxRate, getDecodedCategoryName} from '@libs/CategoryUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import {getLatestErrorMessageField} from '@libs/ErrorUtils'; @@ -29,7 +28,13 @@ import {getWorkflowApprovalsUnavailable, isControlPolicy} from '@libs/PolicyUtil import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {clearCategoryErrors, deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@userActions/Policy/Category'; +import { + clearCategoryErrors, + deleteWorkspaceCategories, + setPolicyCategoryDescriptionRequired, + setWorkspaceCategoryDescriptionHint, + setWorkspaceCategoryEnabled, +} from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -122,22 +127,6 @@ function CategorySettingsPage({ return formatRequireReceiptsOverText(translate, policy, policyCategory?.maxAmountNoReceipt); }, [policy, policyCategory?.maxAmountNoReceipt, translate]); - const requiredFieldsTitle = useMemo(() => { - if (!policyCategory) { - return ''; - } - return formatRequiredFieldsTitle(translate, policyCategory, policy?.isAttendeeTrackingEnabled); - }, [policyCategory, translate, policy?.isAttendeeTrackingEnabled]); - - const requireFieldsPendingAction = useMemo(() => { - if (policy?.isAttendeeTrackingEnabled) { - // Pending fields are objects so we can't use nullish coalescing - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return policyCategory?.pendingFields?.areAttendeesRequired || policyCategory?.pendingFields?.areCommentsRequired; - } - return policyCategory?.pendingFields?.areCommentsRequired; - }, [policyCategory?.pendingFields, policy?.isAttendeeTrackingEnabled]); - if (!policyCategory) { return ; } @@ -299,37 +288,40 @@ function CategorySettingsPage({ /> - {areCommentsRequired && ( - - { - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - )} - {!isThereAnyAccountingConnection && ( - { - if (shouldPreventDisableOrDelete) { - setIsCannotDeleteOrDisableLastCategoryModalVisible(true); - return; - } - setDeleteCategoryConfirmModalVisible(true); - }} - /> - )} - {!!policy?.areRulesEnabled && ( <> {translate('workspace.rules.categoryRules.title')} + + + + {translate('workspace.rules.categoryRules.requireDescription')} + { + if (policyCategory.commentHint && areCommentsRequired) { + setWorkspaceCategoryDescriptionHint(policyID, categoryName, '', policyCategories); + } + setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired, policyCategories); + }} + /> + + + + {!!policyCategory?.areCommentsRequired && ( + + { + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} - - { - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_REQUIRED_FIELDS.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - )} + {!isThereAnyAccountingConnection && ( + { + if (shouldPreventDisableOrDelete) { + setIsCannotDeleteOrDisableLastCategoryModalVisible(true); + return; + } + setDeleteCategoryConfirmModalVisible(true); + }} + /> + )} diff --git a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx index 668e90f4d6cc..c79e070306e2 100644 --- a/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx +++ b/src/pages/workspace/rules/IndividualExpenseRulesSection.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -8,10 +8,8 @@ import Section from '@components/Section'; import useCardFeeds from '@hooks/useCardFeeds'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {setPolicyCategoryAttendeesRequired} from '@libs/actions/Policy/Category'; import {getCashExpenseReimbursableMode, setPolicyAttendeeTrackingEnabled, setPolicyRequireCompanyCardsEnabled, setWorkspaceEReceiptsEnabled} from '@libs/actions/Policy/Policy'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -19,7 +17,6 @@ import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOpt import type {ThemeStyles} from '@styles/index'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -75,27 +72,9 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection const policy = usePolicy(policyID); const [cardFeeds] = useCardFeeds(policyID); const {environmentURL} = useEnvironment(); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; - const handleAttendeeTrackingToggle = useCallback( - (newValue: boolean) => { - // When disabling attendee tracking, disable areAttendeesRequired on all categories - // that have it enabled in order to avoid BE validation errors - if (!newValue && policyCategories) { - for (const [categoryName, category] of Object.entries(policyCategories)) { - if (!category?.areAttendeesRequired) { - continue; - } - setPolicyCategoryAttendeesRequired(policyID, categoryName, false, policyCategories); - } - } - setPolicyAttendeeTrackingEnabled(policyID, newValue); - }, - [policyID, policyCategories], - ); - const maxExpenseAmountNoReceiptText = useMemo(() => { if (policy?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE) { return ''; @@ -201,7 +180,7 @@ function IndividualExpenseRulesSection({policyID}: IndividualExpenseRulesSection // For backwards compatibility with Expensify Classic, we assume that Attendee Tracking is enabled by default on // Control policies if the policy does not contain the attribute - const isAttendeeTrackingEnabled = policy?.isAttendeeTrackingEnabled ?? false; + const isAttendeeTrackingEnabled = policy?.isAttendeeTrackingEnabled ?? true; return (
handleAttendeeTrackingToggle(!isAttendeeTrackingEnabled)} + onToggle={() => setPolicyAttendeeTrackingEnabled(policyID, !isAttendeeTrackingEnabled)} pendingAction={policy?.pendingFields?.isAttendeeTrackingEnabled} /> diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts index 4f0848dde72f..90aa92ca9367 100644 --- a/src/types/onyx/PolicyCategory.ts +++ b/src/types/onyx/PolicyCategory.ts @@ -48,9 +48,6 @@ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Max expense amount with no receipt violation */ maxAmountNoReceipt?: number | null; - - /** If true, not providing attendees for a transaction with this category will trigger a violation */ - areAttendeesRequired?: boolean; }>; /** Record of policy categories, indexed by their name */ diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index cfa737877593..43e0798470c8 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -9,12 +9,6 @@ import type {Policy, PolicyCategories, PolicyTagLists, Report, Transaction, Tran import type {TransactionCollectionDataSet} from '@src/types/onyx/Transaction'; import {translateLocal} from '../utils/TestHelper'; -// Mock getCurrentUserEmail from Report actions -const MOCK_CURRENT_USER_EMAIL = 'test@expensify.com'; -jest.mock('@libs/actions/Report', () => ({ - getCurrentUserEmail: jest.fn(() => MOCK_CURRENT_USER_EMAIL), -})); - const categoryOutOfPolicyViolation = { name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, @@ -603,203 +597,6 @@ describe('getViolationsOnyxData', () => { expect(result.value).toEqual(expect.arrayContaining([missingDepartmentTag, missingRegionTag, missingProjectTag])); }); }); - - describe('missingAttendees violation', () => { - const missingAttendeesViolation = { - name: CONST.VIOLATIONS.MISSING_ATTENDEES, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }; - - const ownerAccountID = 123; - const otherAccountID = 456; - - let iouReport: Report; - - beforeEach(() => { - policy.type = CONST.POLICY.TYPE.CORPORATE; - policy.isAttendeeTrackingEnabled = true; - policyCategories = { - Meals: { - name: 'Meals', - enabled: true, - areAttendeesRequired: true, - }, - }; - transaction.category = 'Meals'; - iouReport = { - reportID: '1234', - ownerAccountID, - } as Report; - }); - - it('should add missingAttendees violation when no attendees are present', () => { - transaction.comment = {attendees: []}; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should add missingAttendees violation when only owner is an attendee', () => { - transaction.comment = { - attendees: [{email: 'owner@example.com', displayName: 'Owner', avatarUrl: '', accountID: ownerAccountID}], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should not add missingAttendees violation when there is at least one non-owner attendee', () => { - transaction.comment = { - attendees: [ - {email: 'owner@example.com', displayName: 'Owner', avatarUrl: '', accountID: ownerAccountID}, - {email: 'other@example.com', displayName: 'Other', avatarUrl: '', accountID: otherAccountID}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should remove missingAttendees violation when attendees are added', () => { - transactionViolations = [missingAttendeesViolation]; - transaction.comment = { - attendees: [ - {email: 'owner@example.com', displayName: 'Owner', avatarUrl: '', accountID: ownerAccountID}, - {email: 'other@example.com', displayName: 'Other', avatarUrl: '', accountID: otherAccountID}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should not add missingAttendees violation when attendee tracking is disabled', () => { - policy.isAttendeeTrackingEnabled = false; - transaction.comment = {attendees: []}; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should not add missingAttendees violation when category does not require attendees', () => { - policyCategories.Meals.areAttendeesRequired = false; - transaction.comment = {attendees: []}; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, iouReport); - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - describe('optimistic / offline scenarios (iouReport is undefined)', () => { - // In offline scenarios, iouReport is undefined so we can't get ownerAccountID. - // The code falls back to using getCurrentUserEmail() to identify the owner by login/email. - it('should correctly calculate violation when iouReport is undefined but attendees have matching email', () => { - // When iouReport is undefined, we use getCurrentUserEmail() as fallback - // If only the current user (matching MOCK_CURRENT_USER_EMAIL) is an attendee, violation should show - transactionViolations = []; - transaction.comment = { - attendees: [{email: MOCK_CURRENT_USER_EMAIL, displayName: 'Test User', avatarUrl: ''}], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // Violation should be added since the only attendee is the current user (owner) - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should not add violation when iouReport is undefined but there are non-owner attendees (by email)', () => { - // When there are attendees with different emails than the current user, no violation - transactionViolations = []; - transaction.comment = { - attendees: [ - {email: MOCK_CURRENT_USER_EMAIL, displayName: 'Test User', avatarUrl: ''}, - {email: 'other@example.com', displayName: 'Other User', avatarUrl: ''}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // Violation should NOT be added since there's a non-owner attendee - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should remove violation when non-owner attendee is added (offline)', () => { - // If violation existed and a non-owner attendee is added, violation should be removed - transactionViolations = [missingAttendeesViolation]; - transaction.comment = { - attendees: [ - {email: MOCK_CURRENT_USER_EMAIL, displayName: 'Test User', avatarUrl: ''}, - {email: 'other@example.com', displayName: 'Other User', avatarUrl: ''}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // Violation should be removed - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should preserve violation when only owner attendee remains (offline)', () => { - // If violation existed and only owner attendee remains, violation stays - transactionViolations = [missingAttendeesViolation]; - transaction.comment = { - attendees: [{email: MOCK_CURRENT_USER_EMAIL, displayName: 'Test User', avatarUrl: ''}], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // Violation should be preserved - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - }); - - describe('fallback case (iouReport undefined AND getCurrentUserEmail returns falsy)', () => { - // This tests the edge case where we cannot identify the owner at all: - // - ownerAccountID is undefined (iouReport unavailable) - // - getCurrentUserEmail() returns falsy (no current user email) - // In this case, we assume owner is one of the attendees, so we need at least 2 attendees - // for there to be a non-owner attendee. - - beforeEach(() => { - // Mock getCurrentUserEmail to return empty string - jest.spyOn(require('@libs/actions/Report'), 'getCurrentUserEmail').mockReturnValue(''); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it("should add missingAttendees violation when no attendees are present (can't identify owner)", () => { - transactionViolations = []; - transaction.comment = {attendees: []}; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // With 0 attendees, attendeesMinusOwnerCount = Math.max(0, 0 - 1) = 0, violation should be added - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should add missingAttendees violation when only 1 attendee exists (assumed to be owner)', () => { - transactionViolations = []; - transaction.comment = { - attendees: [{email: 'anyone@example.com', displayName: 'Someone', avatarUrl: ''}], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // With 1 attendee, attendeesMinusOwnerCount = Math.max(0, 1 - 1) = 0, violation should be added - expect(result.value).toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should not add missingAttendees violation when 2+ attendees exist (assumes owner is one of them)', () => { - transactionViolations = []; - transaction.comment = { - attendees: [ - {email: 'person1@example.com', displayName: 'Person 1', avatarUrl: ''}, - {email: 'person2@example.com', displayName: 'Person 2', avatarUrl: ''}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // With 2 attendees, attendeesMinusOwnerCount = Math.max(0, 2 - 1) = 1, no violation - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - - it('should remove missingAttendees violation when second attendee is added', () => { - transactionViolations = [missingAttendeesViolation]; - transaction.comment = { - attendees: [ - {email: 'person1@example.com', displayName: 'Person 1', avatarUrl: ''}, - {email: 'person2@example.com', displayName: 'Person 2', avatarUrl: ''}, - ], - }; - const result = ViolationsUtils.getViolationsOnyxData(transaction, transactionViolations, policy, policyTags, policyCategories, false, false, false, undefined); - // Violation should be removed since we now have 2 attendees - expect(result.value).not.toEqual(expect.arrayContaining([missingAttendeesViolation])); - }); - }); - }); }); const getFakeTransaction = (transactionID: string, comment?: Transaction['comment']) => ({