Skip to content

Fix negative transaction total after moving IOU report to a workspace#91079

Open
trasnake87 wants to merge 11 commits into
Expensify:mainfrom
trasnake87:fix-iou-negative-amount-90219
Open

Fix negative transaction total after moving IOU report to a workspace#91079
trasnake87 wants to merge 11 commits into
Expensify:mainfrom
trasnake87:fix-iou-negative-amount-90219

Conversation

@trasnake87
Copy link
Copy Markdown

@trasnake87 trasnake87 commented May 19, 2026

Explanation of Change

When an IOU report is moved to a workspace, convertIOUReportToExpenseReport builds optimistic transaction data that negates amount and modifiedAmount to match the expense-report sign convention, but it never negates convertedAmount / convertedTaxAmount.

The per-row TOTAL column renders through getConvertedAmount, which returns the opposite sign of the stored convertedAmount for expense reports (expense-report transactions are stored with negative converted amounts — see the existing fixture in tests/unit/ReportUtilsTest.ts). Because the IOU convertedAmount was left positive, getConvertedAmount returns a negative value once the report becomes an expense report, so each transaction's TOTAL cell flips negative. The report-level header total stays correct because it is negated separately, which matches the reported behavior (only the table cells go wrong).

This change negates convertedAmount and convertedTaxAmount in the optimistic transaction data so the stored shape matches the backend representation of expense-report transactions. A conditional spread is used so absent values (e.g. an offline expense with no backend-computed conversion yet) are not overwritten with -undefined/-0. The same buggy loop is duplicated in createWorkspaceFromIOUPayment (paying an IOU by creating a new workspace), so the fix is applied in both places to fully close the bug class. transactionFailureData is built from the unmodified transaction, so rollback on failure already restores the original positive values.

Fixed Issues

$ #90219
PROPOSAL: #90219 (comment)

Tests

  1. Have a workspace.
  2. Open a DM with any user.
  3. Submit two expenses to that user.
  4. Open the report.
  5. Click More > Change workspace and select any workspace.
  6. Verify the per-transaction Total column keeps its original positive value (it does not flip to a negative amount), and the report header total remains correct.
  7. Repeat using the flow that pays an IOU by creating a new workspace (pay the IOU and choose to create a new workspace) and verify the transaction totals stay positive there as well.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. Perform the same More > Change workspace flow on a report with two expenses.
  3. Verify the transaction Total column stays positive while offline (the optimistic data is correct on its own).
  4. Go back online and verify the values remain correct after the server response reconciles (no flip on reconnect).

QA Steps

Same as the Tests section above.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Web

End-to-end walkthrough of the OP repro on the Web build:

  1. Open a DM with another user (Submitter Emp).
  2. Submit two expenses to that user ($50.00 then $35.00) — the chat shows the IOU card Submitter Emp owes $85.00 · 2 expenses.
  3. Click View to open the IOU report (header reads Submitter Emp owes $85.00 · Outstanding, two rows of May 26).
  4. Click More → Change workspace and pick JAG-01.
  5. Post-conversion the report becomes Expense Report 2026-05-26 (Draft) · From Thomas Rasnake's expenses in JAG-01, and the per-row Total column holds at $50.00 and $35.00 (positive) — without this PR's fix both rows would flip to a negative amount on the table.
web_91079_op_20260526_082744.mp4
Android: Native

Same OP repro on the Android native build (Pixel 7 / API 35 emulator):

  1. Open the Submitter Emp DM.
  2. Submit two expenses ($50.00 then $35.00) via + → Create expense → keypad → Next → Create $X.00 expense.
  3. Tap View on the IOU card (header reads Submitter Emp owes $85.00 · Outstanding).
  4. Tap More → Change workspace → JAG-01.
  5. The report becomes Expense Report 2026-05-26 (Draft) and the two rows hold at $50.00 / $35.00 (positive) — without this PR's fix both would flip negative.
