Skip to content
Merged
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
17 changes: 17 additions & 0 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,22 @@ function hasValidModifiedAmount(transaction: OnyxEntry<Transaction> | null): boo
return transaction?.modifiedAmount !== undefined && transaction?.modifiedAmount !== null && transaction?.modifiedAmount !== '';
}

/**
* Builds the optimistic transaction used when an IOU report is converted to an expense report.
*
* Expense reports store amounts with the opposite sign of IOU reports (see `getAmount`/`getConvertedAmount`),
* so `amount`, `modifiedAmount` and `convertedAmount` are negated to match the expense-report convention.
* Absent converted values are not added so they keep being derived from the amount.
*/
function getNegatedAmountTransaction(transaction: Transaction): Transaction {
return {
...transaction,
amount: -transaction.amount,
modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
...(transaction.convertedAmount != null && {convertedAmount: -transaction.convertedAmount}),
};
}

function isCreatedMissing(transaction: OnyxEntry<Transaction>) {
if (!transaction) {
return true;
Expand Down Expand Up @@ -2991,6 +3007,7 @@ export {
hasPendingRTERViolation,
hasAnyPendingRTERViolation,
hasValidModifiedAmount,
getNegatedAmountTransaction,
allHavePendingRTERViolation,
hasPendingUI,
getWaypointIndex,
Expand Down
8 changes: 2 additions & 6 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ import * as PhoneNumber from '@libs/PhoneNumber';
import * as PolicyUtils from '@libs/PolicyUtils';
import {getCustomUnitsForDuplication, getMemberAccountIDsForWorkspace, goBackWhenEnableFeature, isControlPolicy, navigateToExpensifyCardPage} from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import {hasValidModifiedAmount} from '@libs/TransactionUtils';
import {getNegatedAmountTransaction} from '@libs/TransactionUtils';
import type {AvatarSource} from '@libs/UserAvatarUtils';
import type {Feature} from '@pages/OnboardingInterestedFeatures/types';
import * as PaymentMethods from '@userActions/PaymentMethods';
Expand Down Expand Up @@ -4461,11 +4461,7 @@ function createWorkspaceFromIOUPayment(
const transactionsOptimisticData: Record<string, Transaction> = {};
const transactionFailureData: Record<string, Transaction> = {};
for (const transaction of reportTransactions) {
transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = {
...transaction,
amount: -transaction.amount,
modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
};
transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = getNegatedAmountTransaction(transaction);

transactionFailureData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction;
}
Expand Down
8 changes: 2 additions & 6 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ import {
} from '@libs/ReportUtils';
import {buildOptimisticSnapshotData, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
import playSound, {SOUNDS} from '@libs/Sound';
import {getAmount, getCurrency, hasValidModifiedAmount, isOnHold, recalculateUnreportedTransactionDetails, shouldClearConvertedAmount} from '@libs/TransactionUtils';
import {getAmount, getCurrency, getNegatedAmountTransaction, isOnHold, recalculateUnreportedTransactionDetails, shouldClearConvertedAmount} from '@libs/TransactionUtils';
import addTrailingForwardSlash from '@libs/UrlUtils';
import Visibility from '@libs/Visibility';
import {cacheAttachment, removeCachedAttachment} from '@userActions/Attachment';
Expand Down Expand Up @@ -6781,11 +6781,7 @@ function convertIOUReportToExpenseReport(iouReport: Report, policy: Policy, poli
const transactionsOptimisticData: Record<string, Transaction> = {};
const transactionFailureData: Record<string, Transaction> = {};
for (const transaction of reportTransactions) {
transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = {
...transaction,
amount: -transaction.amount,
modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
};
transactionsOptimisticData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = getNegatedAmountTransaction(transaction);

transactionFailureData[`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`] = transaction;
}
Expand Down
52 changes: 51 additions & 1 deletion tests/actions/PolicyTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import OnyxUpdateManager from '@src/libs/actions/OnyxUpdateManager';
import {askToJoinPolicy, joinAccessiblePolicy} from '@src/libs/actions/Policy/Member';
import * as Policy from '@src/libs/actions/Policy/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Onboarding, PolicyJoinMember, PolicyReportField, Policy as PolicyType, Report, ReportAction, ReportActions, TransactionViolations} from '@src/types/onyx';
import type {Onboarding, PolicyJoinMember, PolicyReportField, Policy as PolicyType, Report, ReportAction, ReportActions, Transaction, TransactionViolations} from '@src/types/onyx';
import type {Participant} from '@src/types/onyx/Report';
import createRandomPolicy from '../utils/collections/policies';
import {createRandomReport} from '../utils/collections/reports';
import createRandomTransaction from '../utils/collections/transaction';
import getOnyxValue from '../utils/getOnyxValue';
import * as TestHelper from '../utils/TestHelper';
import type {MockFetch} from '../utils/TestHelper';
Expand Down Expand Up @@ -7093,5 +7094,54 @@ describe('actions/Policy', () => {
apiWriteSpy.mockRestore();
isIOUReportUsingReportSpy.mockRestore();
});

it('should negate the converted transaction amounts on the optimistic data so the expense-report table total stays positive', async () => {
await Onyx.set(ONYXKEYS.SESSION, {email: ESH_EMAIL, accountID: ESH_ACCOUNT_ID});

const employeeAccountID = 400;
const iouReportOwnerEmail = 'employee@example.com';

const iouReport: Report = {
...createRandomReport(1, undefined),
reportID: '900',
type: CONST.REPORT.TYPE.IOU,
ownerAccountID: employeeAccountID,
chatReportID: '901',
policyID: 'oldPolicyID',
currency: CONST.CURRENCY.USD,
total: 5000,
};

const transaction: Transaction = {
...createRandomTransaction(900),
transactionID: 'transaction900',
reportID: iouReport.reportID,
amount: 5000,
modifiedAmount: '',
convertedAmount: 6000,
};

await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport);
await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction);
await waitForBatchedUpdates();

const mockTranslate = ((key: string) => key) as unknown as Parameters<typeof Policy.createWorkspaceFromIOUPayment>[8];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove this line

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed the explanatory comment above the transaction setup in 54ed55e — the test name already describes the expected behavior. I left the mockTranslate line itself in place since it's the required localeTranslate argument used the same way by the other createWorkspaceFromIOUPayment tests above; let me know if you meant a different line.

Policy.createWorkspaceFromIOUPayment(iouReport, undefined, ESH_ACCOUNT_ID, ESH_EMAIL, iouReportOwnerEmail, undefined, CONST.CURRENCY.USD, undefined, mockTranslate, {});
await waitForBatchedUpdates();

// Optimistic merge: read the stored transaction from Onyx
const optimisticTransaction: OnyxEntry<Transaction> = await new Promise((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value);
},
});
});

expect(optimisticTransaction?.amount).toBe(-5000);
expect(optimisticTransaction?.convertedAmount).toBe(-6000);
});
});
});
2 changes: 2 additions & 0 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4070,6 +4070,7 @@ describe('actions/Report', () => {
reportID: iouReport.reportID,
amount: 5000,
modifiedAmount: 6000,
convertedAmount: 6000,
};

await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
Expand Down Expand Up @@ -4097,6 +4098,7 @@ describe('actions/Report', () => {
});
expect(updatedTransaction?.amount).toBe(-5000);
expect(updatedTransaction?.modifiedAmount).toBe(-6000);
expect(updatedTransaction?.convertedAmount).toBe(-6000);
});

it('should convert IOU report to expense report with correct policyID when reportTransactions are provided', async () => {
Expand Down
Loading