Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0941876
Add vendor-matching types + constants (Track D1)
Beamanator May 20, 2026
45b0d77
Add QBO default-vendor action for non-reimbursable Credit/Debit card …
Beamanator May 20, 2026
d81bc20
Add updateMoneyRequestVendor action (Track D2b)
Beamanator May 20, 2026
b9c0196
Add PolicyUtils vendor helpers (Track D3)
Beamanator May 20, 2026
570611e
Add inactive-vendor violation logic to ViolationsUtils (Track D4)
Beamanator May 20, 2026
5111e70
Fix ESLint failures on Transaction.ts and ViolationsUtils.ts
Beamanator May 21, 2026
4fc3e44
Add inactiveVendor translation to getViolationTranslation + all locales
Beamanator May 21, 2026
b7fbbc4
Remove now-redundant 'as never' cast in violation switch default
Beamanator May 21, 2026
7a0bf97
Clear inactive-vendor violation when user clears the vendor
Beamanator May 22, 2026
a377b7e
Merge remote-tracking branch 'origin/main' into beaman/vendor-matchin…
Beamanator May 25, 2026
9283f98
Defer areVendorsEnabled / ARE_VENDORS_ENABLED to Phase 2
Beamanator May 25, 2026
3a8a17f
Add unit tests for vendor matching foundations
Beamanator May 25, 2026
f621634
Fix spellcheck: replace 'Initech' vendor name with generic 'Other Co'
Beamanator May 25, 2026
6275b29
Drop project/release tags from vendor-matching code comments
Beamanator May 25, 2026
ebc4ea1
Add VENDOR_MATCHING_CC beta gate so backend can ship independently
Beamanator May 25, 2026
da962e0
Drop misleading @param JSDoc on hasVendorFeature
Beamanator May 25, 2026
adaf5fa
Clear inactive-vendor violation optimistically in updateMoneyRequestV…
Beamanator May 25, 2026
7fa99c3
Align beta name with PHP: VENDOR_MATCHING_CC -> VENDOR_MATCHING
Beamanator May 25, 2026
43d488c
Restore exhaustiveness guard on getViolationTranslation switch
Beamanator May 26, 2026
0289f82
updateMoneyRequestVendor: fall back to Onyx transaction lookup for fa…
Beamanator May 26, 2026
85a13b1
updateMoneyRequestVendor: skip vendor rollback when prior state is un…
Beamanator May 26, 2026
c99ed35
ViolationsUtils: gate inactiveVendor reconcile on betas loaded
Beamanator May 26, 2026
108d265
Test updateMoneyRequestVendor Onyx fallback + missing-snapshot guard
Beamanator May 26, 2026
3566223
Seed ONYXKEYS.BETAS in inactiveVendor tests so allBetas gate is exerc…
Beamanator May 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ const CONST = {
WORKSPACE_ROOMS_PAGE: 'workspaceRoomsPage',
CERTINIA: 'financialForceNewDot',
MERGE_HR: 'mergeHRConnections',
VENDOR_MATCHING: 'vendorMatching',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -2746,6 +2747,7 @@ const CONST = {
REIMBURSABLE_EXPENSES_ACCOUNT: 'reimbursableExpensesAccount',
REIMBURSABLE_EXPENSES_EXPORT_DESTINATION: 'reimbursableExpensesExportDestination',
NON_REIMBURSABLE_BILL_DEFAULT_VENDOR: 'nonReimbursableBillDefaultVendor',
NON_REIMBURSABLE_CREDIT_CARD_DEFAULT_VENDOR: 'nonReimbursableCreditCardDefaultVendor',
NON_REIMBURSABLE_EXPENSE_EXPORT_DESTINATION: 'nonReimbursableExpensesExportDestination',
NON_REIMBURSABLE_EXPENSE_ACCOUNT: 'nonReimbursableExpensesAccount',
RECEIVABLE_ACCOUNT: 'receivableAccount',
Expand Down Expand Up @@ -6821,6 +6823,7 @@ const CONST = {
MODIFIED_AMOUNT: 'modifiedAmount',
MODIFIED_DATE: 'modifiedDate',
INCREASED_DISTANCE: 'increasedDistance',
INACTIVE_VENDOR: 'inactiveVendor',
PROHIBITED_EXPENSE: 'prohibitedExpense',
NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense',
OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit',
Expand Down
3 changes: 2 additions & 1 deletion src/hooks/useViolations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', 'waypoints', 'none'] as const;
const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'vendor', 'waypoints', 'none'] as const;

type ViolationField = TupleToUnion<typeof validationFields>;

Expand All @@ -24,6 +24,7 @@ const violationNameToField: Record<ViolationName, (violation: TransactionViolati
duplicatedTransaction: () => 'merchant',
fieldRequired: () => 'merchant',
futureDate: () => 'date',
inactiveVendor: () => 'vendor',
invoiceMarkup: () => 'amount',
maxAge: () => 'date',
missingCategory: () => 'category',
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8607,6 +8607,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
duplicatedTransaction: 'Möglicherweise dupliziert',
fieldRequired: 'Berichtsfelder sind erforderlich',
futureDate: 'Zukünftiges Datum nicht erlaubt',
inactiveVendor: 'Anbieter nicht mehr gültig',
invoiceMarkup: (invoiceMarkup: number) => `Um ${invoiceMarkup}% erhöht`,
maxAge: (maxAge: number) => `Datum ist älter als ${maxAge} Tage`,
missingCategory: 'Fehlende Kategorie',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8611,6 +8611,7 @@ const translations = {
duplicatedTransaction: 'Potential duplicate',
fieldRequired: 'Report fields are required',
futureDate: 'Future date not allowed',
inactiveVendor: 'Vendor no longer valid',
invoiceMarkup: (invoiceMarkup: number) => `Marked up by ${invoiceMarkup}%`,
maxAge: (maxAge: number) => `Date older than ${maxAge} days`,
missingCategory: 'Missing category',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8780,6 +8780,7 @@ ${amount} para ${merchant} - ${date}`,
duplicatedTransaction: 'Posible duplicado',
fieldRequired: 'Los campos del informe son obligatorios',
futureDate: 'Fecha futura no permitida',
inactiveVendor: 'El proveedor ya no es válido',
invoiceMarkup: (invoiceMarkup) => `Incrementado un ${invoiceMarkup}%`,
maxAge: (maxAge) => `Fecha de más de ${maxAge} días`,
missingCategory: 'Falta categoría',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8639,6 +8639,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
duplicatedTransaction: 'Doublon potentiel',
fieldRequired: 'Les champs de note de frais sont obligatoires',
futureDate: 'Date future non autorisée',
inactiveVendor: 'Fournisseur plus valide',
invoiceMarkup: (invoiceMarkup: number) => `Majoration de ${invoiceMarkup} %`,
maxAge: (maxAge: number) => `Date antérieure de plus de ${maxAge} jours`,
missingCategory: 'Catégorie manquante',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8600,6 +8600,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
duplicatedTransaction: 'Duplice potenziale',
fieldRequired: 'I campi del report sono obbligatori',
futureDate: 'Data futura non consentita',
inactiveVendor: 'Fornitore non più valido',
invoiceMarkup: (invoiceMarkup: number) => `Maggiorato del ${invoiceMarkup}%`,
maxAge: (maxAge: number) => `Data precedente a ${maxAge} giorni`,
missingCategory: 'Categoria mancante',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8491,6 +8491,7 @@ ${reportName}
duplicatedTransaction: '重複の可能性',
fieldRequired: 'レポートの項目は必須です',
futureDate: '将来の日付は使用できません',
inactiveVendor: 'ベンダーは無効です',
invoiceMarkup: (invoiceMarkup: number) => `${invoiceMarkup}%値上げ済み`,
maxAge: (maxAge: number) => `日付が${maxAge}日より前です`,
missingCategory: 'カテゴリが未選択です',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8569,6 +8569,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
duplicatedTransaction: 'Mogelijke duplicaat',
fieldRequired: 'Rapportvelden zijn verplicht',
futureDate: 'Toekomstige datum niet toegestaan',
inactiveVendor: 'Leverancier niet meer geldig',
invoiceMarkup: (invoiceMarkup: number) => `Met ${invoiceMarkup}% verhoogd`,
maxAge: (maxAge: number) => `Datum ouder dan ${maxAge} dagen`,
missingCategory: 'Ontbrekende categorie',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8556,6 +8556,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
duplicatedTransaction: 'Potencjalny duplikat',
fieldRequired: 'Pola raportu są wymagane',
futureDate: 'Przyszła data jest niedozwolona',
inactiveVendor: 'Dostawca nie jest już prawidłowy',
invoiceMarkup: (invoiceMarkup: number) => `Podwyższono o ${invoiceMarkup}%`,
maxAge: (maxAge: number) => `Data starsza niż ${maxAge} dni`,
missingCategory: 'Brak kategorii',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8562,6 +8562,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
duplicatedTransaction: 'Possível duplicata',
fieldRequired: 'Os campos do relatório são obrigatórios',
futureDate: 'Data futura não permitida',
inactiveVendor: 'Fornecedor não é mais válido',
invoiceMarkup: (invoiceMarkup: number) => `Reajustado em ${invoiceMarkup}%`,
maxAge: (maxAge: number) => `Data anterior a ${maxAge} dias`,
missingCategory: 'Categoria ausente',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8339,6 +8339,7 @@ ${reportName}
duplicatedTransaction: '可能重复',
fieldRequired: '报表字段为必填项',
futureDate: '不允许使用未来日期',
inactiveVendor: '供应商不再有效',
invoiceMarkup: (invoiceMarkup: number) => `加价 ${invoiceMarkup}%`,
maxAge: (maxAge: number) => `日期早于 ${maxAge} 天`,
missingCategory: '缺少类别',
Expand Down
8 changes: 8 additions & 0 deletions src/libs/API/parameters/UpdateMoneyRequestVendorParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type UpdateMoneyRequestVendorParams = {
transactionID: string;
reportActionID: string;
vendorID: string;
isManuallySet: boolean;
};

export default UpdateMoneyRequestVendorParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ export type {default as SetReportNameParams} from './SetReportNameParams';
export type {default as DeleteReportFieldParams} from './DeleteReportFieldParams';
export type {default as CompleteSplitBillParams} from './CompleteSplitBillParams';
export type {default as UpdateMoneyRequestParams} from './UpdateMoneyRequestParams';
export type {default as UpdateMoneyRequestVendorParams} from './UpdateMoneyRequestVendorParams';
export type {default as RequestMoneyParams} from './RequestMoneyParams';
export type {default as SplitBillParams} from './SplitBillParams';
export type {SplitTransactionParams, SplitTransactionSplitsParam, RevertSplitTransactionParams} from './SplitTransactionParams';
Expand Down
4 changes: 4 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ const WRITE_COMMANDS = {
UPDATE_QUICKBOOKS_ONLINE_SYNC_CUSTOMERS: 'UpdateQuickbooksOnlineSyncCustomers',
UPDATE_QUICKBOOKS_ONLINE_SYNC_CLASSES: 'UpdateQuickbooksOnlineSyncClasses',
UPDATE_QUICKBOOKS_ONLINE_MAPPING: 'UpdateQuickbooksOnlineMapping',
UPDATE_MONEY_REQUEST_VENDOR: 'UpdateMoneyRequestVendor',
UPDATE_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_BILL_DEFAULT_VENDOR: 'UpdateQuickbooksOnlineNonReimbursableBillDefaultVendor',
UPDATE_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_CREDIT_CARD_DEFAULT_VENDOR: 'UpdateQuickbooksOnlineNonReimbursableCreditCardDefaultVendor',
UPDATE_QUICKBOOKS_ONLINE_AUTO_SYNC: 'UpdateQuickbooksOnlineAutoSync',
UPDATE_QUICKBOOKS_ONLINE_SYNC_PEOPLE: 'UpdateQuickbooksOnlineSyncPeople',
UPDATE_QUICKBOOKS_ONLINE_REIMBURSEMENT_ACCOUNT_ID: 'UpdateQuickbooksOnlineReimbursementAccountID',
Expand Down Expand Up @@ -957,7 +959,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_SYNC_LOCATIONS]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_SYNC_CUSTOMERS]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_SYNC_CLASSES]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_MONEY_REQUEST_VENDOR]: Parameters.UpdateMoneyRequestVendorParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_BILL_DEFAULT_VENDOR]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_NON_REIMBURSABLE_CREDIT_CARD_DEFAULT_VENDOR]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_REIMBURSABLE_EXPENSES_ACCOUNT]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_AUTO_SYNC]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
[WRITE_COMMANDS.UPDATE_QUICKBOOKS_ONLINE_SYNC_PEOPLE]: Parameters.UpdateQuickbooksOnlineGenericTypeParams;
Expand Down
2 changes: 2 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1052,6 +1052,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
subRates: CONST.RED_BRICK_ROAD_PENDING_ACTION,
comment: CONST.RED_BRICK_ROAD_PENDING_ACTION,
hold: CONST.RED_BRICK_ROAD_PENDING_ACTION,
vendor: CONST.RED_BRICK_ROAD_PENDING_ACTION,
waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION,
isLoading: CONST.RED_BRICK_ROAD_PENDING_ACTION,
type: CONST.RED_BRICK_ROAD_PENDING_ACTION,
Expand Down Expand Up @@ -1179,6 +1180,7 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
source: 'string',
originalTransactionID: 'string',
liabilityType: CONST.TRANSACTION.LIABILITY_TYPE,
vendor: 'object',
splits: 'array',
dismissedViolations: 'object',
splitExpenses: 'array',
Expand Down
43 changes: 43 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
PolicyFeatureName,
Rate,
Tenant,
Vendor,
} from '@src/types/onyx/Policy';
import type PolicyEmployee from '@src/types/onyx/PolicyEmployee';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
Expand Down Expand Up @@ -1999,6 +2000,45 @@ function getConnectedIntegration(policy: Policy | undefined, connectionNames: re
return connectionNames.find((integration) => !!policy?.connections?.[integration]);
}

/**
* QBO vendor feature gate. Returns true when the workspace has the `vendorMatching` beta enabled
* AND QBO is connected with an individual card transaction non-reimbursable export type — the only
* scope the Vendor field is shown for. Mirrors `QuickbooksOnline::hasVendorFeature` on the PHP side
* so the App and backend agree on which workspaces see the field.
*/
function hasVendorFeature(policy: OnyxEntry<Policy>, isVendorMatchingBetaEnabled: boolean): boolean {
if (!isVendorMatchingBetaEnabled || !policy) {
return false;
}
const qboConnection = policy.connections?.[CONST.POLICY.CONNECTIONS.NAME.QBO];
if (!qboConnection) {
return false;
}
const exportDestination = qboConnection.config?.nonReimbursableExpensesExportDestination;
return exportDestination === CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD || exportDestination === CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD;
}

/**
* Returns the QBO vendor list imported into the workspace (empty array when QBO isn't connected or
* the sync hasn't populated vendors yet). Source of truth for the workspace Vendors tab and the
* vendor selector RHP.
*/
function getQBOVendors(policy: OnyxEntry<Policy>): Vendor[] {
return policy?.connections?.[CONST.POLICY.CONNECTIONS.NAME.QBO]?.data?.vendors ?? [];
}

/**
* Look up a single QBO vendor by `externalID`. Used to resolve the vendor name for display when
* only the ID is stored on the transaction NVP. Returns undefined when the ID isn't found
* (which happens after a vendor is deleted from QBO — see the inactive-vendor violation).
*/
function getQBOVendorByID(policy: OnyxEntry<Policy>, vendorID: string | undefined): Vendor | undefined {
if (!vendorID) {
return undefined;
}
return getQBOVendors(policy).find((vendor) => vendor.id === vendorID);
}

function getValidConnectedIntegration(policy: Policy | undefined, connectionNames: readonly ConnectionName[] = getAccountingConnectionNames()) {
return connectionNames.find((integration) => !!policy?.connections?.[integration] && !isConnectionUnverified(policy, integration));
}
Expand Down Expand Up @@ -2384,6 +2424,9 @@ export {
getConnectedIntegration,
getConnectedIntegrationNamesForPolicies,
getConnectionExporters,
getQBOVendorByID,
getQBOVendors,
hasVendorFeature,
getValidConnectedIntegration,
getCountOfEnabledTagsOfList,
getIneligibleInvitees,
Expand Down
Loading
Loading