android_native_91079_op_FINAL.mp4
Android: mWeb Chrome

Same OP repro driven through Chrome on the Android emulator (mobile web). After the conversion, the per-row Totals stay positive at $50.00 and $35.00.

android_mweb_91079_op_cropped.mp4
iOS: Native

Same OP repro on the iOS native build (iPhone 17e simulator, iOS 26.5):

  1. Open the Submitter Emp DM.
  2. Submit two expenses ($50.00 then $35.00) via + → Create expense → keypad → Next → Create $X.00 expense.
  3. Tap View on the IOU card (header reads Submitter Emp owes $85.00 · Outstanding).
  4. Tap More → Change workspace → JAG-01.
  5. The report becomes Expense Report 2026-05-26 (Draft) and the two rows hold at $50.00 / $35.00 (positive) — without this PR's fix both would flip negative.
ios_91079.mp4
iOS: mWeb Safari

Same OP repro on Mobile Safari (iPhone 17e simulator, iOS 26.5). Post-conversion the per-row Totals hold at $50.00 and $35.00 (positive).

ios_mweb_91079_op_20260526_162655_cropped.mp4

@trasnake87 trasnake87 requested review from a team as code owners May 19, 2026 13:08
@melvin-bot melvin-bot Bot requested review from FitseTLT and joekaufmanexpensify and removed request for a team and FitseTLT May 19, 2026 13:08
@melvin-bot
Copy link
Copy Markdown

melvin-bot Bot commented May 19, 2026

@FitseTLT Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot requested review from a team and removed request for a team May 19, 2026 13:08
@melvin-bot melvin-bot Bot requested review from FitseTLT and inimaga and removed request for a team May 19, 2026 13:08
@melvin-bot
Copy link
Copy Markdown

melvin-bot Bot commented May 19, 2026

@FitseTLT @inimaga One of you needs to copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

Comment thread src/libs/actions/Report/index.ts Outdated
...transaction,
amount: -transaction.amount,
modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
...(transaction.convertedAmount != null && {convertedAmount: -transaction.convertedAmount}),
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.

❌ CONSISTENCY-3 (docs)

The transaction sign-conversion logic (negating amount, modifiedAmount, convertedAmount, convertedTaxAmount) is now identically duplicated between convertIOUReportToExpenseReport in src/libs/actions/Report/index.ts and createWorkspaceFromIOUPayment in src/libs/actions/Policy/Policy.ts. Adding new fields to negate (like this PR does) requires updating both locations in lockstep, which is error-prone.

Extract the negation logic into a shared helper function, for example:

function negateTransactionAmounts(transaction: Transaction): Transaction {
    return {
        ...transaction,
        amount: -transaction.amount,
        modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
        ...(transaction.convertedAmount != null && {convertedAmount: -transaction.convertedAmount}),
        ...(transaction.convertedTaxAmount != null && {convertedTaxAmount: -transaction.convertedTaxAmount}),
    };
}

Then use it in both call sites:

transactionsOptimisticData[...] = negateTransactionAmounts(transaction);

Reviewed at: 36d06bc | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Comment thread src/libs/actions/Policy/Policy.ts Outdated
...transaction,
amount: -transaction.amount,
modifiedAmount: hasValidModifiedAmount(transaction) ? -Number(transaction.modifiedAmount) : '',
...(transaction.convertedAmount != null && {convertedAmount: -transaction.convertedAmount}),
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.

❌ CONSISTENCY-3 (docs)

This is the same duplicated transaction sign-conversion block that also exists in src/libs/actions/Report/index.ts (convertIOUReportToExpenseReport). See the comment on that file for the suggested shared helper extraction.


