diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 53df7ab78939..dcc80e5366e3 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -46,6 +46,7 @@ import type {TupleToUnion, ValueOf} from 'type-fest'; import {Str} from 'expensify-common'; +import {getQuickbooksOnlineIntegrationName} from './AccountingUtils'; import {getBankAccountFromID} from './actions/BankAccounts'; import {hasSynchronizationErrorMessage, isConnectionUnverified} from './actions/connections'; import {shouldShowQBOReimbursableExportDestinationAccountError} from './actions/connections/QuickbooksOnline'; @@ -2459,16 +2460,23 @@ function isXeroVendorMatchingActive(policy: OnyxEntry): boolean { return !!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.XERO]?.config?.isConfigured; } +/** + * True when Rillet is connected AND configured. Mirrors `Rillet::hasVendorFeature` on the PHP side. + */ +function isRilletVendorMatchingActive(policy: OnyxEntry): boolean { + return !!policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.RILLET]?.config?.isConfigured; +} + /** * True when Xero is the *active* vendor-matching source for the workspace — i.e. Xero is * connected AND neither QBO nor Intacct is in a vendor-matching export mode. Mirrors the precedence - * in `getMatchingVendors` (QBO → Intacct → Xero) so the UI labels, copy, and inactive-vendor - * guardrail stay bound to whichever integration's vendor list is actually being consulted. Without - * this scoping, a workspace with active QBO matching + a lingering Xero connection would render + * in `getActiveVendorMatchingIntegration` (QBO → Intacct → Xero → Rillet) so the UI labels, copy, and + * inactive-vendor guardrail stay bound to whichever integration's vendor list is actually being consulted. + * Without this scoping, a workspace with active QBO matching + a lingering Xero connection would render * QBO vendors under the "Supplier" label. */ function isXeroActiveMatchingSource(policy: OnyxEntry): boolean { - return isXeroVendorMatchingActive(policy) && !isQBOVendorMatchingActive(policy) && !isIntacctVendorMatchingActive(policy); + return getActiveVendorMatchingIntegration(policy) === CONST.POLICY.CONNECTIONS.NAME.XERO; } /** @@ -2478,10 +2486,11 @@ function isXeroActiveMatchingSource(policy: OnyxEntry): boolean { * the field. * * The `vendorMatching` beta only gates the integrations that haven't reached GA yet, so - * `isVendorMatchingBetaEnabled` is consulted on the Intacct and Xero branches but not on QBO: + * `isVendorMatchingBetaEnabled` is consulted on the Intacct, Xero, and Rillet branches but not on QBO: * - QBO (R1) with non-reimbursable export = Credit Card or Debit Card. GA, so no beta required * - Sage Intacct (R2) with non-reimbursable export = Credit Card Charge. Beta required - * - Xero (R3) has no export destination enum, so a present connection is enough. Beta required + * - Xero (R3) has no export destination enum, so a configured connection is enough. Beta required + * - Rillet (R4) configured connection. Beta required */ function hasVendorFeature(policy: OnyxEntry, isVendorMatchingBetaEnabled: boolean): boolean { if (!policy) { @@ -2490,12 +2499,12 @@ function hasVendorFeature(policy: OnyxEntry, isVendorMatchingBetaEnabled if (isQBOVendorMatchingActive(policy)) { return true; } - return isVendorMatchingBetaEnabled && (isIntacctVendorMatchingActive(policy) || isXeroVendorMatchingActive(policy)); + return isVendorMatchingBetaEnabled && (isIntacctVendorMatchingActive(policy) || isXeroVendorMatchingActive(policy) || isRilletVendorMatchingActive(policy)); } /** * Single source of truth for which connected integration scopes the vendor field for this workspace - * (QBO, Sage Intacct, or Xero) and what its vendor list looks like. Returns `undefined` when no + * (QBO, Sage Intacct, Xero, or Rillet) and what its vendor list looks like. Returns `undefined` when no * vendor-matching integration is active OR when the active integration's list hasn't synced yet — * distinct from `[]` (loaded-empty). Lets callers tell "no vendors" from "not loaded". * @@ -2527,6 +2536,12 @@ function getActiveVendorMatchingIntegration(policy: OnyxEntry): Connecti if (isIntacctVendorMatchingActive(policy)) { return CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT; } + if (isXeroVendorMatchingActive(policy)) { + return CONST.POLICY.CONNECTIONS.NAME.XERO; + } + if (isRilletVendorMatchingActive(policy)) { + return CONST.POLICY.CONNECTIONS.NAME.RILLET; + } return undefined; } @@ -2556,6 +2571,18 @@ function getActiveVendorMatchingVendors(policy: OnyxEntry): Vendor[] | u } return Object.values(xeroContacts).map((contact) => ({id: contact.id, name: contact.name, currency: '', email: contact.email})); } + if (isRilletVendorMatchingActive(policy)) { + const rilletVendors = policy.connections?.[CONST.POLICY.CONNECTIONS.NAME.RILLET]?.data?.vendors; + if (rilletVendors === undefined) { + return undefined; + } + return rilletVendors.map((vendor) => ({ + id: vendor.id, + name: vendor.name, + currency: '', + email: vendor.email ?? '', + })); + } return undefined; } @@ -2627,6 +2654,15 @@ function findVendorByID(policy: OnyxEntry, vendorID: string | undefined) if (xeroContact) { return {id: xeroContact.id, name: xeroContact.name, currency: '', email: xeroContact.email}; } + const rilletVendor = policy.connections?.[CONST.POLICY.CONNECTIONS.NAME.RILLET]?.data?.vendors?.find((vendor) => vendor.id === vendorID); + if (rilletVendor) { + return { + id: rilletVendor.id, + name: rilletVendor.name, + currency: '', + email: rilletVendor.email ?? '', + }; + } return undefined; } @@ -2647,10 +2683,42 @@ function getVendorRuleDisplayValue(policy: OnyxEntry, vendorID: string, } const historicalVendorName = findVendorByID(policy, vendorID)?.name; - const hasActiveVendorMatchingSource = getActiveVendorMatchingIntegration(policy) !== undefined || isXeroActiveMatchingSource(policy); + const hasActiveVendorMatchingSource = getActiveVendorMatchingIntegration(policy) !== undefined; return historicalVendorName ?? (hasActiveVendorMatchingSource ? vendorID : unavailableLabel); } +/** + * Source-specific empty state copy for the vendor selector when the active integration has zero vendors. + */ +function getVendorEmptyState(policy: OnyxEntry, translate: LocaleContextProps['translate']): {title: string; subtitle: string} { + const activeIntegration = getActiveVendorMatchingIntegration(policy); + switch (activeIntegration) { + case CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT: + return { + title: translate('workspace.sageIntacct.noAccountsFound'), + subtitle: translate('workspace.sageIntacct.noAccountsFoundDescription'), + }; + case CONST.POLICY.CONNECTIONS.NAME.XERO: + return { + title: translate('workspace.xero.noSuppliersFound'), + subtitle: translate('workspace.xero.noSuppliersFoundDescription'), + }; + case CONST.POLICY.CONNECTIONS.NAME.RILLET: + return { + title: translate('workspace.rillet.noVendorsFound'), + subtitle: translate('workspace.rillet.noVendorsFoundDescription'), + }; + case CONST.POLICY.CONNECTIONS.NAME.QBO: + default: { + const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); + return { + title: translate('workspace.qbo.noAccountsFound'), + subtitle: translate('workspace.qbo.noAccountsFoundDescription', integrationName), + }; + } + } +} + /** * Xero-scoped supplier list, normalized to the shared `Vendor` shape. Use this from Xero-specific * UI (the default-supplier picker, the Xero export config row) so the data source stays bound to @@ -3147,9 +3215,11 @@ export { getActiveVendorMatchingIntegration, getMatchingVendorByID, getMatchingVendors, + getVendorEmptyState, getVendorRuleDisplayValue, getXeroSupplierByID, getXeroSuppliers, + isRilletVendorMatchingActive, isXeroActiveMatchingSource, isXeroVendorMatchingActive, hasVendorFeature, diff --git a/src/pages/iou/request/step/IOURequestStepVendor.tsx b/src/pages/iou/request/step/IOURequestStepVendor.tsx index 2ee5e1a33401..69a76c67a719 100644 --- a/src/pages/iou/request/step/IOURequestStepVendor.tsx +++ b/src/pages/iou/request/step/IOURequestStepVendor.tsx @@ -15,11 +15,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {updateMoneyRequestVendor} from '@libs/actions/IOU/UpdateMoneyRequest'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; -import {getMatchingVendors, hasVendorFeature, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; +import {getMatchingVendors, getVendorEmptyState, hasVendorFeature, isXeroActiveMatchingSource} from '@libs/PolicyUtils'; import {isPerDiemRequest} from '@libs/TransactionUtils'; -import {getQuickbooksOnlineIntegrationName} from '@pages/workspace/accounting/utils'; - import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -67,7 +65,7 @@ function IOURequestStepVendor({ const isFeatureAvailable = hasVendorFeature(policy, isBetaEnabled(CONST.BETAS.VENDOR_MATCHING)); const isOnXero = isXeroActiveMatchingSource(policy); - const integrationName = getQuickbooksOnlineIntegrationName(policy, translate); + const emptyState = getVendorEmptyState(policy, translate); // 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; @@ -135,8 +133,8 @@ function IOURequestStepVendor({ icon={illustrations.Telescope} iconWidth={variables.emptyListIconWidth} iconHeight={variables.emptyListIconHeight} - title={isOnXero ? translate('workspace.xero.noSuppliersFound') : translate('workspace.qbo.noAccountsFound')} - subtitle={isOnXero ? translate('workspace.xero.noSuppliersFoundDescription') : translate('workspace.qbo.noAccountsFoundDescription', integrationName)} + title={emptyState.title} + subtitle={emptyState.subtitle} containerStyle={styles.pb10} /> ) : null; diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx index f6cdb604ab86..92d2443fec89 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage/index.tsx @@ -168,9 +168,14 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro // vendors right now), so it can't double as the visibility gate. // // Beta gating mirrors `hasVendorFeature`: QBO (R1) is GA, so a connected QBO workspace always - // sees the row regardless of the `vendorMatching` beta. Sage Intacct (R2) and Xero (R3) haven't - // reached GA, so they only show the row while the beta is enabled. - const vendorMatchingConnection = getConnectedIntegration(policy, [CONST.POLICY.CONNECTIONS.NAME.QBO, CONST.POLICY.CONNECTIONS.NAME.XERO, CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]); + // sees the row regardless of the `vendorMatching` beta. Sage Intacct (R2), Xero (R3), and Rillet (R4) + // haven't reached GA, so they only show the row while the beta is enabled. + const vendorMatchingConnection = getConnectedIntegration(policy, [ + CONST.POLICY.CONNECTIONS.NAME.QBO, + CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, + CONST.POLICY.CONNECTIONS.NAME.XERO, + CONST.POLICY.CONNECTIONS.NAME.RILLET, + ]); const shouldShowVendorsFeature = vendorMatchingConnection === CONST.POLICY.CONNECTIONS.NAME.QBO || (isVendorMatchingEnabled && !!vendorMatchingConnection); const warnAccountingManagesOrganizeFeature = async () => { diff --git a/tests/unit/PolicyUtilsTest.ts b/tests/unit/PolicyUtilsTest.ts index 57baa12d9ed7..2943f43ca7b0 100644 --- a/tests/unit/PolicyUtilsTest.ts +++ b/tests/unit/PolicyUtilsTest.ts @@ -29,8 +29,10 @@ import { getExcludedUsers, getExpensifyTeamExclusions, getManagerAccountID, + getActiveVendorMatchingIntegration, getMatchingVendorByID, getMatchingVendors, + getVendorEmptyState, getPolicyApproverLogins, getPolicyBrickRoadIndicatorStatus, getPolicyByCustomUnitID, @@ -66,6 +68,7 @@ import { isPerDiemEnabled, isPolicyMemberWithoutPendingDelete, isSubmitterApproveBlockedOnSubmitWorkspace, + isRilletVendorMatchingActive, isTaxCodeCustomized, isXeroActiveMatchingSource, isXeroVendorMatchingActive, @@ -3755,6 +3758,21 @@ describe('PolicyUtils', () => { }, }); + const RILLET_VENDORS_UNSYNCED = Symbol('RILLET_VENDORS_UNSYNCED'); + const buildRilletPolicy = ( + vendors: Array<{id: string; name: string; email?: string}> | typeof RILLET_VENDORS_UNSYNCED = [{id: 'rv-1', name: 'Acme Rillet', email: 'acme@rillet.com'}], + {isConfigured = true}: {isConfigured?: boolean} = {}, + ): Policy => + createMock({ + ...createRandomPolicy(0), + connections: { + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured}, + data: vendors === RILLET_VENDORS_UNSYNCED ? {} : {vendors}, + }, + }, + }); + describe('hasVendorFeature', () => { it('returns true when beta is enabled and QBO non-reimbursable export is Credit Card', () => { expect(hasVendorFeature(buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD), true)).toBe(true); @@ -3787,6 +3805,18 @@ describe('PolicyUtils', () => { expect(hasVendorFeature(buildXeroPolicy(undefined, {isConfigured: false}), true)).toBe(false); }); + it('returns true when beta is enabled and Rillet is connected with isConfigured=true', () => { + expect(hasVendorFeature(buildRilletPolicy(), true)).toBe(true); + }); + + it('returns true when beta is enabled and Rillet is configured but vendors have not synced yet', () => { + expect(hasVendorFeature(buildRilletPolicy(RILLET_VENDORS_UNSYNCED), true)).toBe(true); + }); + + it('returns false when Rillet is connected but isConfigured=false', () => { + expect(hasVendorFeature(buildRilletPolicy(undefined, {isConfigured: false}), true)).toBe(false); + }); + it('returns true when beta is disabled and QBO non-reimbursable export is Credit Card because QBO (R1) is generally available', () => { expect(hasVendorFeature(buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD), false)).toBe(true); }); @@ -3807,6 +3837,10 @@ describe('PolicyUtils', () => { expect(hasVendorFeature(buildXeroPolicy(), false)).toBe(false); }); + it('returns false when beta is disabled and Rillet is connected because Rillet is still pre-GA', () => { + expect(hasVendorFeature(buildRilletPolicy(), false)).toBe(false); + }); + it('returns false when QBO non-reimbursable export is Vendor Bill', () => { expect(hasVendorFeature(buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL), true)).toBe(false); }); @@ -3908,6 +3942,41 @@ describe('PolicyUtils', () => { expect(getMatchingVendors(buildXeroPolicy(XERO_CONTACTS_UNSYNCED))).toEqual([]); }); + it('returns the Rillet vendor list normalized to {id, name, currency, email}', () => { + const rilletVendors = [ + {id: 'rv-1', name: 'Acme Rillet', email: 'acme@rillet.com'}, + {id: 'rv-2', name: 'Other Rillet'}, + ]; + const policy = buildRilletPolicy(rilletVendors); + expect(getMatchingVendors(policy)).toEqual([ + {id: 'rv-1', name: 'Acme Rillet', currency: '', email: 'acme@rillet.com'}, + {id: 'rv-2', name: 'Other Rillet', currency: '', email: ''}, + ]); + }); + + it('returns Xero vendors when both Xero and Rillet are connected (Xero precedence over Rillet)', () => { + const xeroContacts = {xc1: {id: 'xc1', name: 'Acme Xero', email: 'acme@xero.com'}}; + const rilletVendors = [{id: 'rv-1', name: 'Acme Rillet', email: 'acme@rillet.com'}]; + const policy = createMock({ + ...createRandomPolicy(0), + connections: createMock({ + [CONST.POLICY.CONNECTIONS.NAME.XERO]: { + config: {isConfigured: true}, + data: {contacts: xeroContacts}, + }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured: true}, + data: {vendors: rilletVendors}, + }, + }), + }); + expect(getMatchingVendors(policy)).toEqual([{id: 'xc1', name: 'Acme Xero', currency: '', email: 'acme@xero.com'}]); + }); + + it('returns an empty array when Rillet is connected but vendors have not synced yet (data.vendors undefined)', () => { + expect(getMatchingVendors(buildRilletPolicy(RILLET_VENDORS_UNSYNCED))).toEqual([]); + }); + it('returns an empty array when no supported connection exists', () => { const policy = createMock({...createRandomPolicy(0), connections: {}}); expect(getMatchingVendors(policy)).toEqual([]); @@ -3947,6 +4016,19 @@ describe('PolicyUtils', () => { expect(getMatchingVendorByID(buildXeroPolicy(), 'xcMissing')).toBeUndefined(); }); + it('returns the matching Rillet vendor (normalized) when the ID exists in the list', () => { + const policy = buildRilletPolicy([ + {id: 'rv-1', name: 'Acme Rillet', email: 'acme@rillet.com'}, + {id: 'rv-2', name: 'Other Rillet'}, + ]); + expect(getMatchingVendorByID(policy, 'rv-1')).toEqual({id: 'rv-1', name: 'Acme Rillet', currency: '', email: 'acme@rillet.com'}); + expect(getMatchingVendorByID(policy, 'rv-2')).toEqual({id: 'rv-2', name: 'Other Rillet', currency: '', email: ''}); + }); + + it('returns undefined for a Rillet vendor ID that is not in the list (inactive-vendor case)', () => { + expect(getMatchingVendorByID(buildRilletPolicy(), 'rv-missing')).toBeUndefined(); + }); + it('returns undefined when the ID is not in the list (the inactive-vendor case)', () => { const policy = buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD); expect(getMatchingVendorByID(policy, 'v-missing')).toBeUndefined(); @@ -3974,6 +4056,41 @@ describe('PolicyUtils', () => { expect(findVendorByID(policy, 'xc1')).toEqual({id: 'xc1', name: 'Acme Xero', currency: '', email: 'acme@example.com'}); }); + it('resolves a Rillet vendor (normalized) from connections.rillet.data.vendors', () => { + const policy = buildRilletPolicy([{id: 'rv-1', name: 'Acme Rillet', email: 'acme@rillet.com'}]); + expect(findVendorByID(policy, 'rv-1')).toEqual({id: 'rv-1', name: 'Acme Rillet', currency: '', email: 'acme@rillet.com'}); + }); + + it('prefers the active Xero integration over stale Rillet data when both hold the same vendor ID', () => { + const xeroPolicy = buildXeroPolicy({shared: {id: 'shared', name: 'Xero Name', email: 'xero@xero.com'}}); + const policy = createMock({ + ...xeroPolicy, + connections: { + ...xeroPolicy.connections, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured: true}, + data: {vendors: [{id: 'shared', name: 'Rillet Name', email: 'rillet@rillet.com'}]}, + }, + }, + }); + expect(findVendorByID(policy, 'shared')).toEqual({id: 'shared', name: 'Xero Name', currency: '', email: 'xero@xero.com'}); + }); + + it('prefers the active Rillet integration over stale QBO data when both hold the same vendor ID', () => { + const rilletPolicy = buildRilletPolicy([{id: 'shared', name: 'Rillet Name', email: 'rillet@rillet.com'}]); + const policy = createMock({ + ...rilletPolicy, + connections: { + ...rilletPolicy.connections, + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}, + data: {vendors: [{id: 'shared', name: 'QBO Stale Name', currency: 'USD'}]}, + }, + }, + }); + expect(findVendorByID(policy, 'shared')).toEqual({id: 'shared', name: 'Rillet Name', currency: '', email: 'rillet@rillet.com'}); + }); + it('prefers the active Intacct integration over stale QBO data when both hold the same vendor ID', () => { // Workspace state: QBO connected but in Vendor Bill mode (not vendor-matching), // Intacct connected in Credit Card Charge mode (the active matching integration). @@ -4253,6 +4370,136 @@ describe('PolicyUtils', () => { expect(isXeroVendorMatchingActive(undefined)).toBe(false); }); }); + + describe('isRilletVendorMatchingActive', () => { + it('returns true when Rillet is connected with isConfigured=true', () => { + expect(isRilletVendorMatchingActive(buildRilletPolicy())).toBe(true); + }); + + it('returns false when Rillet is connected but isConfigured=false', () => { + expect(isRilletVendorMatchingActive(buildRilletPolicy(undefined, {isConfigured: false}))).toBe(false); + }); + + it('returns false when Rillet is not connected', () => { + const policy = {...createRandomPolicy(0), connections: {}}; + expect(isRilletVendorMatchingActive(policy)).toBe(false); + }); + + it('returns false when policy is undefined', () => { + expect(isRilletVendorMatchingActive(undefined)).toBe(false); + }); + }); + + describe('getActiveVendorMatchingIntegration', () => { + it('returns QBO when QBO is in vendor matching export mode', () => { + expect(getActiveVendorMatchingIntegration(buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD))).toBe(CONST.POLICY.CONNECTIONS.NAME.QBO); + }); + + it('returns Sage Intacct when Intacct is in vendor matching export mode', () => { + expect(getActiveVendorMatchingIntegration(buildIntacctPolicy(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE))).toBe( + CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT, + ); + }); + + it('returns Xero when Xero is connected and configured', () => { + expect(getActiveVendorMatchingIntegration(buildXeroPolicy())).toBe(CONST.POLICY.CONNECTIONS.NAME.XERO); + }); + + it('returns Rillet when Rillet is connected and configured', () => { + expect(getActiveVendorMatchingIntegration(buildRilletPolicy())).toBe(CONST.POLICY.CONNECTIONS.NAME.RILLET); + }); + + it('follows cascade precedence QBO > Sage Intacct > Xero > Rillet', () => { + const policy = createMock({ + ...createRandomPolicy(0), + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}, + }, + [CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT]: { + config: {export: {nonReimbursable: CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE}}, + }, + [CONST.POLICY.CONNECTIONS.NAME.XERO]: { + config: {isConfigured: true}, + }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured: true}, + }, + }, + }); + expect(getActiveVendorMatchingIntegration(policy)).toBe(CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT); + }); + + it('returns Xero over Rillet when both are configured', () => { + const policy = createMock({ + ...createRandomPolicy(0), + connections: { + [CONST.POLICY.CONNECTIONS.NAME.XERO]: { + config: {isConfigured: true}, + }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured: true}, + }, + }, + }); + expect(getActiveVendorMatchingIntegration(policy)).toBe(CONST.POLICY.CONNECTIONS.NAME.XERO); + }); + + it('returns undefined when no integration is configured for vendor matching', () => { + const policy = createMock({ + ...createRandomPolicy(0), + connections: { + [CONST.POLICY.CONNECTIONS.NAME.QBO]: { + config: {nonReimbursableExpensesExportDestination: CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.VENDOR_BILL}, + }, + [CONST.POLICY.CONNECTIONS.NAME.RILLET]: { + config: {isConfigured: false}, + }, + }, + }); + expect(getActiveVendorMatchingIntegration(policy)).toBeUndefined(); + }); + + it('returns undefined when policy is undefined', () => { + expect(getActiveVendorMatchingIntegration(undefined)).toBeUndefined(); + }); + }); + + describe('getVendorEmptyState', () => { + const translate = TestHelper.translateLocal; + + it('returns QBO empty state copy when QBO is the active integration', () => { + const policy = buildQBOPolicy(CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD); + expect(getVendorEmptyState(policy, translate)).toEqual({ + title: translate('workspace.qbo.noAccountsFound'), + subtitle: translate('workspace.qbo.noAccountsFoundDescription', 'QuickBooks Online'), + }); + }); + + it('returns Sage Intacct empty state copy when Intacct is the active integration', () => { + const policy = buildIntacctPolicy(CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE); + expect(getVendorEmptyState(policy, translate)).toEqual({ + title: translate('workspace.sageIntacct.noAccountsFound'), + subtitle: translate('workspace.sageIntacct.noAccountsFoundDescription'), + }); + }); + + it('returns Xero empty state copy when Xero is the active integration', () => { + const policy = buildXeroPolicy(); + expect(getVendorEmptyState(policy, translate)).toEqual({ + title: translate('workspace.xero.noSuppliersFound'), + subtitle: translate('workspace.xero.noSuppliersFoundDescription'), + }); + }); + + it('returns Rillet empty state copy when Rillet is the active integration', () => { + const policy = buildRilletPolicy(); + expect(getVendorEmptyState(policy, translate)).toEqual({ + title: translate('workspace.rillet.noVendorsFound'), + subtitle: translate('workspace.rillet.noVendorsFoundDescription'), + }); + }); + }); }); describe('hasPolicyRulesError', () => {