Skip to content

Commit ab2ca38

Browse files
Merge branch 'main' into claude-fixUSPhoneValidationForTerritories
Co-authored-by: Lydia Barclay <lydiabarclay@users.noreply.github.com>
2 parents 35415b1 + e01e89a commit ab2ca38

3 files changed

Lines changed: 267 additions & 15 deletions

File tree

src/libs/actions/IOU/Split.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,61 @@ function updateSplitTransactions({
14471447
errors: null,
14481448
},
14491449
});
1450+
const isLastTransactionInReport = Object.values(allTransactionsList ?? {}).filter((itemTransaction) => itemTransaction?.reportID === expenseReportID).length === 1;
1451+
if (isLastTransactionInReport) {
1452+
optimisticData.push({
1453+
onyxMethod: Onyx.METHOD.MERGE,
1454+
key: `${ONYXKEYS.COLLECTION.REPORT}${transactionData.reportID}`,
1455+
value: {
1456+
reportID: null,
1457+
pendingFields: {
1458+
preview: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
1459+
},
1460+
},
1461+
});
1462+
successData.push({
1463+
onyxMethod: Onyx.METHOD.SET,
1464+
key: `${ONYXKEYS.COLLECTION.REPORT}${transactionData.reportID}`,
1465+
value: null,
1466+
});
1467+
failureData.push({
1468+
onyxMethod: Onyx.METHOD.MERGE,
1469+
key: `${ONYXKEYS.COLLECTION.REPORT}${transactionData.reportID}`,
1470+
value: {
1471+
reportID: transactionData.reportID,
1472+
pendingFields: null,
1473+
},
1474+
});
1475+
if (expenseReport?.parentReportID && expenseReport?.parentReportActionID) {
1476+
optimisticData.push({
1477+
onyxMethod: Onyx.METHOD.MERGE,
1478+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.parentReportID}`,
1479+
value: {
1480+
[expenseReport?.parentReportActionID]: {
1481+
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
1482+
},
1483+
},
1484+
});
1485+
successData.push({
1486+
onyxMethod: Onyx.METHOD.MERGE,
1487+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.parentReportID}`,
1488+
value: {
1489+
[expenseReport?.parentReportActionID]: {
1490+
pendingAction: null,
1491+
},
1492+
},
1493+
});
1494+
failureData.push({
1495+
onyxMethod: Onyx.METHOD.MERGE,
1496+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.parentReportID}`,
1497+
value: {
1498+
[expenseReport?.parentReportActionID]: {
1499+
pendingAction: null,
1500+
},
1501+
},
1502+
});
1503+
}
1504+
}
14501505
}
14511506

14521507
if (isReverseSplitOperation) {

src/pages/workspace/categories/CategorySettingsPage.tsx

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ function CategorySettingsPage({
7070

7171
const [isCannotDeleteOrDisableLastCategoryModalVisible, setIsCannotDeleteOrDisableLastCategoryModalVisible] = useState(false);
7272
const shouldPreventDisableOrDelete = isDisablingOrDeletingLastEnabledCategory(policy, policyData.categories, [policyCategory]);
73-
const areCommentsRequired = policyCategory?.areCommentsRequired ?? false;
7473
const isQuickSettingsFlow = name === SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORY_SETTINGS;
7574
const {
7675
taskReport: setupCategoryTaskReport,
@@ -354,20 +353,6 @@ function CategorySettingsPage({
354353
shouldShowRightIcon
355354
/>
356355
</OfflineWithFeedback>
357-
358-
{areCommentsRequired && (
359-
<OfflineWithFeedback pendingAction={policyCategory.pendingFields?.commentHint}>
360-
<MenuItemWithTopDescription
361-
title={policyCategory?.commentHint}
362-
description={translate('workspace.rules.categoryRules.descriptionHint')}
363-
onPress={() => {
364-
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name));
365-
}}
366-
shouldShowRightIcon
367-
/>
368-
</OfflineWithFeedback>
369-
)}
370-
371356
{!isThereAnyAccountingConnection && (
372357
<MenuItem
373358
icon={expensifyIcons.Trashcan}
@@ -453,6 +438,16 @@ function CategorySettingsPage({
453438
shouldShowRightIcon
454439
/>
455440
</OfflineWithFeedback>
441+
<OfflineWithFeedback pendingAction={policyCategory.pendingFields?.commentHint}>
442+
<MenuItemWithTopDescription
443+
title={policyCategory?.commentHint}
444+
description={translate('workspace.rules.categoryRules.descriptionHint')}
445+
onPress={() => {
446+
Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name));
447+
}}
448+
shouldShowRightIcon
449+
/>
450+
</OfflineWithFeedback>
456451
</>
457452
)}
458453
</ScrollView>

tests/actions/IOUTest/SplitTest.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,6 +1604,208 @@ describe('updateSplitTransactionsFromSplitExpensesFlow', () => {
16041604
expect(searchSnapshot?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransactionID1}`]).toBeDefined();
16051605
expect(searchSnapshot?.data?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransactionID2}`]).toBeDefined();
16061606
});
1607+
1608+
it('should mark the report for deletion when reverting a split and the single expense moves to a different report', async () => {
1609+
const amount = 10000;
1610+
let expenseReport: OnyxEntry<Report>;
1611+
let chatReport: OnyxEntry<Report>;
1612+
let originalTransactionID: string | undefined;
1613+
1614+
// Create workspace and expense
1615+
const policyID = generatePolicyID();
1616+
createWorkspace({
1617+
policyOwnerEmail: CARLOS_EMAIL,
1618+
makeMeAdmin: true,
1619+
policyName: "Carlos's Workspace",
1620+
policyID,
1621+
introSelected: {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM},
1622+
currentUserAccountIDParam: CARLOS_ACCOUNT_ID,
1623+
currentUserEmailParam: CARLOS_EMAIL,
1624+
});
1625+
setWorkspaceApprovalMode(policyID, CARLOS_EMAIL, CONST.POLICY.APPROVAL_MODE.BASIC);
1626+
await waitForBatchedUpdates();
1627+
1628+
await getOnyxData({
1629+
key: ONYXKEYS.COLLECTION.REPORT,
1630+
waitForCollectionCallback: true,
1631+
callback: (allReports) => {
1632+
chatReport = Object.values(allReports ?? {}).find((report) => report?.chatType === CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT);
1633+
},
1634+
});
1635+
1636+
requestMoney({
1637+
report: chatReport,
1638+
betas: [CONST.BETAS.ALL],
1639+
participantParams: {
1640+
payeeEmail: RORY_EMAIL,
1641+
payeeAccountID: RORY_ACCOUNT_ID,
1642+
participant: {login: CARLOS_EMAIL, accountID: CARLOS_ACCOUNT_ID, isPolicyExpenseChat: true, reportID: chatReport?.reportID},
1643+
},
1644+
transactionParams: {
1645+
amount,
1646+
attendees: [],
1647+
currency: CONST.CURRENCY.USD,
1648+
created: '',
1649+
merchant: 'TestMerchant',
1650+
comment: 'test comment',
1651+
},
1652+
shouldGenerateTransactionThreadReport: true,
1653+
isASAPSubmitBetaEnabled: false,
1654+
currentUserAccountIDParam: RORY_ACCOUNT_ID,
1655+
currentUserEmailParam: RORY_EMAIL,
1656+
transactionViolations: {},
1657+
policyRecentlyUsedCurrencies: [],
1658+
quickAction: undefined,
1659+
isSelfTourViewed: false,
1660+
personalDetails: {},
1661+
});
1662+
await waitForBatchedUpdates();
1663+
1664+
await getOnyxData({
1665+
key: ONYXKEYS.COLLECTION.REPORT,
1666+
waitForCollectionCallback: true,
1667+
callback: (allReports) => {
1668+
expenseReport = Object.values(allReports ?? {}).find((report) => report?.type === CONST.REPORT.TYPE.EXPENSE);
1669+
},
1670+
});
1671+
await getOnyxData({
1672+
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport?.reportID}`,
1673+
waitForCollectionCallback: false,
1674+
callback: (allReportActions) => {
1675+
const iouActions = Object.values(allReportActions ?? {}).filter((reportAction): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> =>
1676+
isMoneyRequestAction(reportAction),
1677+
);
1678+
const originalMessage = isMoneyRequestAction(iouActions?.at(0)) ? getOriginalMessage(iouActions?.at(0)) : undefined;
1679+
originalTransactionID = originalMessage?.IOUTransactionID;
1680+
},
1681+
});
1682+
1683+
const originalTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`);
1684+
const originalReportID = originalTransaction?.reportID;
1685+
1686+
// Step 1: Split into 2 (creates child transactions via creation path)
1687+
const splitTransactionID1 = rand64();
1688+
const splitTransactionID2 = rand64();
1689+
1690+
let allTransactions: OnyxCollection<Transaction>;
1691+
let allReports: OnyxCollection<Report>;
1692+
let allReportNameValuePairs: OnyxCollection<ReportNameValuePairs>;
1693+
await getOnyxData({
1694+
key: ONYXKEYS.COLLECTION.TRANSACTION,
1695+
waitForCollectionCallback: true,
1696+
callback: (value) => {
1697+
allTransactions = value;
1698+
},
1699+
});
1700+
await getOnyxData({
1701+
key: ONYXKEYS.COLLECTION.REPORT,
1702+
waitForCollectionCallback: true,
1703+
callback: (value) => {
1704+
allReports = value;
1705+
},
1706+
});
1707+
await getOnyxData({
1708+
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
1709+
waitForCollectionCallback: true,
1710+
callback: (value) => {
1711+
allReportNameValuePairs = value;
1712+
},
1713+
});
1714+
1715+
updateSplitTransactionsFromSplitExpensesFlow({
1716+
allTransactionsList: allTransactions,
1717+
betas: [CONST.BETAS.ALL],
1718+
allReportsList: allReports,
1719+
allReportNameValuePairsList: allReportNameValuePairs,
1720+
transactionData: {
1721+
reportID: originalReportID ?? String(CONST.DEFAULT_NUMBER_ID),
1722+
originalTransactionID: originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID),
1723+
splitExpenses: [
1724+
{transactionID: splitTransactionID1, amount: amount / 2, created: DateUtils.getDBTime()},
1725+
{transactionID: splitTransactionID2, amount: amount / 2, created: DateUtils.getDBTime()},
1726+
],
1727+
},
1728+
policyCategories: undefined,
1729+
policy: undefined,
1730+
policyRecentlyUsedCategories: [],
1731+
iouReport: expenseReport,
1732+
firstIOU: undefined,
1733+
isASAPSubmitBetaEnabled: false,
1734+
currentUserPersonalDetails,
1735+
transactionViolations: {},
1736+
policyRecentlyUsedCurrencies: [],
1737+
quickAction: undefined,
1738+
iouReportNextStep: undefined,
1739+
});
1740+
await waitForBatchedUpdates();
1741+
1742+
// Verify child transactions were created (prerequisite for isReverseSplitOperation in step 2)
1743+
await getOnyxData({
1744+
key: ONYXKEYS.COLLECTION.TRANSACTION,
1745+
waitForCollectionCallback: true,
1746+
callback: (value) => {
1747+
allTransactions = value;
1748+
},
1749+
});
1750+
const childTxs = Object.values(allTransactions ?? {}).filter((tx) => tx?.comment?.originalTransactionID === originalTransactionID);
1751+
expect(childTxs.length).toBeGreaterThan(0);
1752+
1753+
// Step 2: Revert to 1 split expense, moving it to a different report
1754+
// This should trigger isReverseSplitOperation (1 split + existing children)
1755+
const differentReportID = rand64();
1756+
await getOnyxData({
1757+
key: ONYXKEYS.COLLECTION.REPORT,
1758+
waitForCollectionCallback: true,
1759+
callback: (value) => {
1760+
allReports = value;
1761+
},
1762+
});
1763+
await getOnyxData({
1764+
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
1765+
waitForCollectionCallback: true,
1766+
callback: (value) => {
1767+
allReportNameValuePairs = value;
1768+
},
1769+
});
1770+
1771+
updateSplitTransactionsFromSplitExpensesFlow({
1772+
allTransactionsList: allTransactions,
1773+
betas: [CONST.BETAS.ALL],
1774+
allReportsList: allReports,
1775+
allReportNameValuePairsList: allReportNameValuePairs,
1776+
transactionData: {
1777+
reportID: originalReportID ?? String(CONST.DEFAULT_NUMBER_ID),
1778+
originalTransactionID: originalTransactionID ?? String(CONST.DEFAULT_NUMBER_ID),
1779+
splitExpenses: [{transactionID: splitTransactionID1, amount, created: DateUtils.getDBTime(), reportID: differentReportID}],
1780+
},
1781+
policyCategories: undefined,
1782+
policy: undefined,
1783+
policyRecentlyUsedCategories: [],
1784+
iouReport: expenseReport,
1785+
firstIOU: undefined,
1786+
isASAPSubmitBetaEnabled: false,
1787+
currentUserPersonalDetails,
1788+
transactionViolations: {},
1789+
policyRecentlyUsedCurrencies: [],
1790+
quickAction: undefined,
1791+
iouReportNextStep: undefined,
1792+
});
1793+
await waitForBatchedUpdates();
1794+
1795+
// After success, the report should be removed (set to null) since no split expenses remain in the same report
1796+
const report = await new Promise<OnyxEntry<Report>>((resolve) => {
1797+
const connection = Onyx.connect({
1798+
key: `${ONYXKEYS.COLLECTION.REPORT}${differentReportID}`,
1799+
callback: (val) => {
1800+
Onyx.disconnect(connection);
1801+
resolve(val);
1802+
},
1803+
});
1804+
});
1805+
// The report should be null/undefined (removed by successData) or marked for deletion (optimistic)
1806+
const isDeleted = report === null || report === undefined || report?.pendingFields?.preview === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
1807+
expect(isDeleted).toBe(true);
1808+
});
16071809
});
16081810

16091811
describe('updateSplitTransactionsFromSplitExpensesFlow', () => {

0 commit comments

Comments
 (0)