Reviewed at: 36d06bc | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36d06bcf4b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/actions/Report/index.ts Outdated
Comment on lines +6904 to +6905
...(transaction.convertedAmount != null && {convertedAmount: -transaction.convertedAmount}),
...(transaction.convertedTaxAmount != null && {convertedTaxAmount: -transaction.convertedTaxAmount}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize converted amounts before flipping sign

When an IOU transaction already has a negative convertedAmount (the transaction helpers explicitly handle IOU amounts that are stored negative), this unconditional negation stores a positive converted amount on the new expense report. Expense report rendering flips converted amounts for display, so that input will make the converted total/tax show negative again; use the expense-report sign convention directly, e.g. negate the absolute value, and apply the same fix to the duplicate conversion in createWorkspaceFromIOUPayment.

Useful? React with 👍 / 👎.

@trasnake87
Copy link
Copy Markdown
Author

Addressed the review feedback in 7876b36:

  • Extracted the IOU→expense-report sign conversion into a single getExpenseReportSignedTransaction helper in TransactionUtils, and used it in both convertIOUReportToExpenseReport and createWorkspaceFromIOUPayment so the negation logic is no longer duplicated across the two call sites.
  • Normalized the converted amounts: since IOU transactions can be stored with either sign, the helper now uses -Math.abs(convertedAmount) / -Math.abs(convertedTaxAmount). The expense-report convention flips the stored sign for display, so storing a negative magnitude keeps the per-row TOTAL positive even when the source IOU stored the converted amounts negative. A plain negation would have flipped a negative-stored value back to positive and reproduced the bug for that case.
  • Added a regression test in ReportTest.ts covering an IOU transaction with negative-stored converted amounts (it fails with a plain negation, passes with the magnitude normalization).

amount/modifiedAmount keep their existing negation behavior unchanged. jest and eslint are green.

Copy link
Copy Markdown
Contributor

@joekaufmanexpensify joekaufmanexpensify left a comment

Choose a reason for hiding this comment

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

Makes sense 👍

@codecov
Copy link
Copy Markdown

codecov Bot commented May 20, 2026

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/libs/actions/Policy/Policy.ts 69.98% <ø> (+0.55%) ⬆️
src/libs/actions/Report/index.ts 69.41% <100.00%> (+0.04%) ⬆️
src/libs/TransactionUtils/index.ts 87.54% <0.00%> (-1.15%) ⬇️
... and 771 files with indirect coverage changes

@FitseTLT
Copy link
Copy Markdown
Contributor

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT good catch — the Policy.ts call site wasn't exercised by my existing tests (the regression coverage all lived in tests/actions/ReportTest.ts against convertIOUReportToExpenseReport).

I added a matching test in tests/actions/PolicyTest.ts under the createWorkspaceFromIOUPayment describe block. It feeds the function an IOU transaction whose convertedAmount/convertedTaxAmount are stored negative (the case the Codex review flagged earlier), spies on API.write, and asserts that the transaction collection in optimisticData ends up with a negative magnitude (so the expense-report sign convention renders a positive total) and that the failureData restores the original values.

Verified the test catches the pre-fix behavior: temporarily replacing -Math.abs(...) with a plain -transaction.convertedAmount in the helper makes the new test fail with Expected: -6000 / Received: 6000. With the helper restored, the test is green alongside the three existing createWorkspaceFromIOUPayment tests. npx eslint and npx prettier --check are clean on the change.

When an IOU report is converted to an expense report, the optimistic
transaction data negated amount and modifiedAmount to match the
expense-report sign convention but left convertedAmount and
convertedTaxAmount untouched. Because getConvertedAmount flips the sign
for expense reports, the per-row TOTAL column rendered the still-positive
convertedAmount as a negative value.

Negate convertedAmount and convertedTaxAmount in the optimistic data in
both convertIOUReportToExpenseReport and createWorkspaceFromIOUPayment,
using a conditional spread so absent values are not overwritten.
Rollback data is built from the unmodified transaction, so failure
restores the original values.
…unts

Move the IOU-to-expense-report transaction sign conversion into a single
getExpenseReportSignedTransaction helper in TransactionUtils so the logic
is no longer duplicated between convertIOUReportToExpenseReport and
createWorkspaceFromIOUPayment.

IOU transactions can be stored with either sign, so negate the absolute
value of convertedAmount/convertedTaxAmount. The expense-report convention
flips the stored sign for display, so a negative magnitude keeps the table
total positive even when the source IOU stored the converted amounts
negative. Added a regression test covering that case.
…conversion

Cover the Policy.ts call site of getExpenseReportSignedTransaction with a
test that feeds an IOU transaction whose converted amounts are stored
negative and asserts the optimistic data stores a negative magnitude (so
the expense-report sign convention renders a positive table total) and
that the failure data restores the original values.
@trasnake87 trasnake87 force-pushed the fix-iou-negative-amount-90219 branch from 4bd5eca to c2e1d23 Compare May 21, 2026 23:05
@FitseTLT
Copy link
Copy Markdown
Contributor

Will review on Monday

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT Added the Android Native recording — same IOU → More → Change workspace → JAG-01 flow on the Android native build, the per-row Amount stays at $75.00 (positive) and Total renders at ₹7,177.54 (positive). iOS Native follows next.

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT iOS Native is now attached as well. All three platform recordings show the same flow — IOU created in a DM, then More → Change workspace → JAG-01, and the per-row Amount + Total stay positive after the conversion. Web, Android Native, iOS Native rows are ticked.

Comment thread tests/actions/PolicyTest.ts Outdated
Comment on lines +7131 to +7143
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());
const isIOUReportUsingReportSpy = jest.spyOn(ReportUtils, 'isIOUReportUsingReport').mockReturnValue(true);
const getReportTransactionsSpy = jest.spyOn(ReportUtils, 'getReportTransactions').mockReturnValue([transaction]);

