From 0afd6ce0867f8683cd8f8736d2318ea909af7280 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Fri, 9 Oct 2026 07:49:27 -0600 Subject: [PATCH] Revert "Add imported vendor enable and disable controls" --- .../Tables/WorkspaceVendorsTable/index.tsx | 92 ++----- src/languages/de.ts | 5 - src/languages/el.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 | 12 +- .../SetPolicyVendorsEnabledParams.ts | 11 - src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 - src/libs/PolicyUtils/vendor.ts | 53 +---- src/libs/Violations/ViolationsUtils.ts | 10 +- src/libs/actions/IOU/UpdateMoneyRequest.ts | 6 - src/libs/actions/Policy/Vendor.ts | 113 --------- .../iou/request/step/IOURequestStepVendor.tsx | 62 ++--- .../vendors/WorkspaceVendorsPage.tsx | 225 ++---------------- src/types/onyx/PolicyVendor.ts | 36 ++- src/types/onyx/index.ts | 2 - .../actions/IOUTest/UpdateMoneyRequestTest.ts | 183 +------------- tests/ui/MoneyRequestViewTest.tsx | 77 +----- tests/unit/PolicyUtilsTest.ts | 213 ----------------- tests/unit/PolicyVendorActionTest.ts | 162 ------------- tests/unit/ViolationUtilsTest.ts | 108 --------- 28 files changed, 82 insertions(+), 1336 deletions(-) delete mode 100644 src/libs/API/parameters/SetPolicyVendorsEnabledParams.ts delete mode 100644 src/libs/actions/Policy/Vendor.ts delete mode 100644 tests/unit/PolicyVendorActionTest.ts diff --git a/src/components/Tables/WorkspaceVendorsTable/index.tsx b/src/components/Tables/WorkspaceVendorsTable/index.tsx index 6cf3a2002350..7f3cd7444099 100644 --- a/src/components/Tables/WorkspaceVendorsTable/index.tsx +++ b/src/components/Tables/WorkspaceVendorsTable/index.tsx @@ -1,4 +1,3 @@ -import Switch from '@components/Switch'; import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData} from '@components/Table'; import Table, {composeTableListHeader} from '@components/Table'; import TextWithTooltip from '@components/TextWithTooltip'; @@ -8,47 +7,28 @@ import useThemeStyles from '@hooks/useThemeStyles'; import tokenizedSearch from '@libs/tokenizedSearch'; -import variables from '@styles/variables'; - import CONST from '@src/CONST'; -import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {ListRenderItemInfo} from '@shopify/flash-list'; import React from 'react'; import {View} from 'react-native'; -type WorkspaceVendorTableColumnKey = 'name' | 'enabled'; +type WorkspaceVendorTableColumnKey = 'name'; type WorkspaceVendorTableRowData = TableData & { name: string; - enabled: boolean; - disabled: boolean; - isLocked: boolean; - errors?: OnyxCommon.Errors; - pendingAction?: OnyxCommon.PendingAction; - onToggleEnabled: (enabled: boolean) => void; - dismissError: () => void; }; type WorkspaceVendorsTableProps = { /** Vendor rows to render */ vendors: WorkspaceVendorTableRowData[]; - /** Whether row selection is enabled */ - selectionEnabled?: boolean; - - /** Currently selected vendor keys */ - selectedKeys?: string[]; - - /** Callback fired when row selection changes */ - onRowSelectionChange?: (selectedKeys: string[]) => void; - /** Page-level content rendered above the table header inside the scrollable list */ headerComponent?: React.ReactElement; }; -function WorkspaceVendorsTable({vendors, selectionEnabled = false, selectedKeys = [], onRowSelectionChange, headerComponent}: WorkspaceVendorsTableProps) { +function WorkspaceVendorsTable({vendors, headerComponent}: WorkspaceVendorsTableProps) { const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); @@ -58,24 +38,10 @@ function WorkspaceVendorsTable({vendors, selectionEnabled = false, selectedKeys label: translate('common.name'), sortable: true, }, - { - key: 'enabled', - label: translate('common.enabled'), - sortable: true, - width: variables.tableSwitchColumnWidth, - styling: { - containerStyles: [styles.justifyContentEnd], - }, - }, ]; const compareItems: CompareItemsCallback = (item1, item2, activeSorting) => { const orderMultiplier = activeSorting.order === 'asc' ? 1 : -1; - if (activeSorting.columnKey === 'enabled') { - const enabled1 = item1.enabled ? 1 : 0; - const enabled2 = item2.enabled ? 1 : 0; - return (enabled1 - enabled2) * orderMultiplier; - } return localeCompare(item1.name, item2.name) * orderMultiplier; }; @@ -84,41 +50,22 @@ function WorkspaceVendorsTable({vendors, selectionEnabled = false, selectedKeys return results.length > 0; }; - const renderVendorItem = ({item, index}: ListRenderItemInfo) => { - const accessibilityLabel = [item.name, item.enabled ? translate('common.enabled') : translate('common.disabled')].filter(Boolean).join(', '); - - return ( - - - - - - - - - ); - }; + const renderVendorItem = ({item, index}: ListRenderItemInfo) => ( + + + + + + ); const tableHeaderComponent = composeTableListHeader(headerComponent, ); @@ -132,9 +79,6 @@ function WorkspaceVendorsTable({vendors, selectionEnabled = false, selectedKeys isItemInSearch={isItemInSearch} renderItem={renderVendorItem} keyExtractor={(item) => item.keyForList} - selectionEnabled={selectionEnabled} - selectedKeys={selectedKeys} - onRowSelectionChange={onRowSelectionChange} > {tableHeaderComponent} ): Vendor[] { /** * Sorts vendors alphabetically by name using the provided localeCompare. - * Uses vendor id or externalID as a stable tie-breaker when names match. + * Uses vendor id as a stable tie-breaker when names match. * Non-mutating: returns a new sorted array. */ -function sortVendors(vendors: TVendor[], localeCompare: LocaleContextProps['localeCompare']): TVendor[] { +function sortVendors(vendors: TVendor[], localeCompare: LocaleContextProps['localeCompare']): TVendor[] { return [...vendors].sort((a, b) => { const nameComparison = localeCompare(a.name ?? '', b.name ?? ''); if (nameComparison !== 0) { return nameComparison; } - const keyA = a.id ?? a.externalID; - const keyB = b.id ?? b.externalID; - if (!keyA || !keyB) { - return 0; - } - return localeCompare(keyA, keyB); + return localeCompare(a.id, b.id); }); } -/** - * Resolves the configured default vendor ID for the active integration that supports vendor enablement. - * Returns undefined if no default vendor is configured or if the integration does not have an active default fallback. - */ -function getDefaultVendorID(policy: OnyxEntry, origin?: ConnectionName): string | undefined { - const integrationOrigin = origin ?? getActiveVendorMatchingIntegration(policy); - if (!policy?.connections || !integrationOrigin) { - return undefined; - } - - if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.QBO) { - const qboConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.QBO]?.config; - if (qboConfig?.nonReimbursableExpensesExportDestination === CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD) { - return qboConfig.nonReimbursableCreditCardDefaultVendor ?? undefined; - } - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT) { - const intacctConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]?.config; - return intacctConfig?.export?.nonReimbursableCreditCardChargeDefaultVendor ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.XERO) { - const xeroConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.XERO]?.config; - return xeroConfig?.defaultVendor ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.RILLET) { - const rilletConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.RILLET]?.config; - return rilletConfig?.export?.defaultVendorID ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.DUALENTRY) { - const dualentryConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.DUALENTRY]?.config; - return dualentryConfig?.export?.defaultVendorID ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE) { - const campfireConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE]?.config; - return campfireConfig?.export?.defaultVendorID ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL) { - const businessCentralConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL]?.config; - return businessCentralConfig?.export?.defaultVendorID ?? undefined; - } else if (integrationOrigin === CONST.POLICY.CONNECTIONS.NAME.CERTINIA) { - const certiniaConfig = policy.connections[CONST.POLICY.CONNECTIONS.NAME.CERTINIA]?.config; - return certiniaConfig?.export?.vendorAccount ?? undefined; - } - - return undefined; -} - /** * True only when the active vendor-matching integration's vendor list has been written to Onyx — * including the loaded-but-empty case. Lets callers distinguish "vendor not in list" (the @@ -563,7 +517,6 @@ export { getActiveVendorMatchingIntegration, getMatchingVendorByID, getMatchingVendors, - getDefaultVendorID, sortVendors, getVendorEmptyState, getVendorRuleDisplayValue, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index e6be6cadf952..5ff8303c087a 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -688,7 +688,6 @@ const ViolationsUtils = { distanceOriginalPolicy, ownerLogin: ownerLoginParam, isVendorMatchingBetaEnabled, - policyVendors: policyVendorsParam, }: { updatedTransaction: Transaction; transactionViolations: TransactionViolation[]; @@ -705,7 +704,6 @@ const ViolationsUtils = { ownerLogin: string | undefined; /** Undefined while the account betas are still loading, which leaves the inactive vendor violation untouched */ isVendorMatchingBetaEnabled: boolean | undefined; - policyVendors?: OnyxEntry; }): OnyxUpdate { const isScanning = TransactionUtils.isScanning(updatedTransaction); const isScanRequest = TransactionUtils.isScanRequest(updatedTransaction); @@ -814,23 +812,21 @@ const ViolationsUtils = { // positive in Onyx, and rejecting it would strip a legitimate one. Leave the // existing violation state untouched until the list arrives. const matchedVendor = getMatchingVendorByID(policy, transactionVendorID); - const isVendorDisabled = policyVendorsParam?.[transactionVendorID]?.enabled === false; - const isVendorInactive = !matchedVendor || isVendorDisabled; // Stamp Xero-specific copy on the violation so the render site can use the // "Supplier" wording the rest of the Xero UI uses; QBO/Intacct keep the default // "Vendor" wording. const isSupplierViolation = isXeroActiveMatchingSource(policy); - if (isVendorInactive && !hasInactiveVendorViolation) { + if (!matchedVendor && !hasInactiveVendorViolation) { newTransactionViolations.push({ name: CONST.VIOLATIONS.INACTIVE_VENDOR, type: CONST.VIOLATION_TYPES.VIOLATION, showInReview: true, ...(isSupplierViolation ? {data: {isSupplierViolation: true}} : {}), }); - } else if (!isVendorInactive && hasInactiveVendorViolation) { + } else if (matchedVendor && hasInactiveVendorViolation) { newTransactionViolations = reject(newTransactionViolations, {name: CONST.VIOLATIONS.INACTIVE_VENDOR}); - } else if (isVendorInactive && hasInactiveVendorViolation) { + } else if (!matchedVendor && hasInactiveVendorViolation) { // Reconcile data.isSupplierViolation with the current active matching source. // Backfills the flag when Xero is now active (server-fired violation, or // persisted from before this code path existed). Strips a stale flag when the diff --git a/src/libs/actions/IOU/UpdateMoneyRequest.ts b/src/libs/actions/IOU/UpdateMoneyRequest.ts index f2a19294be74..81152ff8065a 100644 --- a/src/libs/actions/IOU/UpdateMoneyRequest.ts +++ b/src/libs/actions/IOU/UpdateMoneyRequest.ts @@ -1456,7 +1456,6 @@ function updateMoneyRequestDescription({ getCurrencySymbol, rules, isVendorMatchingBetaEnabled, - policyVendors, }: { isVendorMatchingBetaEnabled: boolean | undefined; transactionID: string; @@ -1470,7 +1469,6 @@ function updateMoneyRequestDescription({ policy: OnyxEntry; policyTagList: OnyxEntry; policyCategories: OnyxEntry; - policyVendors?: OnyxEntry; currentUserAccountIDParam: number; currentUserEmailParam: string; isASAPSubmitBetaEnabled: boolean; @@ -1513,7 +1511,6 @@ function updateMoneyRequestDescription({ policyTagList, reportPolicyTags, policyCategories, - policyVendors, currentUserAccountIDParam, currentUserEmailParam, isASAPSubmitBetaEnabled, @@ -1833,7 +1830,6 @@ type GetUpdateMoneyRequestParamsType = { getCurrencySymbol: CurrencyListActionsContextType['getCurrencySymbol']; rules: OnyxCollection; isVendorMatchingBetaEnabled: boolean | undefined; - policyVendors?: OnyxEntry; }; type UpdateMoneyRequestDataKeys = @@ -1887,7 +1883,6 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U getCurrencySymbol, rules, isVendorMatchingBetaEnabled, - policyVendors: policyVendorsParam, } = params; const optimisticData: Array< OnyxUpdate< @@ -2412,7 +2407,6 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U isFromExpenseReport, distanceOriginalPolicy, isVendorMatchingBetaEnabled, - policyVendors: policyVendorsParam, }); optimisticData.push(violationsOnyxData); failureData.push({ diff --git a/src/libs/actions/Policy/Vendor.ts b/src/libs/actions/Policy/Vendor.ts deleted file mode 100644 index 972bc219efdc..000000000000 --- a/src/libs/actions/Policy/Vendor.ts +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Actions for enabling/disabling policy vendors and clearing their offline errors. - */ -import {write} from '@libs/API'; -import type {SetPolicyVendorsEnabledParams} from '@libs/API/parameters'; -import {WRITE_COMMANDS} from '@libs/API/types'; -import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyVendor, PolicyVendors} from '@src/types/onyx'; -import type {OnyxData} from '@src/types/onyx/Request'; - -import type {OnyxEntry} from 'react-native-onyx'; - -import Onyx from 'react-native-onyx'; - -type SetPolicyVendorsEnabledArgs = { - policyID: string; - vendorIDs: string[]; - enabled: boolean; - policyVendors?: OnyxEntry; -}; - -function setPolicyVendorsEnabled({policyID, vendorIDs, enabled, policyVendors}: SetPolicyVendorsEnabledArgs) { - if (!vendorIDs.length) { - return; - } - - const optimisticVendors: Record> = {}; - const successVendors: Record> = {}; - const failureVendors: Record> = {}; - - for (const vendorID of vendorIDs) { - const existing = policyVendors?.[vendorID]; - optimisticVendors[vendorID] = { - ...existing, - enabled, - pendingFields: { - ...existing?.pendingFields, - enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - pendingAction: existing?.pendingAction ?? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - errors: null, - }; - - successVendors[vendorID] = { - ...existing, - enabled, - pendingFields: { - ...existing?.pendingFields, - enabled: null, - }, - pendingAction: null, - errors: null, - }; - - failureVendors[vendorID] = { - ...existing, - pendingFields: { - ...existing?.pendingFields, - enabled: null, - }, - pendingAction: null, - errors: getMicroSecondOnyxErrorWithTranslationKey('workspace.vendors.updateFailureMessage'), - }; - } - - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, - value: optimisticVendors, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, - value: successVendors, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, - value: failureVendors, - }, - ], - }; - - const parameters: SetPolicyVendorsEnabledParams = { - policyID, - vendorIDs: JSON.stringify(vendorIDs), - enabled, - }; - - write(WRITE_COMMANDS.SET_POLICY_VENDORS_ENABLED, parameters, onyxData); -} - -function clearVendorErrors(policyID: string, vendorID: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, { - [vendorID]: { - errors: null, - pendingAction: null, - pendingFields: null, - errorFields: null, - }, - }); -} - -export {setPolicyVendorsEnabled, clearVendorErrors}; diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 08b637143124..af98674fe953 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -15,7 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {updateMoneyRequestVendor} from '@libs/actions/IOU/UpdateMoneyRequest'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; -import {getVendorEmptyState, hasVendorFeature, isXeroActiveMatchingSource, sortVendors} from '@libs/PolicyUtils'; +import {getMatchingVendors, getVendorEmptyState, hasVendorFeature, isXeroActiveMatchingSource, sortVendors} from '@libs/PolicyUtils'; import {isPerDiemRequest} from '@libs/TransactionUtils'; import variables from '@styles/variables'; @@ -61,7 +61,6 @@ function IOURequestStepVendor({ }); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`); const [transactionViolations] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${getNonEmptyStringOnyxID(transactionID)}`); - const [policyVendors] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policy?.id}`); const delegateAccountID = useDelegateAccountID(); const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); @@ -72,55 +71,36 @@ function IOURequestStepVendor({ // Vendor is scoped to non-reimbursable expenses on a policy expense chat; block deep-link / stale-open access if the transaction is reimbursable or is an invoice (invoices are non-reimbursable but don't route through the vendor-matching flow). const isReimbursable = !!transaction?.reimbursable; const isInvoice = iouType === CONST.IOU.TYPE.INVOICE; - + const vendors = getMatchingVendors(policy); + const sortedVendors = sortVendors(vendors, localeCompare); const currentVendorID = transaction?.comment?.vendor?.externalID; - const currentVendor = currentVendorID ? policyVendors?.[currentVendorID] : undefined; - const isCurrentVendorDisabled = !!currentVendorID && !currentVendor?.enabled; - - const enabledVendors = Object.values(policyVendors ?? {}).filter((vendor) => vendor.enabled); - const sortedEnabledVendors = sortVendors(enabledVendors, localeCompare); const vendorLabel = isOnXero ? translate('common.supplier') : translate('common.vendor'); const trimmedSearch = searchValue.trim().toLowerCase(); - const enabledRows: VendorListItem[] = sortedEnabledVendors + const vendorRows: VendorListItem[] = sortedVendors .filter((vendor) => !trimmedSearch || vendor.name.toLowerCase().includes(trimmedSearch)) .map((vendor) => ({ - value: vendor.externalID, + value: vendor.id, text: vendor.name, - keyForList: vendor.externalID, - isSelected: vendor.externalID === currentVendorID, + keyForList: vendor.id, + isSelected: vendor.id === currentVendorID, searchText: vendor.name, })); - const disabledCurrentVendorRow: VendorListItem | undefined = - !trimmedSearch && isCurrentVendorDisabled - ? { - value: currentVendorID, - text: currentVendor?.name ?? transaction?.comment?.vendor?.name ?? '', - keyForList: currentVendorID, - isSelected: true, - searchText: currentVendor?.name ?? transaction?.comment?.vendor?.name ?? '', - alternateText: translate('common.disabled'), - } - : undefined; - + // When a vendor is currently set, offer a "None" row so the user can clear a stale (e.g. removed from the accounting integration) vendor without picking a replacement, which resolves an inactiveVendor violation. Hidden during search to keep results clean. const shouldShowNoneRow = !!currentVendorID && !trimmedSearch; - - const data: VendorListItem[] = [ - ...(shouldShowNoneRow - ? [ - { - value: '', - text: translate('common.none'), - keyForList: 'clear-vendor', - isSelected: false, - searchText: '', - }, - ] - : []), - ...(disabledCurrentVendorRow ? [disabledCurrentVendorRow] : []), - ...enabledRows, - ]; + const data: VendorListItem[] = shouldShowNoneRow + ? [ + { + value: '', + text: translate('common.none'), + keyForList: 'clear-vendor', + isSelected: false, + searchText: '', + }, + ...vendorRows, + ] + : vendorRows; const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, report, transaction) || !isFeatureAvailable || isReimbursable || isInvoice; @@ -150,7 +130,7 @@ function IOURequestStepVendor({ const headerMessage = searchValue && data.length === 0 ? translate('common.noResultsFound') : ''; const listEmptyContent = - enabledVendors.length === 0 && !disabledCurrentVendorRow ? ( + vendors.length === 0 ? ( ; - defaultVendorID?: string; - onClearSelection: () => void; -}; - -function VendorBulkActions({policyID, selectedVendorKeys, policyVendors, defaultVendorID, onClearSelection}: VendorBulkActionsProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); - const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close']); - - const options: Array>> = []; - const disabledVendors = selectedVendorKeys.filter((id) => !policyVendors?.[id]?.enabled); - if (disabledVendors.length > 0) { - options.push({ - icon: icons.Checkmark, - text: translate(disabledVendors.length === 1 ? 'workspace.vendors.enableVendor' : 'workspace.vendors.enableVendors'), - value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, - onSelected: () => { - onClearSelection(); - setPolicyVendorsEnabled({ - policyID, - vendorIDs: disabledVendors, - enabled: true, - policyVendors, - }); - }, - }); - } - - const vendorsToDisable = selectedVendorKeys.filter((id) => policyVendors?.[id]?.enabled && id !== defaultVendorID); - if (vendorsToDisable.length > 0) { - options.push({ - icon: icons.Close, - text: translate(vendorsToDisable.length === 1 ? 'workspace.vendors.disableVendor' : 'workspace.vendors.disableVendors'), - value: CONST.POLICY.BULK_ACTION_TYPES.DISABLE, - onSelected: () => { - onClearSelection(); - setPolicyVendorsEnabled({ - policyID, - vendorIDs: vendorsToDisable, - enabled: false, - policyVendors, - }); - }, - }); - } - - if (options.length === 0) { - return null; - } - - return ( - null} - shouldAlwaysShowDropdownMenu - size={CONST.BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedVendorKeys.length})} - options={options} - isSplitButton={false} - style={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} - isDisabled={!selectedVendorKeys.length} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.INITIAL.VENDORS} - testID="WorkspaceVendorsPage-header-dropdown-menu-button" - /> - ); -} - -VendorBulkActions.displayName = 'VendorBulkActions'; - type WorkspaceVendorsPageProps = WithPolicyConnectionsProps & PlatformStackScreenProps; function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { @@ -126,71 +33,24 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { const {translate, localeCompare} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isBetaEnabled} = usePermissions(); - const {isOffline} = useNetwork(); - const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); - const isMobileSelectionModeEnabled = useMobileSelectionMode(); - const {canWrite: canWriteVendors, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.VENDORS); - - const [policyVendors] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`); - const [selectedVendorKeys, setSelectedVendorKeys] = useFilteredSelection(policyVendors, (vendor) => !!vendor); useWorkspaceDocumentTitle(policy?.name, 'workspace.common.vendors'); const isVendorMatchingBetaEnabled = isBetaEnabled(CONST.BETAS.VENDOR_MATCHING); const isFeatureAvailable = hasVendorFeature(policy, isVendorMatchingBetaEnabled); + const vendors = getMatchingVendors(policy); + const sortedVendors = sortVendors(vendors, localeCompare); const connectedIntegration = getActiveVendorMatchingIntegration(policy); const currentConnectionName = connectedIntegration ? CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectedIntegration] : undefined; - const defaultVendorID = getDefaultVendorID(policy, connectedIntegration); - const canSelectMultiple = canWriteVendors && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true); - - const clearTableSelection = () => { - setSelectedVendorKeys((prev) => (prev.length > 0 ? [] : prev)); - }; - - useCleanupSelectedOptions(clearTableSelection); - - useSearchBackPress({ - onClearSelection: clearTableSelection, - onNavigationCallBack: () => Navigation.goBack(), - }); - - const handleVendorToggle = (enabled: boolean, vendorID: string) => { - if (!canWriteVendors) { - showReadOnlyModal(); - return; - } - - if (!enabled && vendorID === defaultVendorID) { - return; - } - - setPolicyVendorsEnabled({ - policyID, - vendorIDs: [vendorID], - enabled, - policyVendors, - }); - }; - - const sortedVendors = sortVendors(Object.values(policyVendors ?? {}), localeCompare); - - const vendorRows: WorkspaceVendorTableRowData[] = sortedVendors.map((vendor) => { - const isDefaultVendor = vendor.externalID === defaultVendorID; - const isLocked = (vendor.enabled && isDefaultVendor) || !canWriteVendors; - - return { - keyForList: vendor.externalID, - name: vendor.name, - enabled: vendor.enabled, - disabled: !canWriteVendors, - isLocked, - errors: vendor.errors ?? undefined, - pendingAction: vendor.pendingAction, - onToggleEnabled: (enabled: boolean) => handleVendorToggle(enabled, vendor.externalID), - dismissError: () => clearVendorErrors(policyID, vendor.externalID), - }; - }); + const vendorRows: WorkspaceVendorTableRowData[] = useMemo( + () => + sortedVendors.map((vendor) => ({ + keyForList: vendor.id, + name: vendor.name, + })), + [sortedVendors], + ); const headerContent = currentConnectionName ? ( @@ -203,10 +63,6 @@ function WorkspaceVendorsPage({policy, route}: WorkspaceVendorsPageProps) { ) : undefined; - const canShowBulkActions = canWriteVendors && selectedVendorKeys.length > 0; - const isLoading = !isOffline && policyVendors === undefined; - const selectionModeHeader = isMobileSelectionModeEnabled && shouldUseNarrowLayout; - return ( { - if (isMobileSelectionModeEnabled) { - clearTableSelection(); - turnOffMobileSelectionMode(); - return; - } - Navigation.goBack(); - }} - > - {!shouldDisplayButtonsInSeparateLine && canShowBulkActions && ( - - )} - - {shouldDisplayButtonsInSeparateLine && canShowBulkActions && ( - - - - )} - - {isLoading && ( - - )} - - {!isLoading && ( - - )} + title={translate('workspace.common.vendors')} + onBackButtonPress={() => Navigation.goBack()} + /> + ); diff --git a/src/types/onyx/PolicyVendor.ts b/src/types/onyx/PolicyVendor.ts index 65cbfbcd2238..913978785477 100644 --- a/src/types/onyx/PolicyVendor.ts +++ b/src/types/onyx/PolicyVendor.ts @@ -1,29 +1,21 @@ -import type {ErrorFields, Errors, OnyxValueWithOfflineFeedback} from './OnyxCommon'; import type {PolicyConnectionName} from './Policy'; -/** Model of policy vendor */ -type PolicyVendor = OnyxValueWithOfflineFeedback<{ - /** The vendor identifier scoped to its active accounting connection. */ - externalID: string; - - /** The accounting-system display name. */ - name: string; - - /** Whether the policy preference permits this vendor to be selected. */ - enabled: boolean; - - /** The active accounting connection that supplied this vendor. */ - origin?: PolicyConnectionName; +/** Record of normalized policy vendors, indexed by externalID. */ +type PolicyVendors = Record< + string, + { + /** The vendor identifier scoped to its active accounting connection. */ + externalID: string; - /** A list of errors keyed by microtime */ - errors?: Errors | null; + /** The accounting-system display name. */ + name: string; - /** Error objects keyed by field name containing errors keyed by microtime */ - errorFields?: ErrorFields | null; -}>; + /** Whether the policy preference permits this vendor to be selected. */ + enabled: boolean; -/** Record of normalized policy vendors, indexed by externalID. */ -type PolicyVendors = Record; + /** The active accounting connection that supplied this vendor. */ + origin?: PolicyConnectionName; + } +>; export default PolicyVendors; -export type {PolicyVendor}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 55ecf1ec316e..e9271054cba9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -151,7 +151,6 @@ import type PolicyOwnershipChangeChecks from './PolicyOwnershipChangeChecks'; import type PolicyRoomsMetadata from './PolicyRoomsMetadata'; import type {ParticipantsPolicyTags, PolicyTag, PolicyTagLists, PolicyTags, SearchTagFilterItem} from './PolicyTag'; import type PolicyVendors from './PolicyVendor'; -import type {PolicyVendor} from './PolicyVendor'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type PrivatePromoDiscount from './PrivatePromoDiscount'; import type PrivateSubscription from './PrivateSubscription'; @@ -333,7 +332,6 @@ export type { PolicyCategories, PolicyCategory, PolicyDataLoadingState, - PolicyVendor, PolicyVendors, PolicyEmployee, PolicyEmployeeList, diff --git a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts index af3d8e0afb69..148aaec1ae3b 100644 --- a/tests/actions/IOUTest/UpdateMoneyRequestTest.ts +++ b/tests/actions/IOUTest/UpdateMoneyRequestTest.ts @@ -24,10 +24,9 @@ import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyTagLists, PolicyVendors, RecentlyUsedTags, RecentWaypoint, Report, SearchResults, TransactionViolation} from '@src/types/onyx'; +import type {Policy, PolicyTagLists, RecentlyUsedTags, RecentWaypoint, Report, SearchResults, TransactionViolation} from '@src/types/onyx'; import type {Attendee} from '@src/types/onyx/IOU'; import type {CurrentUserPersonalDetails} from '@src/types/onyx/PersonalDetails'; -import type {Connections} from '@src/types/onyx/Policy'; import type {Routes} from '@src/types/onyx/Transaction'; import type Transaction from '@src/types/onyx/Transaction'; @@ -48,10 +47,6 @@ import getOnyxValue from '../../utils/getOnyxValue'; import {createGlobalFetchMock, getCurrencyDecimalsLocal, getCurrencySymbolLocal} from '../../utils/TestHelper'; import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; -jest.mock('@expensify/react-native-hybrid-app', () => ({ - isHybridApp: jest.fn(() => false), -})); - const topMostReportID = '23423423'; jest.mock('@src/libs/Navigation/Navigation', () => ({ navigate: jest.fn(), @@ -3132,180 +3127,4 @@ describe('actions/IOU/UpdateMoneyRequest', () => { expect(modifiedExpenseAction?.delegateAccountID).toBe(DELEGATE_ACCOUNT_ID); }); }); - - describe('vendor matching inactiveVendor violation retention', () => { - it('retains inactiveVendor violation during optimistic expense update when assigned vendor is disabled in policyVendors', async () => { - const transactionID = 'txnVendorDisabled1'; - const transactionThreadReportID = 'threadVendorDisabled1'; - const parentReportID = 'parentVendorDisabled1'; - const policyID = '50'; - const vendorID = 'v-disabled'; - - const policy = createMock({ - ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: { - config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: vendorID, name: 'Acme Disabled', currency: 'USD', email: 'vendor@example.com'}]}, - }, - }), - }); - - const parentReport: Report = { - ...createRandomReport(1, undefined), - reportID: parentReportID, - type: CONST.REPORT.TYPE.EXPENSE, - policyID, - ownerAccountID: RORY_ACCOUNT_ID, - }; - const transactionThreadReport: Report = { - ...createRandomReport(2, undefined), - reportID: transactionThreadReportID, - parentReportID, - type: CONST.REPORT.TYPE.CHAT, - }; - const fakeTransaction: Transaction = { - ...createRandomTransaction(3), - transactionID, - reportID: parentReportID, - category: '', - reimbursable: false, - comment: {vendor: {externalID: vendorID, wasManuallySet: false}}, - }; - const inactiveVendorViolation: TransactionViolation = { - name: CONST.VIOLATIONS.INACTIVE_VENDOR, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }; - - const policyVendors: PolicyVendors = { - [vendorID]: { - externalID: vendorID, - name: 'Acme Disabled', - enabled: false, - }, - }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, parentReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, transactionThreadReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, policyVendors); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [inactiveVendorViolation]); - - updateMoneyRequestDescription({ - isVendorMatchingBetaEnabled: true, - transactionID, - transactionThreadReport, - parentReport, - rules: undefined, - iouReportOwnerLogin: undefined, - comment: 'Updated description', - policy, - policyTagList: undefined, - policyCategories: undefined, - policyVendors, - reportPolicyTags: undefined, - currentUserAccountIDParam: RORY_ACCOUNT_ID, - currentUserEmailParam: RORY_EMAIL, - isASAPSubmitBetaEnabled: false, - delegateAccountID: undefined, - isTrackIntentUser: false, - violations: [inactiveVendorViolation], - getCurrencyDecimals: getCurrencyDecimalsLocal, - getCurrencySymbol: getCurrencySymbolLocal, - }); - - await waitForBatchedUpdates(); - - expect(await getStoredViolationNames(transactionID)).toEqual([CONST.VIOLATIONS.INACTIVE_VENDOR]); - }); - - it('clears inactiveVendor violation during optimistic expense update when assigned vendor is re-enabled in policyVendors', async () => { - const transactionID = 'txnVendorEnabled1'; - const transactionThreadReportID = 'threadVendorEnabled1'; - const parentReportID = 'parentVendorEnabled1'; - const policyID = '51'; - const vendorID = 'v-enabled'; - - const policy = createMock({ - ...createRandomPolicy(Number(policyID), CONST.POLICY.TYPE.TEAM), - requiresCategory: false, - requiresTag: false, - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: { - config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: vendorID, name: 'Acme Enabled', currency: 'USD', email: 'vendor@example.com'}]}, - }, - }), - }); - - const parentReport: Report = { - ...createRandomReport(1, undefined), - reportID: parentReportID, - type: CONST.REPORT.TYPE.EXPENSE, - policyID, - ownerAccountID: RORY_ACCOUNT_ID, - }; - const transactionThreadReport: Report = { - ...createRandomReport(2, undefined), - reportID: transactionThreadReportID, - parentReportID, - type: CONST.REPORT.TYPE.CHAT, - }; - const fakeTransaction: Transaction = { - ...createRandomTransaction(4), - transactionID, - reportID: parentReportID, - category: '', - reimbursable: false, - comment: {vendor: {externalID: vendorID, wasManuallySet: false}}, - }; - const inactiveVendorViolation: TransactionViolation = { - name: CONST.VIOLATIONS.INACTIVE_VENDOR, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }; - - const policyVendors: PolicyVendors = { - [vendorID]: { - externalID: vendorID, - name: 'Acme Enabled', - enabled: true, - }, - }; - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, parentReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, transactionThreadReport); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, fakeTransaction); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); - await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, policyVendors); - await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [inactiveVendorViolation]); - - updateMoneyRequestDescription({ - isVendorMatchingBetaEnabled: true, - transactionID, - transactionThreadReport, - parentReport, - rules: undefined, - iouReportOwnerLogin: undefined, - comment: 'Updated description', - policy, - policyTagList: undefined, - policyCategories: undefined, - policyVendors, - reportPolicyTags: undefined, - currentUserAccountIDParam: RORY_ACCOUNT_ID, - currentUserEmailParam: RORY_EMAIL, - isASAPSubmitBetaEnabled: false, - delegateAccountID: undefined, - isTrackIntentUser: false, - violations: [inactiveVendorViolation], - getCurrencyDecimals: getCurrencyDecimalsLocal, - getCurrencySymbol: getCurrencySymbolLocal, - }); - - await waitForBatchedUpdates(); - - expect(await getStoredViolationNames(transactionID)).toEqual([]); - }); - }); }); diff --git a/tests/ui/MoneyRequestViewTest.tsx b/tests/ui/MoneyRequestViewTest.tsx index 9621c3343ef3..d211842d524f 100644 --- a/tests/ui/MoneyRequestViewTest.tsx +++ b/tests/ui/MoneyRequestViewTest.tsx @@ -23,10 +23,6 @@ import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -jest.mock('@expensify/react-native-hybrid-app', () => ({ - isHybridApp: jest.fn(() => false), -})); - jest.mock('@hooks/useLocalize', () => jest.fn(() => ({ translate: jest.fn((key: string) => key), @@ -63,19 +59,7 @@ jest.mock('@pages/inbox/report/AnimatedEmptyStateBackground', () => { // the menu-item testID stay strict-equal — they don't pick up the title text. jest.mock('@components/MenuItemWithTopDescription', () => { const RN = jest.requireActual>>('react-native'); - return ({ - description, - title, - interactive, - errorText, - brickRoadIndicator, - }: { - description?: string; - title?: string; - interactive?: boolean; - errorText?: string; - brickRoadIndicator?: string; - }) => ( + return ({description, title, interactive}: {description?: string; title?: string; interactive?: boolean}) => ( <> {interactive ? 'editable' : 'readonly'} @@ -85,16 +69,6 @@ jest.mock('@components/MenuItemWithTopDescription', () => { {title} )} - {errorText !== undefined && ( - - {errorText} - - )} - {brickRoadIndicator !== undefined && ( - - {brickRoadIndicator} - - )} ); }); @@ -1058,55 +1032,6 @@ describe('MoneyRequestView edit fields', () => { }); }); - it('shows the vendor-field error when assigned vendor is disabled in policyVendors', async () => { - const threadReport = { - ...LHNTestUtils.getFakeReport(), - parentReportID: expenseReportID, - parentReportActionID, - }; - - const disabledVendorID = 'disabled-vendor-id'; - - await setupTestData(); - await act(async () => { - await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.VENDOR_MATCHING]); - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}`, { - [disabledVendorID]: { - externalID: disabledVendorID, - name: 'Disabled Vendor', - enabled: false, - }, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { - reimbursable: false, - comment: {vendor: {externalID: disabledVendorID, wasManuallySet: false}}, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`, [ - { - name: CONST.VIOLATIONS.INACTIVE_VENDOR, - type: CONST.VIOLATION_TYPES.VIOLATION, - showInReview: true, - }, - ]); - }); - await waitForBatchedUpdatesWithAct(); - - renderMoneyRequestView(threadReport, { - connections: { - [CONST.POLICY.CONNECTIONS.NAME.QBO]: { - config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}, - data: {vendors: [{id: disabledVendorID, name: 'Disabled Vendor', currency: 'USD', email: 'vendor@example.com'}]}, - }, - }, - }); - await waitForBatchedUpdatesWithAct(); - - await waitFor(() => { - expect(screen.getByRole('alert')).toHaveTextContent('violations.inactiveVendor.'); - expect(screen.getByTestId('menu-item-brick-road-indicator')).toBeOnTheScreen(); - }); - }); - describe('commuter exclusion in the Distance field', () => { const selfDMReportID = 'self_dm_mrv_123'; // `translate` is mocked to return the key, so the commuter description is the plain distance label plus the "Original" key diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 9e19e1a31f40..0503f84b7f89 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -114,7 +114,6 @@ import { shouldHideDynamicExternalWorkflowPeople, shouldShowPolicy, sortPoliciesByName, - getDefaultVendorID, sortVendors, sortWorkspacesBySelected, tryNavigateToSubmitWorkspaceUpgrade, @@ -4666,218 +4665,6 @@ describe('PolicyUtils', () => { }); }); - describe('sortVendors', () => { - const localeCompare = (a: string, b: string) => a.localeCompare(b); - - it('sorts vendors alphabetically by name using localeCompare', () => { - const vendors = [ - {id: '1', name: 'Zebra'}, - {id: '2', name: 'Apple'}, - {id: '3', name: 'Banana'}, - ]; - - const result = sortVendors(vendors, localeCompare); - expect(result.map((v) => v.name)).toEqual(['Apple', 'Banana', 'Zebra']); - }); - - it('breaks name ties using vendor id', () => { - const vendors = [ - {id: 'vendor_b', name: 'Acme'}, - {id: 'vendor_a', name: 'Acme'}, - ]; - - const result = sortVendors(vendors, localeCompare); - expect(result.map((v) => v.id)).toEqual(['vendor_a', 'vendor_b']); - }); - - it('does not sort the input array in place', () => { - const vendors = [ - {id: '2', name: 'Zebra'}, - {id: '1', name: 'Alpha'}, - ]; - - const result = sortVendors(vendors, localeCompare); - expect(result).not.toBe(vendors); - expect(vendors.map((v) => v.name)).toEqual(['Zebra', 'Alpha']); - }); - - it('returns empty array for empty input', () => { - expect(sortVendors([], localeCompare)).toEqual([]); - }); - - it('returns single-element array as-is', () => { - const vendors = [{id: '1', name: 'Only'}]; - const result = sortVendors(vendors, localeCompare); - expect(result).toHaveLength(1); - expect(result.at(0)?.name).toBe('Only'); - }); - - it('breaks name ties using externalID when id is absent', () => { - const vendors = [ - {externalID: 'vendor_b', name: 'Acme'}, - {externalID: 'vendor_a', name: 'Acme'}, - ]; - - const result = sortVendors(vendors, localeCompare); - expect(result.map((v) => v.externalID)).toEqual(['vendor_a', 'vendor_b']); - }); - }); - - describe('getDefaultVendorID', () => { - it('resolves QBO credit card default vendor when destination is credit_card', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: { - config: { - nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD, - nonReimbursableCreditCardDefaultVendor: 'qbo_vendor_1', - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.QBO)).toBe('qbo_vendor_1'); - }); - - it('returns undefined for QBO when destination is not credit_card', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.QBO]: { - config: { - nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD, - nonReimbursableCreditCardDefaultVendor: 'qbo_vendor_1', - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.QBO)).toBeUndefined(); - }); - - it('resolves Sage Intacct credit card charge default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: { - config: { - export: { - nonReimbursableCreditCardChargeDefaultVendor: 'intacct_vendor_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT)).toBe('intacct_vendor_1'); - }); - - it('resolves Xero default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.XERO]: { - config: { - defaultVendor: 'xero_contact_1', - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.XERO)).toBe('xero_contact_1'); - }); - - it('resolves Rillet default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { - config: { - export: { - defaultVendorID: 'rillet_vendor_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.RILLET)).toBe('rillet_vendor_1'); - }); - - it('resolves DualEntry default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.DUALENTRY]: { - config: { - export: { - defaultVendorID: 'dualentry_vendor_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.DUALENTRY)).toBe('dualentry_vendor_1'); - }); - - it('resolves Campfire default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE]: { - config: { - export: { - defaultVendorID: 'campfire_vendor_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.CAMPFIRE)).toBe('campfire_vendor_1'); - }); - - it('resolves Business Central default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL]: { - config: { - export: { - defaultVendorID: 'bc_vendor_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.BUSINESS_CENTRAL)).toBe('bc_vendor_1'); - }); - - it('resolves Certinia default vendor', () => { - const policy = createMock({ - id: '1', - connections: createMock({ - [CONST.POLICY.CONNECTIONS.NAME.CERTINIA]: { - config: { - export: { - vendorAccount: 'certinia_acc_1', - }, - }, - }, - }), - }); - - expect(getDefaultVendorID(policy, CONST.POLICY.CONNECTIONS.NAME.CERTINIA)).toBe('certinia_acc_1'); - }); - - it('returns undefined when policy has no connections', () => { - expect(getDefaultVendorID(undefined, CONST.POLICY.CONNECTIONS.NAME.QBO)).toBeUndefined(); - }); - }); - describe('getSageIntacctVendors', () => { const localeCompare = (a: string, b: string) => a.localeCompare(b); diff --git a/tests/unit/PolicyVendorActionTest.ts b/tests/unit/PolicyVendorActionTest.ts deleted file mode 100644 index 68732ce5b0e2..000000000000 --- a/tests/unit/PolicyVendorActionTest.ts +++ /dev/null @@ -1,162 +0,0 @@ -import {clearVendorErrors, setPolicyVendorsEnabled} from '@libs/actions/Policy/Vendor'; -import {write} from '@libs/API'; -import {WRITE_COMMANDS} from '@libs/API/types'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {PolicyVendors} from '@src/types/onyx'; - -import Onyx from 'react-native-onyx'; - -jest.mock('@libs/API'); -jest.mock('@expensify/react-native-hybrid-app', () => ({ - __esModule: true, - default: {isHybridApp: () => false}, -})); - -const mockWrite = jest.mocked(write); -const policyID = 'test_policy_1'; -const policyVendorsKey = `${ONYXKEYS.COLLECTION.POLICY_VENDORS}${policyID}` as const; - -describe('PolicyVendorAction', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); - - describe('setPolicyVendorsEnabled', () => { - it('does nothing when vendorIDs is empty', () => { - setPolicyVendorsEnabled({ - policyID, - vendorIDs: [], - enabled: true, - }); - - expect(mockWrite).not.toHaveBeenCalled(); - }); - - it('dispatches SetPolicyVendorsEnabled with optimistic, success, and failure data', () => { - const initialPolicyVendors: PolicyVendors = { - vendor1: { - externalID: 'vendor1', - name: 'Acme Corp', - enabled: false, - origin: CONST.POLICY.CONNECTIONS.NAME.QBO, - }, - vendor2: { - externalID: 'vendor2', - name: 'Beta LLC', - enabled: false, - origin: CONST.POLICY.CONNECTIONS.NAME.QBO, - }, - }; - - setPolicyVendorsEnabled({ - policyID, - vendorIDs: ['vendor1', 'vendor2'], - enabled: true, - policyVendors: initialPolicyVendors, - }); - - expect(mockWrite).toHaveBeenCalledTimes(1); - expect(mockWrite).toHaveBeenCalledWith( - WRITE_COMMANDS.SET_POLICY_VENDORS_ENABLED, - { - policyID, - vendorIDs: JSON.stringify(['vendor1', 'vendor2']), - enabled: true, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyVendorsKey, - value: { - vendor1: expect.objectContaining({ - enabled: true, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - errors: null, - }), - vendor2: expect.objectContaining({ - enabled: true, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - enabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - errors: null, - }), - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyVendorsKey, - value: { - vendor1: expect.objectContaining({ - enabled: true, - pendingAction: null, - pendingFields: { - enabled: null, - }, - errors: null, - }), - vendor2: expect.objectContaining({ - enabled: true, - pendingAction: null, - pendingFields: { - enabled: null, - }, - errors: null, - }), - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: policyVendorsKey, - value: { - vendor1: expect.objectContaining({ - pendingAction: null, - pendingFields: { - enabled: null, - }, - errors: expect.anything() as unknown, - }), - vendor2: expect.objectContaining({ - pendingAction: null, - pendingFields: { - enabled: null, - }, - errors: expect.anything() as unknown, - }), - }, - }, - ], - }, - ); - }); - }); - - describe('clearVendorErrors', () => { - it('merges null errors and pending fields to Onyx', () => { - const onyxMergeSpy = jest.spyOn(Onyx, 'merge'); - - clearVendorErrors(policyID, 'vendor1'); - - expect(onyxMergeSpy).toHaveBeenCalledWith(policyVendorsKey, { - vendor1: { - errors: null, - pendingAction: null, - pendingFields: null, - errorFields: null, - }, - }); - - onyxMergeSpy.mockRestore(); - }); - }); -}); diff --git a/tests/unit/ViolationUtilsTest.ts b/tests/unit/ViolationUtilsTest.ts index c0ab4807b7d5..1059502aaf94 100644 --- a/tests/unit/ViolationUtilsTest.ts +++ b/tests/unit/ViolationUtilsTest.ts @@ -24,10 +24,6 @@ jest.mock('@libs/actions/Report', () => ({ getCurrentUserEmail: jest.fn(() => MOCK_CURRENT_USER_EMAIL), })); -jest.mock('@expensify/react-native-hybrid-app', () => ({ - isHybridApp: jest.fn(() => false), -})); - const categoryOutOfPolicyViolation = { name: CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY, type: CONST.VIOLATION_TYPES.VIOLATION, @@ -3266,110 +3262,6 @@ describe('getViolationsOnyxData', () => { expect(result.value).not.toContainEqual(inactiveVendorViolation); }); - it('adds the violation when the vendor remains in the raw integration list but is disabled in policyVendors', () => { - const activeVendorID = 'v-active'; - policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; - const policyVendors: PolicyVendors = { - [activeVendorID]: { - externalID: activeVendorID, - name: 'Acme Co', - enabled: false, - }, - }; - const result = ViolationsUtils.getViolationsOnyxData({ - isVendorMatchingBetaEnabled: true, - ownerLogin: undefined, - updatedTransaction: transaction, - transactionViolations: [], - policy, - policyTagList: policyTags, - policyCategories, - hasDependentTags: false, - isInvoiceTransaction: false, - policyVendors, - }); - expect(result.value).toEqual(expect.arrayContaining([inactiveVendorViolation])); - }); - - it('retains an existing violation through an optimistic update while the vendor remains disabled in policyVendors', () => { - const activeVendorID = 'v-active'; - policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; - const policyVendors: PolicyVendors = { - [activeVendorID]: { - externalID: activeVendorID, - name: 'Acme Co', - enabled: false, - }, - }; - const result = ViolationsUtils.getViolationsOnyxData({ - isVendorMatchingBetaEnabled: true, - ownerLogin: undefined, - updatedTransaction: transaction, - transactionViolations: [inactiveVendorViolation], - policy, - policyTagList: policyTags, - policyCategories, - hasDependentTags: false, - isInvoiceTransaction: false, - policyVendors, - }); - expect(result.value).toContainEqual(inactiveVendorViolation); - }); - - it('removes an existing violation when the vendor is re-enabled in policyVendors', () => { - const activeVendorID = 'v-active'; - policy = policyWithQBOVendorFeature(); - transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; - const policyVendors: PolicyVendors = { - [activeVendorID]: { - externalID: activeVendorID, - name: 'Acme Co', - enabled: true, - }, - }; - const result = ViolationsUtils.getViolationsOnyxData({ - isVendorMatchingBetaEnabled: true, - ownerLogin: undefined, - updatedTransaction: transaction, - transactionViolations: [inactiveVendorViolation], - policy, - policyTagList: policyTags, - policyCategories, - hasDependentTags: false, - isInvoiceTransaction: false, - policyVendors, - }); - expect(result.value).not.toContainEqual(inactiveVendorViolation); - }); - - it('leaves the violation untouched when the active integration vendor list is not loaded yet even if policyVendors is present', () => { - const activeVendorID = 'v-active'; - policy = policyWithQBOVendorFeature(null); - transaction.comment = {...transaction.comment, vendor: {externalID: activeVendorID, wasManuallySet: true}}; - const policyVendors: PolicyVendors = { - [activeVendorID]: { - externalID: activeVendorID, - name: 'Acme Co', - enabled: false, - }, - }; - const result = ViolationsUtils.getViolationsOnyxData({ - isVendorMatchingBetaEnabled: true, - ownerLogin: undefined, - updatedTransaction: transaction, - transactionViolations: [inactiveVendorViolation], - policy, - policyTagList: policyTags, - policyCategories, - hasDependentTags: false, - isInvoiceTransaction: false, - policyVendors, - }); - expect(result.value).toContainEqual(inactiveVendorViolation); - }); - it('removes an existing violation when the user clears the vendor while the feature is still active', () => { policy = policyWithQBOVendorFeature(); // transaction.comment has no vendor key — represents a cleared selection