Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
158 changes: 148 additions & 10 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@
getPolicyNameByID,
getPolicyRole,
getRuleApprovers,
getSortedTagKeys,
getSubmitToAccountID,
hasDependentTags as hasDependentTagsPolicyUtils,
hasDynamicExternalWorkflow,
Expand Down Expand Up @@ -290,6 +291,7 @@
getRecentTransactions,
getReimbursable,
getTag,
getTagArrayFromName,
getTaxAmount,
getTaxCode,
getTaxName,
Expand Down Expand Up @@ -1055,7 +1057,7 @@
};

let conciergeReportIDOnyxConnect: OnyxEntry<string>;
Onyx.connect({

Check warning on line 1060 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportIDOnyxConnect = value;
Expand All @@ -1063,7 +1065,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 1068 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -1081,7 +1083,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 1086 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (deprecatedCurrentUserAccountID) {
Expand All @@ -1093,7 +1095,7 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 1098 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
Expand All @@ -1101,7 +1103,7 @@

let allPolicies: OnyxCollection<Policy>;
let policiesArray: Policy[] = [];
Onyx.connect({

Check warning on line 1106 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1111,7 +1113,7 @@
});

let allPolicyDrafts: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 1116 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_DRAFTS,
waitForCollectionCallback: true,
callback: (value) => (allPolicyDrafts = value),
Expand All @@ -1119,7 +1121,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 1124 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -1155,14 +1157,14 @@
});

let betaConfiguration: OnyxEntry<BetaConfiguration> = {};
Onyx.connect({

Check warning on line 1160 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETA_CONFIGURATION,
callback: (value) => (betaConfiguration = value ?? {}),
});

let deprecatedAllTransactions: OnyxCollection<Transaction> = {};
let deprecatedReportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 1167 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1188,7 +1190,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1193 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -2077,27 +2079,31 @@
*/
function pushTransactionViolationsOnyxData(
onyxData: OnyxData<
typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES | typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.POLICY_TAGS | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
| typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES
| typeof ONYXKEYS.COLLECTION.POLICY
| typeof ONYXKEYS.COLLECTION.POLICY_TAGS
| typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
| typeof ONYXKEYS.COLLECTION.TRANSACTION
>,
policyData: PolicyData,
policyUpdate: Partial<Policy> = {},
categoriesUpdate: Record<string, Partial<PolicyCategory>> = {},
tagListsUpdate: Record<string, Partial<PolicyTagList>> = {},
) {
const nonInvoiceReportTransactionsAndViolations = policyData.reports.reduce<ReportTransactionsAndViolations[]>((acc, report) => {
const nonInvoiceReportItems = policyData.reports.reduce<Array<{report: Report; transactionsAndViolations: ReportTransactionsAndViolations}>>((acc, report) => {
// Skipping invoice reports since they should not have any category or tag violations
if (isInvoiceReport(report)) {
return acc;
}
const reportTransactionsAndViolations = policyData.transactionsAndViolations[report.reportID];
if (!isEmptyObject(reportTransactionsAndViolations) && !isEmptyObject(reportTransactionsAndViolations.transactions)) {
acc.push(reportTransactionsAndViolations);
acc.push({report, transactionsAndViolations: reportTransactionsAndViolations});
}
return acc;
}, []);

if (nonInvoiceReportTransactionsAndViolations.length === 0) {
return;
if (nonInvoiceReportItems.length === 0) {
return [];
}

const updatedTagListsNames = Object.keys(tagListsUpdate);
Expand All @@ -2108,7 +2114,7 @@
const isTagListsUpdateEmpty = updatedTagListsNames.length === 0;
const isCategoriesUpdateEmpty = updatedCategoriesNames.length === 0;
if (isPolicyUpdateEmpty && isTagListsUpdateEmpty && isCategoriesUpdateEmpty) {
return;
return [];
}

// Merge the existing policy with the optimistic updates
Expand Down Expand Up @@ -2168,19 +2174,149 @@
}, {}),
};

const hasDependentTags = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticTagLists);
const hasDependentTagsValue = hasDependentTagsPolicyUtils(optimisticPolicy, optimisticTagLists);

// Compute sole remaining values for auto-selection when a policy value is deleted
const enabledCategoryKeys = Object.entries(optimisticCategories)
.filter(([, cat]) => cat.enabled && cat.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
.map(([key]) => key);
const singleRemainingCategory = enabledCategoryKeys.length === 1 ? enabledCategoryKeys.at(0) : undefined;

const tagListKeys = Object.keys(optimisticTagLists);

// Single-level tag auto-selection
let singleRemainingTag: string | undefined;
if (tagListKeys.length === 1) {
const tagListName = tagListKeys.at(0) ?? '';
const enabledTagKeys = Object.entries(optimisticTagLists[tagListName]?.tags ?? {})
.filter(([, tag]) => tag.enabled && tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
.map(([key]) => key);
singleRemainingTag = enabledTagKeys.length === 1 ? enabledTagKeys.at(0) : undefined;
}

// Multi-level tag auto-selection (per level)
let perLevelSingleTag: Array<string | undefined> = [];
if (tagListKeys.length > 1) {
const sortedTagKeys = getSortedTagKeys(optimisticTagLists);
perLevelSingleTag = sortedTagKeys.map((key) => {
const tags = optimisticTagLists[key]?.tags ?? {};
const enabledKeys = Object.entries(tags)
.filter(([, tag]) => tag.enabled && tag.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
.map(([k]) => k);
return enabledKeys.length === 1 ? enabledKeys.at(0) : undefined;
});
}

// Tax auto-selection
const optimisticTaxes = optimisticPolicy.taxRates?.taxes ?? {};
const enabledTaxKeys = Object.entries(optimisticTaxes)
.filter(([, tax]) => !tax.isDisabled && tax.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE)
.map(([key]) => key);
const singleRemainingTaxCode = enabledTaxKeys.length === 1 ? enabledTaxKeys.at(0) : undefined;

// Collect auto-selected transaction updates to return to callers for the API request
const autoSelections: Array<{transactionID: string; category?: string; tag?: string; taxCode?: string}> = [];

// Iterate through all policy reports to find transactions that need optimistic violations
for (const {transactions, violations} of nonInvoiceReportTransactionsAndViolations) {
for (const {
report,
transactionsAndViolations: {transactions, violations},
} of nonInvoiceReportItems) {
const isEligibleForAutoSelect = isOpenOrProcessingReport(report);

for (const transaction of Object.values(transactions)) {
let modifiedTransaction = transaction;
const transactionUpdates: Partial<Transaction> = {};
const transactionRollback: Partial<Transaction> = {};

if (isEligibleForAutoSelect) {
// Category auto-select: if the transaction's category is out of policy and only one enabled category remains
if (singleRemainingCategory && transaction.category && !optimisticCategories[transaction.category]?.enabled) {
transactionUpdates.category = singleRemainingCategory;
transactionRollback.category = transaction.category;
}

// Single-level tag auto-select
if (tagListKeys.length === 1 && singleRemainingTag && transaction.tag) {
const tagListName = tagListKeys.at(0) ?? '';
const isTagInPolicy = !!optimisticTagLists[tagListName]?.tags?.[transaction.tag]?.enabled;
if (!isTagInPolicy) {
transactionUpdates.tag = singleRemainingTag;
transactionRollback.tag = transaction.tag;
}
}

// Multi-level tag auto-select
if (tagListKeys.length > 1 && transaction.tag) {
const sortedTagKeys = getSortedTagKeys(optimisticTagLists);
const currentTags = getTagArrayFromName(transaction.tag);
let anyTagChanged = false;
const newTags = [...currentTags];

for (let i = 0; i < sortedTagKeys.length; i++) {
const currentTag = currentTags.at(i);
if (!currentTag) {
continue;
}
const sortedTagKey = sortedTagKeys.at(i) ?? '';
const levelTags = optimisticTagLists[sortedTagKey]?.tags ?? {};
const isInPolicy = !!levelTags[currentTag]?.enabled;
const singleTag = perLevelSingleTag.at(i);
if (!isInPolicy && singleTag) {
newTags[i] = singleTag;
anyTagChanged = true;
}
}

if (anyTagChanged) {
transactionUpdates.tag = newTags.join(CONST.COLON);
transactionRollback.tag = transaction.tag;
}
}

// Tax auto-select: if the transaction's tax code is out of policy and only one enabled tax remains
if (singleRemainingTaxCode && transaction.taxCode) {
const isTaxInPolicy = !!optimisticTaxes[transaction.taxCode] && !optimisticTaxes[transaction.taxCode].isDisabled;
if (!isTaxInPolicy) {
transactionUpdates.taxCode = singleRemainingTaxCode;
transactionRollback.taxCode = transaction.taxCode;
}
}
}

// If auto-selection modified the transaction, push optimistic transaction updates
if (Object.keys(transactionUpdates).length > 0) {
modifiedTransaction = {...transaction, ...transactionUpdates};

onyxData.optimisticData?.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: transactionUpdates,
});

onyxData.failureData?.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
value: transactionRollback,
});

// Collect auto-selection data for the API request
autoSelections.push({
transactionID: transaction.transactionID,
...(transactionUpdates.category !== undefined && {category: transactionUpdates.category}),
...(transactionUpdates.tag !== undefined && {tag: transactionUpdates.tag}),
...(transactionUpdates.taxCode !== undefined && {taxCode: transactionUpdates.taxCode}),
});
}

const existingViolations = violations[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transaction.transactionID}`];
const optimisticViolations = ViolationsUtils.getViolationsOnyxData(
transaction,
modifiedTransaction,
existingViolations ?? [],
optimisticPolicy,
optimisticTagLists,
optimisticCategories,
hasDependentTags,
hasDependentTagsValue,
false,
);

Expand All @@ -2194,6 +2330,8 @@
}
}
}

return autoSelections;
}

/**
Expand Down
9 changes: 7 additions & 2 deletions src/libs/actions/Policy/Category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ type SetWorkspaceCategoryEnabledParams = {

function appendSetupCategoriesOnboardingData(
onyxData: OnyxData<
typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES | typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT | typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
| typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES
| typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES_DRAFT
| typeof ONYXKEYS.COLLECTION.REPORT
| typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS
| typeof ONYXKEYS.COLLECTION.TRANSACTION
| typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS
>,
setupCategoryTaskReport: OnyxEntry<Report>,
setupCategoryTaskParentReport: OnyxEntry<Report>,
Expand Down Expand Up @@ -1348,7 +1353,7 @@ function deleteWorkspaceCategories(
}
: {};

const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES> = {
const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_CATEGORIES | typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS> = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down
29 changes: 25 additions & 4 deletions src/libs/actions/Policy/DistanceRate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ function deletePolicyDistanceRates(
policyID: string,
customUnit: CustomUnit,
rateIDsToDelete: string[],
transactionIDsAffected: string[],
transactionsAffected: Array<{transactionID: string; customUnitRateID: string}>,
transactionViolations: OnyxCollection<TransactionViolation[]>,
) {
const currentRates = customUnit.rates;
Expand All @@ -413,7 +413,13 @@ function deletePolicyDistanceRates(
};
}

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS>> = [
// Check if there's exactly one remaining enabled rate for auto-selection
const remainingEnabledRateIDs = Object.entries(currentRates)
.filter(([rateID, rate]) => !rateIDsToDelete.includes(rateID) && rate.enabled)
.map(([rateID]) => rateID);
const singleRemainingRateID = remainingEnabledRateIDs.length === 1 ? remainingEnabledRateIDs.at(0) : undefined;

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.TRANSACTION>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
Expand All @@ -427,7 +433,7 @@ function deletePolicyDistanceRates(
},
];

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS>> = [
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS | typeof ONYXKEYS.COLLECTION.TRANSACTION>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
Expand All @@ -444,12 +450,27 @@ function deletePolicyDistanceRates(
const optimisticTransactionsViolations: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS>> = [];
const failureTransactionsViolations: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS>> = [];

for (const transactionID of transactionIDsAffected) {
for (const {transactionID, customUnitRateID} of transactionsAffected) {
const currentTransactionViolations = transactionViolations?.[`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`] ?? [];
if (currentTransactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY)) {
return;
}

// If there's exactly one remaining enabled rate, auto-select it instead of adding a violation
if (singleRemainingRateID) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {comment: {customUnit: {customUnitRateID: singleRemainingRateID}}},
});
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
value: {comment: {customUnit: {customUnitRateID}}},
});
continue;
}

optimisticTransactionsViolations.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
Expand Down
2 changes: 1 addition & 1 deletion src/libs/actions/Policy/Tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ function deletePolicyTags(policyData: PolicyData, tagsToDelete: string[]) {
},
};

const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_TAGS> = {
const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY_TAGS | typeof ONYXKEYS.COLLECTION.TRANSACTION | typeof ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS> = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand Down
Loading
Loading