const mockTranslate = ((key: string) => key) as unknown as Parameters<typeof Policy.createWorkspaceFromIOUPayment>[8];
Policy.createWorkspaceFromIOUPayment(iouReport, undefined, ESH_ACCOUNT_ID, ESH_EMAIL, iouReportOwnerEmail, undefined, CONST.CURRENCY.USD, undefined, mockTranslate, {});
await waitForBatchedUpdates();

const writeOptions = apiWriteSpy.mock.calls.at(0)?.at(2) as {
optimisticData?: Array<{key?: string; value?: Record<string, Transaction> | null}>;
failureData?: Array<{key?: string; value?: Record<string, Transaction> | null}>;
};

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.

The correct way to test is save the positive amount transaction in onyx then after the action createWorkspaceFromIOUPayment read the transaction from onyx and assert the correct value. Read other tests for example.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in c20917c — the test now seeds the IOU report and transaction (with positive convertedAmount) in Onyx, calls createWorkspaceFromIOUPayment with fetch paused, and reads the transaction back via Onyx.connect to assert the optimistic merge. The failure-rollback branch then fails+resumes fetch and re-reads from Onyx.

Comment thread tests/actions/ReportTest.ts Outdated
Comment on lines +3948 to +3955
const iouReport: OnyxTypes.Report = {
...createRandomReport(3, undefined),
reportID: 'iouReport302',
type: CONST.REPORT.TYPE.IOU,
ownerAccountID: 3,
reportName: 'Original IOU Report Name',
total: 5000,
};
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.

Same here

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Same — in c20917c, the test now seeds the transaction in Onyx, calls convertIOUReportToExpenseReport, applies result.optimisticData via Onyx.update, and asserts the stored transaction via Onyx.connect. The follow-up rollback case applies result.failureData and re-reads. The obsolete "negative IOU input" sibling test was removed alongside the Math.abs handling.

Comment thread src/libs/TransactionUtils/index.ts Outdated
Comment on lines +654 to +655
...(transaction.convertedAmount != null && {convertedAmount: -Math.abs(transaction.convertedAmount)}),
...(transaction.convertedTaxAmount != null && {convertedTaxAmount: -Math.abs(transaction.convertedTaxAmount)}),
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.

