Skip to content

Add prohibited expense feature #56550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ce32a0e
Add alcohol and hotel incidentals as prohibited expenses
AndrewGable Feb 7, 2025
1745cf3
Fix type errors and add API commands
AndrewGable Feb 7, 2025
eab99cf
Update switch on name
AndrewGable Feb 7, 2025
3566d1a
Add spanish
AndrewGable Feb 7, 2025
cbe5404
Add more types
AndrewGable Feb 7, 2025
caddee5
Fixing more lint
AndrewGable Feb 7, 2025
a1bc562
Add api types
AndrewGable Feb 7, 2025
ba86262
Fix more lint
AndrewGable Feb 7, 2025
6b7d9c5
Merge branch 'main' into smarterscan
AndrewGable Feb 7, 2025
6bb05f8
Remove duplicate row
AndrewGable Feb 7, 2025
e04647b
Fix string bug
AndrewGable Feb 7, 2025
3c6c4fd
Remove unused variable
AndrewGable Feb 7, 2025
f90daf6
Add ;
AndrewGable Feb 7, 2025
a90247d
Change submodule
AndrewGable Feb 7, 2025
38fd924
Tweak casing of items
AndrewGable Feb 7, 2025
528609b
Alphabatize all items
AndrewGable Feb 7, 2025
33a8ba3
JSON stringify the response sent to API
AndrewGable Feb 7, 2025
7f16d2a
Fix stringify
AndrewGable Feb 7, 2025
9329f47
Tweak strings one more time
AndrewGable Feb 7, 2025
aa1d95c
Translation and const
johnmlee101 Feb 8, 2025
cb78304
Last changes
johnmlee101 Feb 8, 2025
50f361e
Prettier
johnmlee101 Feb 8, 2025
99d87c8
Add en espanol
johnmlee101 Feb 8, 2025
bdda7a6
add fake toggle
dangrous Feb 8, 2025
6eb81b8
Revert "add fake toggle"
dangrous Feb 8, 2025
1c3049c
add receiptLineItems beta
dangrous Feb 12, 2025
67d67fd
put prohibitedExpense setup behind the beta
dangrous Feb 12, 2025
20b1b3e
revert to original value if error
dangrous Feb 12, 2025
8ad3095
Merge branch 'main' into smarterscan
dangrous Feb 12, 2025
39cdd2f
prettier
dangrous Feb 13, 2025
ebc1bee
fix type errors
dangrous Feb 13, 2025
94dd8d9
merge conflicts
dangrous Mar 19, 2025
3677d8f
merge conflicts
dangrous Mar 21, 2025
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
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,7 @@ const CONST = {
NEWDOT_INTERNATIONAL_DEPOSIT_BANK_ACCOUNT: 'newDotInternationalDepositBankAccount',
NSQS: 'nsqs',
CUSTOM_RULES: 'customRules',
RECEIPT_LINE_ITEMS: 'receiptLineItems',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -4994,6 +4995,7 @@ const CONST = {
MISSING_TAG: 'missingTag',
MODIFIED_AMOUNT: 'modifiedAmount',
MODIFIED_DATE: 'modifiedDate',
PROHIBITED_EXPENSE: 'prohibitedExpense',
NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense',
OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit',
OVER_CATEGORY_LIMIT: 'overCategoryLimit',
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/rules/billable',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/billable` as const,
},
RULES_PROHIBITED_DEFAULT: {
route: 'settings/workspaces/:policyID/rules/prohibited',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/prohibited` as const,
},
RULES_CUSTOM: {
route: 'settings/workspaces/:policyID/rules/custom',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/custom` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,7 @@ const SCREENS = {
RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age',
RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default',
RULES_CUSTOM: 'Rules_Custom',
RULES_PROHIBITED_DEFAULT: 'Rules_Prohibited_Default',
PER_DIEM: 'Per_Diem',
PER_DIEM_IMPORT: 'Per_Diem_Import',
PER_DIEM_IMPORTED: 'Per_Diem_Imported',
Expand Down
1 change: 1 addition & 0 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const receiptImageViolationNames: OnyxTypes.ViolationName[] = [
CONST.VIOLATIONS.RECEIPT_NOT_SMART_SCANNED,
CONST.VIOLATIONS.CASH_EXPENSE_WITH_NO_RECEIPT,
CONST.VIOLATIONS.SMARTSCAN_FAILED,
CONST.VIOLATIONS.PROHIBITED_EXPENSE,
];

const receiptFieldViolationNames: OnyxTypes.ViolationName[] = [CONST.VIOLATIONS.MODIFIED_AMOUNT, CONST.VIOLATIONS.MODIFIED_DATE];
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useViolations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const violationNameToField: Record<ViolationName, (violation: TransactionViolati
overLimit: () => 'amount',
overLimitAttendee: () => 'amount',
perDayLimit: () => 'amount',
prohibitedExpense: () => 'receipt',
receiptNotSmartScanned: () => 'receipt',
receiptRequired: () => 'receipt',
customRules: (violation) => {
Expand Down
27 changes: 27 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ import type {
ViolationsOverCategoryLimitParams,
ViolationsOverLimitParams,
ViolationsPerDayLimitParams,
ViolationsProhibitedExpenseParams,
ViolationsReceiptRequiredParams,
ViolationsRterParams,
ViolationsTagOutOfPolicyParams,
Expand Down Expand Up @@ -4647,6 +4648,15 @@ const translations = {
eReceipts: 'eReceipts',
eReceiptsHint: 'eReceipts are auto-created',
eReceiptsHintLink: 'for most USD credit transactions',
prohibitedDefaultDescription:
'Flag any receipts where alcohol, gambling, or other restricted items appear. Expenses with receipts where these line items appear will require manual review.',
prohibitedExpenses: 'Prohibited expenses',
none: 'None',
alcohol: 'Alcohol',
hotelIncidentals: 'Hotel incidentals',
gambling: 'Gambling',
tobacco: 'Tobacco',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on the backend we're doing drugsAndTobacco but could change it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(update - i just updated the BE to be just tobacco)

adultEntertainment: 'Adult entertainment',
},
expenseReportRules: {
examples: 'Examples:',
Expand Down Expand Up @@ -5362,6 +5372,23 @@ const translations = {
}
return message;
},
prohibitedExpense: ({prohibitedExpenseType}: ViolationsProhibitedExpenseParams) => {
const preMessage = 'Prohibited Expense: ';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi!, coming from #60216 (BugZero Checklist), the copy has capitalization errors.

switch (prohibitedExpenseType) {
case 'alcohol':
return `${preMessage} Alcohol`;
case 'gambling':
return `${preMessage} Gambling`;
case 'tobacco':
return `${preMessage} Tobacco`;
case 'adultEntertainment':
return `${preMessage} Adult Entertainment`;
case 'hotelIncidentals':
return `${preMessage} Hotel Incidentals`;
default:
return `${preMessage}${prohibitedExpenseType}`;
}
},
customRules: ({message}: ViolationsCustomRulesParams) => message,
reviewRequired: 'Review required',
rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member, rterType}: ViolationsRterParams) => {
Expand Down
27 changes: 27 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ import type {
ViolationsOverCategoryLimitParams,
ViolationsOverLimitParams,
ViolationsPerDayLimitParams,
ViolationsProhibitedExpenseParams,
ViolationsReceiptRequiredParams,
ViolationsRterParams,
ViolationsTagOutOfPolicyParams,
Expand Down Expand Up @@ -4718,6 +4719,15 @@ const translations = {
eReceipts: 'Recibos electrónicos',
eReceiptsHint: 'Los recibos electrónicos se crean automáticamente',
eReceiptsHintLink: 'para la mayoría de las transacciones en USD',
prohibitedDefaultDescription:
'Marque cualquier recibo donde aparezcan alcohol, apuestas u otros artículos restringidos. Los gastos con recibos que incluyan estos conceptos requerirán una revisión manual.',
prohibitedExpenses: 'Gastos prohibidos',
none: 'Ninguno',
alcohol: 'Alcohol',
hotelIncidentals: 'Gastos incidentales de hotel',
gambling: 'Juegos de azar',
tobacco: 'Tabaco',
adultEntertainment: 'Entretenimiento para adultos',
},
expenseReportRules: {
examples: 'Ejemplos:',
Expand Down Expand Up @@ -5877,6 +5887,23 @@ const translations = {
}
return message;
},
prohibitedExpense: ({prohibitedExpenseType}: ViolationsProhibitedExpenseParams) => {
const preMessage = 'Gasto prohibido: ';
switch (prohibitedExpenseType) {
case 'alcohol':
return `${preMessage} Alcohol`;
case 'gambling':
return `${preMessage} Juego de apuestas`;
case 'tobacco':
return `${preMessage} Tobacco`;
case 'adultEntertainment':
return `${preMessage} Entretenimiento para adultos`;
case 'hotelIncidentals':
return `${preMessage} Gastos adicionales de hotel`;
default:
return `${preMessage}${prohibitedExpenseType}`;
}
},
customRules: ({message}: ViolationsCustomRulesParams) => message,
reviewRequired: 'Revisión requerida',
rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member, rterType}: ViolationsRterParams) => {
Expand Down
3 changes: 3 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,8 @@ type ViolationsRterParams = {

type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined;

type ViolationsProhibitedExpenseParams = {prohibitedExpenseType: string};

type ViolationsTaxOutOfPolicyParams = {taxName?: string} | undefined;

type PaySomeoneParams = {name?: string} | undefined;
Expand Down Expand Up @@ -784,6 +786,7 @@ export type {
ViolationsCustomRulesParams,
ViolationsRterParams,
ViolationsTagOutOfPolicyParams,
ViolationsProhibitedExpenseParams,
ViolationsTaxOutOfPolicyParams,
WaitingOnBankAccountParams,
WalletProgramParams,
Expand Down
11 changes: 11 additions & 0 deletions src/libs/API/parameters/SetPolicyProhibitedExpensesParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type SetPolicyProhibitedExpensesParams = {
policyID: string;
/**
* A JSON string representing the prohibited expenses
*
* e.g. {'alcohol': true, 'gambling': true, 'hotelIncidentals': true, 'tobacco': true}
*/
prohibitedExpenses: string;
};

export default SetPolicyProhibitedExpensesParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,4 @@ export type {default as GetCorpayOnboardingFieldsParams} from './GetCorpayOnboar
export type {SaveCorpayOnboardingCompanyDetailsParams} from './SaveCorpayOnboardingCompanyDetailsParams';
export type {default as AcceptSpotnanaTermsParams} from './AcceptSpotnanaTermsParams';
export type {default as SaveCorpayOnboardingBeneficialOwnerParams} from './SaveCorpayOnboardingBeneficialOwnerParams';
export type {default as SetPolicyProhibitedExpensesParams} from './SetPolicyProhibitedExpensesParams';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ const WRITE_COMMANDS = {
SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled',
SET_POLICY_EXPENSE_MAX_AMOUNT_NO_RECEIPT: 'SetPolicyExpenseMaxAmountNoReceipt',
SET_POLICY_EXPENSE_MAX_AMOUNT: 'SetPolicyExpenseMaxAmount',
SET_POLICY_PROHIBITED_EXPENSES: 'SetPolicyProhibitedExpenses',
SET_POLICY_EXPENSE_MAX_AGE: ' SetPolicyExpenseMaxAge',
UPDATE_CUSTOM_RULES: 'UpdateCustomRules',
SET_POLICY_BILLABLE_MODE: ' SetPolicyBillableMode',
Expand Down Expand Up @@ -794,6 +795,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.RESET_SMS_DELIVERY_FAILURE_STATUS]: Parameters.ResetSMSDeliveryFailureStatusParams;
[WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_COMPANY_DETAILS]: Parameters.SaveCorpayOnboardingCompanyDetailsParams;
[WRITE_COMMANDS.SAVE_CORPAY_ONBOARDING_BENEFICIAL_OWNER]: Parameters.SaveCorpayOnboardingBeneficialOwnerParams;
[WRITE_COMMANDS.SET_POLICY_PROHIBITED_EXPENSES]: Parameters.SetPolicyProhibitedExpensesParams;

[WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams;
[WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams;
Expand Down
1 change: 1 addition & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ function validateTransactionViolationDraftProperty(key: keyof TransactionViolati
tooltip: 'string',
message: 'string',
field: 'string',
prohibitedExpenseRule: 'string',
});
case 'showInReview':
return validateBoolean(value);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesMaxExpenseAgePage').default,
[SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesBillableDefaultPage').default,
[SCREENS.WORKSPACE.RULES_CUSTOM]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesCustomPage').default,
[SCREENS.WORKSPACE.RULES_PROHIBITED_DEFAULT]: () => require<ReactComponentModule>('../../../../pages/workspace/rules/RulesProhibitedDefaultPage').default,
[SCREENS.WORKSPACE.PER_DIEM_IMPORT]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/ImportPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/ImportedPerDiemPage').default,
[SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE,
SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT,
SCREENS.WORKSPACE.RULES_CUSTOM,
SCREENS.WORKSPACE.RULES_PROHIBITED_DEFAULT,
],
[SCREENS.WORKSPACE.PER_DIEM]: [
SCREENS.WORKSPACE.PER_DIEM_IMPORT,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.WORKSPACE.RULES_CUSTOM]: {
path: ROUTES.RULES_CUSTOM.route,
},
[SCREENS.WORKSPACE.RULES_PROHIBITED_DEFAULT]: {
path: ROUTES.RULES_PROHIBITED_DEFAULT.route,
},
[SCREENS.WORKSPACE.PER_DIEM_IMPORT]: {
path: ROUTES.WORKSPACE_PER_DIEM_IMPORT.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: {
policyID: string;
};
[SCREENS.WORKSPACE.RULES_PROHIBITED_DEFAULT]: {
policyID: string;
};
[SCREENS.WORKSPACE.RULES_CUSTOM]: {
policyID: string;
};
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ function canUseCustomRules(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.CUSTOM_RULES) || canUseAllBetas(betas);
}

function canUseProhibitedExpenses(betas: OnyxEntry<Beta[]>): boolean {
return !!betas?.includes(CONST.BETAS.RECEIPT_LINE_ITEMS) || canUseAllBetas(betas);
}

export default {
canUseDefaultRooms,
canUseLinkPreviews,
Expand All @@ -65,4 +69,5 @@ export default {
canUseInternationalBankAccount,
canUseNSQS,
canUseCustomRules,
canUseProhibitedExpenses,
};
4 changes: 4 additions & 0 deletions src/libs/Violations/ViolationsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,10 @@ const ViolationsUtils = {
return translate('violations.taxRequired');
case 'hold':
return translate('violations.hold');
case CONST.VIOLATIONS.PROHIBITED_EXPENSE:
return translate('violations.prohibitedExpense', {
prohibitedExpenseType: violation.data?.prohibitedExpenseRule ?? '',
});
default:
// The interpreter should never get here because the switch cases should be exhaustive.
// If typescript is showing an error on the assertion below it means the switch statement is out of
Expand Down
56 changes: 55 additions & 1 deletion src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import type {
SetPolicyDefaultReportTitleParams,
SetPolicyPreventMemberCreatedTitleParams,
SetPolicyPreventSelfApprovalParams,
SetPolicyProhibitedExpensesParams,
SetPolicyRulesEnabledParams,
SetWorkspaceApprovalModeParams,
SetWorkspaceAutoReportingFrequencyParams,
Expand Down Expand Up @@ -96,7 +97,7 @@ import type {
Transaction,
} from '@src/types/onyx';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, Rate, TaxRate} from '@src/types/onyx/Policy';
import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, ProhibitedExpenses, Rate, TaxRate} from '@src/types/onyx/Policy';
import type {OnyxData} from '@src/types/onyx/Request';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import {buildOptimisticPolicyCategories} from './Category';
Expand Down Expand Up @@ -3821,6 +3822,58 @@ function setPolicyMaxExpenseAmount(policyID: string, maxExpenseAmount: string) {
API.write(WRITE_COMMANDS.SET_POLICY_EXPENSE_MAX_AMOUNT, parameters, onyxData);
}

/**
*
* @param policyID
* @param prohibitedExpenses
*/
function setPolicyProhibitedExpenses(policyID: string, prohibitedExpenses: ProhibitedExpenses) {
const policy = getPolicy(policyID);
const originalProhibitedExpenses = policy?.prohibitedExpenses;
const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
prohibitedExpenses,
pendingFields: {
prohibitedExpenses: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
},
],
successData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
pendingFields: {prohibitedExpenses: null},
errorFields: null,
},
},
],
failureData: [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
prohibitedExpenses: originalProhibitedExpenses,
pendingFields: {prohibitedExpenses: null},
errorFields: {prohibitedExpenses: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage')},
},
},
],
};

const parameters: SetPolicyProhibitedExpensesParams = {
policyID,
prohibitedExpenses: JSON.stringify(prohibitedExpenses),
};

API.write(WRITE_COMMANDS.SET_POLICY_PROHIBITED_EXPENSES, parameters, onyxData);
}

/**
* Call the API to set the max expense age for the given policy
* @param policyID - id of the policy to set the max expense age
Expand Down Expand Up @@ -4983,6 +5036,7 @@ export {
setPolicyMaxExpenseAmount,
setPolicyMaxExpenseAge,
updateCustomRules,
setPolicyProhibitedExpenses,
setPolicyBillableMode,
disableWorkspaceBillableExpenses,
setWorkspaceEReceiptsEnabled,
Expand Down
Loading