IOU dooesn't have tax amount remove the tax amount part and remove abs too like the amount case.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in c20917c — dropped the convertedTaxAmount branch and switched to a plain -transaction.convertedAmount so this mirrors the amount case. Doc-comment trimmed to match.

- getExpenseReportSignedTransaction: drop the convertedTaxAmount branch
  (IOUs don't carry tax) and use a plain negation on convertedAmount
  (mirrors the amount line; IOU convertedAmount is positive).
- PolicyTest createWorkspaceFromIOUPayment: rewrite to seed the IOU
  report and transaction in Onyx with a positive convertedAmount, run
  the action, and assert the stored transaction via Onyx (pause fetch
  for the optimistic check, then fail+resume for the rollback).
- ReportTest convertIOUReportToExpenseReport: same Onyx-read pattern
  via Onyx.update on the returned optimistic and failure data; drop
  the obsolete "negative IOU input" test alongside the abs handling.
@FitseTLT
Copy link
Copy Markdown
Contributor

@trasnake87 Why don't u complete your checklist and include a proper video that shows the steps in the issues OP which will show this pr fixed it? This is the last time I am asking you. We will have to reassign another contributor.

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT Updated Screenshots/Videos → Web with an end-to-end walkthrough of the OP repro: open a DM with another user, submit two expenses ($50.00 and $35.00), open the IOU report (header reads Submitter Emp owes $85.00 · Outstanding with two May 26 rows), click More → Change workspace → JAG-01, and the post-conversion Expense Report 2026-05-26 (Draft) · From Thomas Rasnake's expenses in JAG-01 keeps both per-row Total cells at $50.00 and $35.00 (positive) instead of flipping to a negative amount.

Checklist:

  • Ticked the JS-console verification boxes — no errors related to this PR observed while running the flow.
  • Added an explicit failure-scenario step in the Tests section (the rollback branch where the backend rejects MoveIOUReportToPolicy and failureData restores the original positive convertedAmount) and ticked the corresponding Author-Checklist item.

The Android Native and iOS Native videos already in the same section cover the same fix on those builds.

@FitseTLT
Copy link
Copy Markdown
Contributor

All 5 platform videos are required with the same steps as the current web video. If u can't provide tell me we will reassign another contributor.

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT All five platforms below, each running through the OP repro (open the Submitter Emp DM → submit two expenses $50.00 + $35.00 → open the IOU report → More → Change workspace → JAG-01 → confirm both per-row Totals stay positive post-conversion). PR body updated with five <details> blocks pointing at the assets.

  • Web / MacOS Chromed1c1077a-… (the one you already saw).
  • Android: Native9d60794c-… (Pixel 7 / API 35 emulator, full OP repro).
  • Android: mWeb Chromece07e285-… (Chrome on the same emulator).
  • iOS: mWeb Safari6fb8be7b-… (Mobile Safari on the iPhone 17e simulator).
  • iOS: Native5dfccbaf-… (iPhone 17e simulator, iOS 26.5). One caveat noted in the body: this video uses a single $75.00 IOU rather than the $50 + $35 pair from the OP — same code path (getExpenseReportSignedTransaction), same UI flow, same per-row positivity check, just one transaction. Happy to re-shoot with the $50/$35 pair if you'd like the amounts to match exactly across all five videos.

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT Re-shot the iOS Native recording with the $50.00 + $35.00 pair so all five platform videos run through the same OP repro. The new asset (d43edffe-…) is now in the iOS Native block in the PR body above, replacing 5dfccbaf-….

Comment thread tests/actions/ReportTest.ts Outdated
Comment on lines +3989 to +4002
await Onyx.update(result.failureData);
await waitForBatchedUpdates();

const rolledBackTransaction: OnyxEntry<OnyxTypes.Transaction> = await new Promise((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value);
},
});
});
expect(rolledBackTransaction?.amount).toBe(5000);
expect(rolledBackTransaction?.convertedAmount).toBe(6000);
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.

This is not needed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 1d1ee26.

Comment thread tests/actions/ReportTest.ts Outdated
it('should negate convertedAmount on the optimistic transactions so the table total stays positive', async () => {
// Given a policy and an IOU report with a transaction that has positive converted amounts (IOU sign convention)
const policyID = '302';
const policyWithEmptyFieldList: OnyxTypes.Policy = {
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.

Suggested change
const policyWithEmptyFieldList: OnyxTypes.Policy = {
const policy: OnyxTypes.Policy = {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 1d1ee26.

Comment thread tests/actions/ReportTest.ts Outdated

// When converting the IOU report to an expense report, apply the resulting optimistic data to Onyx
const result = Report.convertIOUReportToExpenseReport(iouReport, policyWithEmptyFieldList, policyID, 'expenseChat302', [transaction]);
await Onyx.update(result.optimisticData);
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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 1d1ee26.

Comment thread tests/actions/PolicyTest.ts Outdated
Comment on lines +7150 to +7167
// Failure rollback: fail the network and verify the original positive values are restored in Onyx
mockFetch?.fail?.();
await mockFetch?.resume?.();
await waitForBatchedUpdates();

const rolledBackTransaction: OnyxEntry<Transaction> = await new Promise((resolve) => {
const connection = Onyx.connect({
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`,
callback: (value) => {
Onyx.disconnect(connection);
resolve(value);
},
});
});

expect(rolledBackTransaction?.amount).toBe(5000);
expect(rolledBackTransaction?.convertedAmount).toBe(6000);
});
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.

Unnecessary

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 1d1ee26.

Comment thread src/libs/TransactionUtils/index.ts Outdated
* 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 getExpenseReportSignedTransaction(transaction: Transaction): Transaction {
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.

Suggested change
function getExpenseReportSignedTransaction(transaction: Transaction): Transaction {
function getNegatedAmountTransaction(transaction: Transaction): Transaction {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 1d1ee26.

- Rename getExpenseReportSignedTransaction to getNegatedAmountTransaction
- ReportTest: inspect optimisticData directly, drop Onyx round-trip and failure-rollback block
- PolicyTest: drop failure-rollback block on the negate-amount test
Comment thread tests/actions/ReportTest.ts Outdated
Comment on lines +3966 to +3975
// When converting the IOU report to an expense report
const result = Report.convertIOUReportToExpenseReport(iouReport, policy, policyID, 'expenseChat302', [transaction]);

// Then the optimistic transactions update negates amount and convertedAmount to the expense-report sign convention
const transactionsUpdate = result.optimisticData.find((update) => update.onyxMethod === Onyx.METHOD.MERGE_COLLECTION && update.key === ONYXKEYS.COLLECTION.TRANSACTION);
const negatedTransaction = (transactionsUpdate?.value as Record<string, OnyxTypes.Transaction> | undefined)?.[
`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`
];
expect(negatedTransaction?.amount).toBe(-5000);
expect(negatedTransaction?.convertedAmount).toBe(-6000);
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.

U shouldn't check on optimisiticData, you need to read from onyx and assert.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 3a0aca7 — the test now seeds the policy + transaction in Onyx, calls Report.convertIOUReportToExpenseReport, applies result.optimisticData via Onyx.update, and asserts the stored transaction's amount / convertedAmount via an Onyx.connect read. This matches the read-from-Onyx pattern used by the createWorkspaceFromIOUPayment test.

@FitseTLT
Copy link
Copy Markdown
Contributor

FitseTLT commented May 28, 2026

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • [ x If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
2026-05-28.19-03-56.mp4
iOS: HybridApp
2026-05-28.22-41-31.mp4
iOS: mWeb Safari
2026-05-28.19-06-48.mp4
MacOS: Chrome / Safari
2026-05-28.19-00-58.mp4

Comment thread tests/actions/ReportTest.ts Outdated

// When converting the IOU report to an expense report and applying the optimistic data to Onyx
const result = Report.convertIOUReportToExpenseReport(iouReport, policy, policyID, 'expenseChat302', [transaction]);
await Onyx.update(result.optimisticData);
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.

U don't have to explicitly apply the optimistic data running the action convertIOUReportToExpenseReport is enough

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Moved that onto the action path. convertIOUReportToExpenseReport only builds and returns the optimistic data, so to read the negated value back from Onyx (rather than asserting on the returned optimisticData) the test has to go through the action that actually dispatches the write. The existing moveIOUReportToPolicyAndInviteSubmitter test already does that, so I folded the convertedAmount check in there — it now asserts amount, modifiedAmount, and convertedAmount are all negated straight from Onyx after the action runs — and dropped the standalone builder-level test. a2f977b

Comment thread tests/actions/PolicyTest.ts Outdated
Comment on lines +7130 to +7132
mockFetch?.pause?.();

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Removed it. The convertedAmount negation is already asserted through the action path in the moveIOUReportToPolicyAndInviteSubmitter test in ReportTest — it runs the action and reads the transaction back from Onyx, checking that amount, modifiedAmount, and convertedAmount are all negated — so this createWorkspaceFromIOUPayment case was duplicating that coverage. Dropped it along with the now-unused imports. 544b927

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.

I only told you to remove the three lines not the whole test.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry, I misread that as the whole block. I've restored the test and dropped just the mockFetch?.pause?.() line — the negated optimistic amounts persist in Onyx after the action resolves, so the assertions still read back -5000/-6000 without pausing the fetch mock. c072bca

I'll get the last two checklist items (offline + high-traffic) wrapped up so the checklist gate goes green.

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.

I'll get the last two checklist items (offline + high-traffic) wrapped up so the checklist gate goes green.

BTW for this pr you don't have to apply those tests but only include the checklist checked in the OP so that the author checklist passes

@FitseTLT
Copy link
Copy Markdown
Contributor

complete the author checklist

The convertedAmount negation is already asserted through the action path
in the ReportTest moveIOUReportToPolicyAndInviteSubmitter test, so the
createWorkspaceFromIOUPayment-based test here duplicated that coverage.
Drop it along with the now-unused imports.
@FitseTLT
Copy link
Copy Markdown
Contributor

And why don't you complete the author checklist it is failing. U are wasting my time @trasnake87

The negated optimistic amounts persist in Onyx after the action resolves,
so the test reads them back without pausing the fetch mock.
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
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.

Comment thread tests/actions/PolicyTest.ts Outdated
Comment on lines +7115 to +7116
// IOU transactions are stored with a positive `convertedAmount`; the expense-report
// sign convention flips it on storage so the table total renders positive.
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.

// IOU transactions are stored with a positive convertedAmount but the expense-report
// sign convention flips it on storage so the table total renders positive.

@trasnake87
Copy link
Copy Markdown
Author

@FitseTLT the author checklist is complete and green now, and I've addressed your comments (removed the explanatory comment block in 54ed55e). Ready for another look whenever you have a moment — appreciate your patience.

@FitseTLT
Copy link
Copy Markdown
Contributor

FitseTLT commented Jun 1, 2026

@trasnake87 remove the other parts except the test steps in Tests section.

@trasnake87
Copy link
Copy Markdown
Author

Done — trimmed the Tests section down to just the test steps.

Copy link
Copy Markdown
Contributor

@FitseTLT FitseTLT left a comment

Choose a reason for hiding this comment

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

LGTM

@FitseTLT
Copy link
Copy Markdown
Contributor

FitseTLT commented Jun 2, 2026

@inimaga to you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants