Skip to content

Allow $0 distance rates and $0 distance expenses, including splits - #102914

Closed
NicolasBonet wants to merge 26 commits into
mainfrom
nicolas/allow-zero-distance-rate
Closed

NicolasBonet wants to merge 26 commits into
mainfrom
nicolas/allow-zero-distance-rate

Conversation

@NicolasBonet

@NicolasBonet NicolasBonet commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Lets workspaces use a $0 distance rate, for mileage that's tracked but not reimbursed.

  • Rates: validateRateValue accepts 0 and only rejects negative rates. lowRateError now reads "Rate can't be negative".
  • Expenses: a $0 rate is no longer treated as a missing rate (!rate becomes rate === undefined). This removes the "Pending..." merchant, the false "Rate out of policy" error and the disabled rate field. A distance of 0 stays blocked.
  • Splits: a $0 rate split is always $0, so these splits go by distance. The initial split, "Make splits even" and split by date divide the distance evenly. The amount and percent fields are read-only, with the info message "Amount can't be edited when the original total is $0.00." "Split" shows for $0 distance expenses, and saving accepts $0 parts that have a distance.

Saving a $0 split also needs the Auth change in https://github.com/Expensify/Auth/pull/25159. Without it, SplitTransaction returns 402 Distance rate missing from policy.

Fixed Issues

$ https://github.com/Expensify/Expensify/issues/685239
PROPOSAL: N/A

Tests

  1. As a workspace admin, open Workspace settings > Distance rates.
  2. Add a rate with a value of 0.
  3. Verify the rate saves with no error.
  4. Edit an existing rate and type -1.
  5. Verify the minus sign isn't accepted, so a negative rate can't be entered.
  6. Clear the rate and type ..
  7. Verify the input shows 0. and saves as $0.00.
  8. Create a distance expense on the workspace with any distance type (Map, Manual, GPS or Odometer), and select the $0 rate.
  9. Verify the confirmation page shows an amount of $0.00 and the rate's name, not "Pending...".
  10. Verify the rate field is pressable and shows no "Rate out of policy" error.
  11. Submit the expense.
  12. Open the expense and verify it shows the $0 rate with no "Rate out of policy" error.
  13. Create a manual distance expense with a distance of 0.
  14. Verify the zero distance is rejected.
  15. Open the $0 distance expense from step 11 and open the More menu.
  16. Verify "Split" is available.
  17. Tap "Split".
  18. Verify there are two splits, each $0 with half of the distance.
  19. Verify the split amounts are read-only and the info message "Amount can't be edited when the original total is $0.00." shows above Save.
  20. Switch to the Percent tab and verify the percentages are read-only and the same message shows. Switch back to the Amount tab.
  21. Tap "Add split", then tap "Make splits even".
  22. Verify the distance is spread evenly across the three splits and every amount stays $0.
  23. Save the split.
  24. Verify the splits are created, each with its distance and a $0 amount (requires https://github.com/Expensify/Auth/pull/25159).
  25. Create a distance expense with a rate above $0 and split it.
  26. Verify the amounts are editable, no info message shows, and amounts and distances are calculated as before.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. Create a distance expense on the $0 rate.
  3. Verify the expense shows as pending with its distance, the $0.00 rate in the merchant, and an amount of $0.00.
  4. Go online.
  5. Verify the expense is saved.

QA Steps

Same as tests. Step 24 requires https://github.com/Expensify/Auth/pull/25159 to be deployed.

  • 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 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 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)
  • 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)
  • 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 the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • 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

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Brave (Chromium), local dev environment.

Steps 1-3: the $0 rate saves

Steps 1-3: the $0 rate saves

Steps 4-5: the minus sign is stripped, so -1 saves as 1

Steps 4-5: the minus sign is stripped, so -1 saves as 1

Steps 6-7: "." becomes "0." and saves as $0.00

Steps 6-7: "." becomes "0." and saves as $0.00

Steps 8-10: $0.00 amount and the rate name, with no Pending or out-of-policy error

Steps 8-10: $0.00 amount and the rate name, with no Pending or out-of-policy error

Steps 11-12: the saved $0 expense

Steps 11-12: the saved $0 expense

Steps 15-16: Split is available

Steps 15-16: Split is available

Steps 17-18: two $0 splits of 6 mi each

Steps 17-18: two $0 splits of 6 mi each

Step 19: the amounts are read-only, with the info message

Step 19: the amounts are read-only, with the info message

Step 20: the percentages are read-only, with the same message

Step 20: the percentages are read-only, with the same message

Step 21: third split added (6 / 6 / 0 mi)

Step 21: third split added (6 / 6 / 0 mi)

Step 22: made even (4 / 4 / 4 mi), all $0

Step 22: made even (4 / 4 / 4 mi), all $0

Steps 23-24: the $0 split is saved as three 4 mi splits at $0.00 (with https://github.com/Expensify/Auth/pull/25159 running locally)

Steps 23-24: the $0 split is saved as three 4 mi splits at $0.00

Steps 25-26: $0.76 split, $3.04 is 4 mi and $6.08 is 8 mi

Steps 25-26: $0.76 split, $3.04 is 4 mi and $6.08 is 8 mi

Steps 25-26: the $0.76 split is saved

Steps 25-26: the $0.76 split is saved

Step 26: at $0.76 the amounts are still editable and no message shows

Step 26: at $0.76 the amounts are still editable and no message shows

Offline: the $0 expense shows as pending

Offline: the $0 expense shows as pending

Offline: saved once back online

Offline: saved once back online

@OSBotify

This comment has been minimized.

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

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 Δ
...tConfirmationList/hooks/useDistanceRequestState.ts 100.00% <100.00%> (ø)
...oneyRequestConfirmationList/sections/RateField.tsx 76.59% <100.00%> (ø)
...c/components/ReportActionItem/MoneyRequestView.tsx 83.95% <100.00%> (-0.04%) ⬇️
...c/components/Search/SearchWriteActionsProvider.tsx 95.34% <100.00%> (+0.02%) ⬆️
src/components/Search/selectionBuilders.ts 99.18% <ø> (ø)
...onList/ListItem/SplitListItem/SplitAmountInput.tsx 88.23% <100.00%> (+88.23%) ⬆️
...st/ListItem/SplitListItem/SplitPercentageInput.tsx 100.00% <100.00%> (+100.00%) ⬆️
src/hooks/useFrozenSplitTransactionIDs.ts 100.00% <ø> (ø)
src/hooks/useSelectedTransactionsActions.ts 79.56% <ø> (ø)
src/libs/DistanceRequestUtils.ts 96.20% <100.00%> (+0.65%) ⬆️
... and 13 more
... and 307 files with indirect coverage changes

@OSBotify

This comment has been minimized.

@NicolasBonet
NicolasBonet marked this pull request as ready for review October 5, 2026 21:10
@NicolasBonet
NicolasBonet requested review from a team as code owners October 5, 2026 21:10
@melvin-bot
melvin-bot Bot requested review from flaviadefaria and thesahindia and removed request for a team October 5, 2026 21:10
@melvin-bot

melvin-bot Bot commented Oct 5, 2026

Copy link
Copy Markdown

@thesahindia 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 removed the request for review from a team October 5, 2026 21:10
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-12T15:25:36.874613Z 03e7847 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

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

Copy link
Copy Markdown

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: 6926c27195

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/libs/ReportSecondaryActionUtils.ts Outdated
Comment thread src/libs/actions/SplitExpenses.ts Outdated
Comment thread tests/ui/SplitListItemInputEditabilityTest.tsx
… Split when the rate is on another workspace
@NicolasBonet
NicolasBonet marked this pull request as draft October 5, 2026 22:12
@NicolasBonet
NicolasBonet marked this pull request as ready for review October 12, 2026 15:13
@NicolasBonet NicolasBonet changed the title Allow $0 distance rates and $0 distance expenses, including splits [Hold Auth#25159] Allow $0 distance rates and $0 distance expenses, including splits Oct 12, 2026
@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@thesahindia we can do the review while auth is deployed, the idea is to speed up the reviewing process so we can merge as soon as Auth is in prod 🙏

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#4:

1: Create a workspace distance rate of €0.00/mile.
2: Create a manual distance expense of 10 miles.
3: Open the expense and select Split.
4: Change the first split to 20 miles.
5: Change the second split to 5 miles.
6: Click Save.

Expected Result:
The app should prevent saving because the total split distance (25 miles) exceeds the original expense distance (10 miles).

Actual Result:
The app successfully saves both splits (20 miles and 5 miles) without displaying any error.

RF-4.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

Bug#5:

Test steps:

1: Create a workspace distance rate of $1.00/mile.
2: Create and save a manual distance expense for 10 miles. Its amount should be $10.00.
3:Go to the workspace's Distance rates settings.
4: Edit that same rate from $1.00 to $0.00/mile.
5: Return to the original expense.
6: If it still shows $10.00, open More → Split.
7: Inspect the two split distances before saving.

Expected result: The original 10 miles should not be duplicated across the two splits.

Aactual result: The two split amounts may be $5 + $5, while both retain the original 10-mile distance.

RF-5.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#6: When splitting an expense, clicking the Make split even option unexpectedly reorders the expenses being split.

RF-6.mp4

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#7: When splitting a zero-amount expense, changing the rate updates the amount, but the amount field remains read-only.

RF-7.mp4

@marufsharifi

marufsharifi commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

BUG#8: In Workspace A, configure a positive mileage rate and set the workspace as the default. Then, create a track distance expense.

Next, change the default workspace to Workspace B and open the distance expense created in Self-DM.

If the calculated amount rounds to zero despite the positive mileage rate, the Split option is incorrectly displayed. Clicking Split navigates to the “Hmm... it's not here” page instead of opening the split expense flow.

RF-8.mp4

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#9:

Preconditions:

  • create the original expense
  • Go to Workspaces → [Workspace] → Distance rates.
  • Select Add rate.
  • Enter Name: QA Historical Rate and Amount per mile: 1.00. Leave optional dates empty and select Save.
  • Go to Create → Track distance → Manual.
  • Enter 10 miles and select Next.
  • On the confirmation screen, select Rate → QA Historical Rate if it is not already selected.
  • Verify the calculated amount is $10.00, then create the expense.
  • Open the saved expense and confirm it shows 10 miles and $10.00.
  • Return to Workspaces → [Workspace] → Distance rates.
  • Open QA Historical Rate → Rate, change 1.00 to 0, and select Save.
  • Return to the original expense. Confirm it still shows $10.00. This is a required checkpoint, also present in the reviewer's

Steps:

  1. From the original $10 expense, select More → Split.
  2. On the Amount tab, check both split rows.
  3. Confirm the initial monetary shares are $5.00 and $5.00.
  4. Select Make splits even.
  5. Check both split amounts again.

Expected: The app should handle the historical $10 amount consistently. The action should not silently convert both $5 shares to $0 merely because the workspace's rate changed.

Actual: Both split amounts become $0.00.

Video_2026-10-16_15-46-37.mp4

- Split a nonzero expense whose rate is now $0 with its historical rate
- Give a $0 split of a positive rate expense no distance, and only accept $0 splits on a $0 rate
- Put the remainder of a $0 rate distance on the first split, like Make splits even
- Lock a split's amount only when the split itself is on a $0 rate
- Look up the distance rate across workspaces before offering Split
@NicolasBonet

Copy link
Copy Markdown
Contributor Author

@marufsharifi thanks for the thorough testing! Fixed in 743e1a9:

  • BUG#3: the €0.00 split of a €0.01 expense now gets 0 distance instead of the full mile, and saving only accepts a $0 split when that split is on a $0 rate. Splitting it now shows the "zero amount" error.
  • BUG#5 / BUG#9: when an expense's rate was changed to $0 after it was created, the split now uses the rate the expense was calculated with (amount ÷ distance). A $10 / 10 mi expense splits into $5 / 5 mi each, and "Make splits even" keeps $5 / $5.
  • BUG#6: the extra distance from rounding (e.g. 12.01 mi → 6.01 + 6.00) is now on the first split both when the splits are created and after "Make splits even", so the distances no longer swap.
  • BUG#7: amounts are now only read-only for splits on a $0 rate. A split moved to a positive rate can be edited. Percentages stay read-only when the original total is $0, because any percentage of $0 is $0.
  • BUG#8: "Split" now looks up the expense's rate in the workspace that owns it, not just the default workspace. A positive rate whose amount rounds to $0 no longer offers Split, and the split page agrees with the menu.
  • BUG#4: this is expected. Distance splits can already add up to more than the original on purpose: on main, a $10 / 10 mi expense split as 20 mi + 5 mi saves fine, with only a non-blocking warning. A $0 rate expense behaves the same way.

Could you re-check, please?

@trjExpensify

Copy link
Copy Markdown
Contributor

@marufsharifi what's your ETA on the re-review?

@marufsharifi

Copy link
Copy Markdown
Contributor

@marufsharifi what's your ETA on the re-review?

@trjExpensify, I am working on this, i will update here in a few hours. thanks.

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#1:

Steps to Reproduce:

  1. Go to Workspaces → Distance rates.
  2. Create a rate named Zero Split Test at $0.00/mile.
  3. Create a manual distance expense of 10 miles ($0.00) using this rate.
  4. Open the expense and confirm More → Split is available.
  5. Go back to Distance rates.
  6. Change Zero Split Test to $1.00/mile and save.
  7. Reopen the original $0.00 expense.
  8. Click More and check whether Split is still available.

Expected Result:
The Split option should remain available for the existing $0.00 distance expense, even after its workspace distance rate changes to $1.00/mile.

Actual Result:
The Split option disappears from the More menu, preventing the user from splitting the existing expense.

RF-1.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#2:

Preconditions:

Desktop Chrome, online. Use your own unreported expense and a workspace rate of $0.01 per mile.

Steps:

  1. In Workspaces → your workspace → Distance rates, add an enabled rate of $0.01/mile.
  2. Open Your space and create a Track distance → Manual expense for 0.4 miles, selecting that rate.
  3. Confirm the saved expense shows 0.4 miles, $0.00, and remains Unreported.
  4. Go to Spend → Expenses.
  5. Select that expense using its checkbox, without opening it.
  6. Choose Split from the selection action bar, or its More menu.
  7. Trigger checkpoint: The selected row is unreported, its amount is $0.00, and opening it confirms a positive $0.01/mile rate with 0.4 miles.

Expected Result:

Split should be unavailable for this rounded-to-zero positive-rate expense, matching the current correction’s stated behavior.

Actual Behavior:

Search selection offers Split, but choosing it opens Hmm... it’s not here

RF-2.mp4

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#3:

Test Steps:

  1. Add an enabled workspace rate named QA Historical, at $1.00/mile.
  2. Create and save a manual distance expense for 10 miles using that rate. Confirm $10.00.
  3. While online, open that rate’s settings once so the screen is available locally, then return to the expense.
  4. Disconnect internet access.
  5. Open the expense’s Distance, change it to 20 miles, and select Save.
  6. Confirm the expense now displays 20 miles and $20.00.
  7. Still offline, open Workspaces → your workspace → Distance rates → QA Historical → Rate. Change it to 0 and select Save.
  8. Return to the expense. Confirm it still displays 20 miles and $20.00.
  9. Select More → Split and inspect both row descriptions before saving.
  10. Trigger checkpoint: All changes after disconnection remain offline; the workspace rate displays zero while the expense still displays 20 miles/$20.00.

Expected Behavior:

The initial equal split should contain $10.00 and 10 miles per row, consistently preserving the current expense.

Actual Behavior:

Both rows show $10.00 and 20 miles, often with a calculated $0.50/mile merchant description. The mileage has doubled during initialization.

RF-3.mp4

@marufsharifi

Copy link
Copy Markdown
Contributor

Confirmation#1:

  1. Create a workspace distance rate of $0.00/mile.
  2. Create a manual distance expense of 10 miles / $0.00.
  3. Open the expense and select More → Split.
  4. Verify that two splits of 5 miles / $0.00 are displayed.
  5. Click Add split to create a third split.
  6. Leave the third split at 0 miles / $0.00.
  7. Click Save.

The error message says Please enter a valid amount before continuing. However, since the amount is 0.0 for all three splits, it should either highlight the invalid field (at least the last one) or display a more relevant error message, such as Please enter a valid distance before continuing.

cc @flaviadefaria could you please confirm this. thanks.

CF-1.mov

@flaviadefaria

Copy link
Copy Markdown
Contributor

The error message says Please enter a valid amount before continuing. However, since the amount is 0.0 for all three splits, it should either highlight the invalid field (at least the last one) or display a more relevant error message, such as Please enter a valid distance before continuing.

This error message has always referred to the distance amount, even when the rate wasn’t $0. But I agree that it’s particularly unclear in this scenario. I think clarifying the message would be helpful regardless of the rate, so let’s update it across all scenarios, not just when the rate is $0.

We can go with your suggestion:

Please enter a valid distance before continuing.

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#4

Steps:

1: Create two distance rates, QA Original and QA Other, both at $1.00/mile.
2: Create a 10-mile distance expense for $10.00 using QA Original.
3: Change QA Original to $0.00/mile in workspace settings.
4: Return to expense > check the expense preview (amount and rate).
5: open the expense > check the amount and rate
6: click more > split > check the amount and rate.

Expected Behavior:

The rate should change to $0.0/mile

Actual Behavior:

The rate remains outdated when splitting the expense.

Rf-4.mov

@NicolasBonet

Copy link
Copy Markdown
Contributor Author

Thanks @marufsharifi! Fixed in bf8855f:

  • BUG#1: A $0 expense whose rate was changed from $0 afterwards keeps Split, and its splits stay on $0, since its amount can only have come from a $0 rate. An expense that's $0 because its distance rounds down on a positive rate (e.g. 0.4 mi at $0.01/mi) still doesn't offer Split.
  • BUG#2: Search now checks the expense's rate in the workspace that owns it, so it agrees with the expense page and no longer offers Split for that rounded-down expense.
  • BUG#3: The split now uses the expense's edited amount, so a $20 / 20 mi expense splits into $10 / 10 mi each. This also happened online, not only offline.
  • BUG#4: This is intentional. The expense is still $10, so the split uses the $1/mi it was calculated with, and each split is $5 / 5 mi. Showing $0/mi would mean $0 splits, which is what BUG#9 flagged.
  • Confirmation#1: Distance splits now show "Please enter a valid distance before continuing." for any rate, per @flaviadefaria.

Could you re-check, please?

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index e3215535..37c2e4b4 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -1392,8 +1392,8 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountGreaterThanOriginal: (amount: string) => `Der Gesamtbetrag ist um ${amount} höher als die ursprüngliche Ausgabe.`,
         totalAmountLessThanOriginal: (amount: string) => `Der Gesamtbetrag ist um ${amount} geringer als die ursprüngliche Ausgabe.`,
         splitExpenseZeroAmount: 'Bitte gib einen gültigen Betrag ein, bevor du fortfährst.',
-        splitExpenseZeroDistance: 'Bitte gib eine gültige Entfernung ein, bevor du fortfährst.',
-        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Der Betrag kann nicht bearbeitet werden, wenn die ursprüngliche Summe ${amount} beträgt.`,
+        splitExpenseZeroDistance: 'Bitte geben Sie eine gültige Entfernung ein, bevor Sie fortfahren.',
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Der Betrag kann nicht bearbeitet werden, wenn die ursprüngliche Summe ${amount} ist.`,
         splitExpenseOneMoreSplit: 'Keine Aufteilungen hinzugefügt. Füge mindestens eine hinzu, um zu speichern.',
         splitExpenseEditTitle: (amount: string, merchant: string) => `${amount} für ${merchant} bearbeiten`,
         removeSplit: 'Aufteilung entfernen',
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 8923c26e..c18175b7 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -244,7 +244,7 @@ const translations: TranslationDeepObject<typeof en> = {
             pleaseCompleteForm: 'Συμπληρώστε τη φόρμα παραπάνω για να συνεχίσετε',
             pleaseSelectOne: 'Παρακαλώ επιλέξτε μια από τις παραπάνω επιλογές',
             invalidRateError: 'Παρακαλούμε εισαγάγετε έγκυρο συντελεστή',
-            lowRateError: 'Το ποσοστό δεν μπορεί να είναι αρνητικό',
+            lowRateError: 'Το επιτόκιο δεν μπορεί να είναι αρνητικό',
             email: 'Παρακαλούμε εισαγάγετε μια έγκυρη διεύθυνση email',
             login: 'Προέκυψε σφάλμα κατά τη σύνδεση. Παρακαλούμε δοκιμάστε ξανά.',
         },
@@ -1439,8 +1439,8 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountGreaterThanOriginal: (amount: string) => `Το συνολικό ποσό είναι κατά ${amount} μεγαλύτερο από την αρχική δαπάνη.`,
         totalAmountLessThanOriginal: (amount: string) => `Το συνολικό ποσό είναι κατά ${amount} λιγότερο από την αρχική δαπάνη.`,
         splitExpenseZeroAmount: 'Παρακαλώ εισαγάγετε ένα έγκυρο ποσό πριν συνεχίσετε.',
-        splitExpenseZeroDistance: 'Παρακαλώ εισαγάγετε μια έγκυρη απόσταση πριν συνεχίσετε.',
-        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Το ποσό δεν μπορεί να επεξεργαστεί όταν το αρχικό σύνολο είναι ${amount}.`,
+        splitExpenseZeroDistance: 'Παρακαλούμε εισαγάγετε μια έγκυρη απόσταση πριν συνεχίσετε.',
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Δεν είναι δυνατή η επεξεργασία του ποσού όταν το αρχικό σύνολο είναι ${amount}.`,
         splitExpenseOneMoreSplit: 'Δεν προστέθηκαν κατανομές. Προσθέστε τουλάχιστον μία για να αποθηκεύσετε.',
         splitExpenseEditTitle: (amount: string, merchant: string) => `Επεξεργαστείτε ${amount} για ${merchant}`,
         removeSplit: 'Αφαίρεση διαχωρισμού',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c71121ab..6220c0bf 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -231,7 +231,7 @@ const translations: TranslationDeepObject<typeof en> = {
             pleaseCompleteForm: 'Por favor complete el formulario de arriba para continuar',
             pleaseSelectOne: 'Seleccione una de las opciones',
             invalidRateError: 'Por favor, introduce una tarifa válida',
-            lowRateError: 'La tarifa no puede ser negativa',
+            lowRateError: 'La tasa no puede ser negativa',
             email: 'Por favor, introduzca una dirección de correo electrónico válida',
             login: 'Se produjo un error al iniciar sesión. Por favor intente nuevamente.',
         },
@@ -1383,7 +1383,7 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountLessThanOriginal: (amount) => `El importe total es ${amount} menor que el gasto original.`,
         splitExpenseZeroAmount: 'Por favor, introduce un importe válido antes de continuar.',
         splitExpenseZeroDistance: 'Por favor, introduce una distancia válida antes de continuar.',
-        splitExpenseZeroTotalAmountNotEditable: (amount) => `El importe no se puede editar cuando el total original es ${amount}.`,
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `No se puede editar el importe cuando el total original es ${amount}.`,
         splitExpenseOneMoreSplit: 'No se han añadido divisiones. Añade al menos una para guardar.',
         splitExpenseEditTitle: (amount, merchant) => `Editar ${amount} para ${merchant}`,
         removeSplit: 'Eliminar división',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 1b16cd2c..37caf28c 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -239,7 +239,7 @@ const translations: TranslationDeepObject<typeof en> = {
             pleaseCompleteForm: '続行するには上のフォームに入力してください',
             pleaseSelectOne: '上からオプションを選択してください',
             invalidRateError: '有効なレートを入力してください',
-            lowRateError: 'レートを負の値にすることはできません',
+            lowRateError: 'レートをマイナスにすることはできません',
             email: '有効なメールアドレスを入力してください',
             login: 'ログイン中にエラーが発生しました。もう一度お試しください。',
         },
@@ -1376,7 +1376,7 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountLessThanOriginal: (amount: string) => `合計金額は元の経費より${amount}少なくなっています。`,
         splitExpenseZeroAmount: '続行する前に有効な金額を入力してください。',
         splitExpenseZeroDistance: '続行する前に有効な距離を入力してください。',
-        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `元の合計が${amount}の場合、金額は編集できません。`,
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `元の合計金額が ${amount} の場合、金額は編集できません。`,
         splitExpenseOneMoreSplit: '分割が追加されていません。保存するには少なくとも 1 件追加してください。',
         splitExpenseEditTitle: (amount: string, merchant: string) => `${merchant} の ${amount} を編集`,
         removeSplit: '分割を削除',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 565b25de..e4d4b66e 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -1391,7 +1391,7 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountLessThanOriginal: (amount: string) => `Het totale bedrag is ${amount} minder dan de oorspronkelijke uitgave.`,
         splitExpenseZeroAmount: 'Voer een geldig bedrag in voordat je verdergaat.',
         splitExpenseZeroDistance: 'Voer een geldige afstand in voordat je verdergaat.',
-        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Het bedrag kan niet worden bewerkt als het oorspronkelijke totaal ${amount} is.`,
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Bedrag kan niet worden bewerkt wanneer het oorspronkelijke totaal ${amount} is.`,
         splitExpenseOneMoreSplit: 'Geen splitsingen toegevoegd. Voeg er minimaal één toe om op te slaan.',
         splitExpenseEditTitle: (amount: string, merchant: string) => `${amount} voor ${merchant} bewerken`,
         removeSplit: 'Splits verwijderen',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 1f608775..e6cf56a2 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -1424,7 +1424,7 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountGreaterThanOriginal: (amount: string) => `Kwota łączna jest o ${amount} wyższa niż pierwotny wydatek.`,
         totalAmountLessThanOriginal: (amount: string) => `Kwota całkowita jest o ${amount} mniejsza niż pierwotny wydatek.`,
         splitExpenseZeroAmount: 'Wprowadź prawidłową kwotę przed kontynuowaniem.',
-        splitExpenseZeroDistance: 'Wprowadź prawidłową odległość przed kontynuowaniem.',
+        splitExpenseZeroDistance: 'Wpisz prawidłową odległość, zanim przejdziesz dalej.',
         splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Kwoty nie można edytować, gdy pierwotna suma wynosi ${amount}.`,
         splitExpenseOneMoreSplit: 'Nie dodano podziałów. Dodaj co najmniej jeden, aby zapisać.',
         splitExpenseEditTitle: (amount: string, merchant: string) => `Edytuj ${amount} dla ${merchant}`,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 179ea368..06b8bdc1 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -1337,8 +1337,8 @@ const translations: TranslationDeepObject<typeof en> = {
         totalAmountGreaterThanOriginal: (amount: string) => `总金额比原始报销多出 ${amount}。`,
         totalAmountLessThanOriginal: (amount: string) => `总金额比原始报销少 ${amount}。`,
         splitExpenseZeroAmount: '请在继续之前输入有效金额。',
-        splitExpenseZeroDistance: '请在继续之前输入有效距离。',
-        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `原始总额为 ${amount} 时,无法编辑金额。`,
+        splitExpenseZeroDistance: '请先输入有效的距离,然后再继续。',
+        splitExpenseZeroTotalAmountNotEditable: (amount: string) => `当原始总额为 ${amount} 时,无法编辑金额。`,
         splitExpenseOneMoreSplit: '尚未添加拆分。请至少添加一项以保存。',
         splitExpenseEditTitle: (amount: string, merchant: string) => `编辑 ${merchant} 的 ${amount}`,
         removeSplit: '移除拆分',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@trjExpensify

Copy link
Copy Markdown
Contributor

@marufsharifi how you looking on the re-review?

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#1:

Steps

  1. Open Workspaces → your workspace → Distance rates.
  2. Add an enabled rate of €0.01/mile.
  3. Create a Manual distance expense for 2 miles using that rate. Confirm the amount is €0.02.
  4. Open the saved expense and select More → Split.
  5. Open the first split, change its Distance to 0.40 miles, and save the split details.
  6. Open the second split, change its Distance to 1.60 miles, and save the split details.
  7. Confirm the split rows show:
    • 0.40 mi @ €0.01/mile — €0.00
    • 1.60 mi @ €0.01/mile — €0.02
  8. Click Save on the main Split screen.

Expected Behavior
If a zero-amount split on a positive rate is prohibited, saving should display an amount-related error explaining that the first split rounds to €0.00. It should not identify either distance as invalid.

Actual Behavior
Saving is blocked with “Please enter a valid distance before continuing,” although both distances are positive and total the original 2 miles.

RF-1.mov

@marufsharifi

marufsharifi commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

BUG#2

Steps

  1. Open Workspaces → your workspace → Distance rates.
  2. Add an enabled rate named Small Positive Rate at €0.01/mile.
  3. Create a Manual distance expense for 0.40 miles using Small Positive Rate.
  4. Confirm the calculated amount rounds to €0.00, then click Create expense.
  5. Open the saved expense and select More. Confirm Split is unavailable.
  6. Return to Distance rates and change Small Positive Rate from €0.01/mile to €0.00/mile.
  7. Save the rate change.
  8. Refresh the app, reopen the same expense, and select More.

Expected Behavior
Split should remain unavailable under the PR’s stated rule for expenses created on a positive rate whose amounts round to zero. Updating the workspace rate should not reclassify the existing expense as originally created at a zero rate.

Actual Behavior
Split becomes available after the workspace rate changes to €0.00/mile, although the existing expense was created at €0.01/mile.

RF-2.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#3

Steps

  1. Open Workspaces → your workspace → Distance rates.
  2. Add an enabled rate named QA Paid Mileage at €1.00/mile.
  3. Open Distance rates → Settings → Exclude commutes.
  4. Configure a fixed exclusion of 10 miles per claim.
  5. Open the workspace expenses chat and select + → Track distance → Map.
  6. Enter two nearby addresses whose route is greater than zero but shorter than 10 miles. The recording uses a 0.82-mile route.
  7. Continue to Confirm details, select QA Paid Mileage, and create the expense.
  8. Open the saved expense. Confirm it shows €0.00, 0.00 reimbursable miles, and the commute deduction.
  9. Open Rate and verify QA Paid Mileage remains €1.00/mile.
  10. Return to the expense and select More → Split.
  11. Inspect both split rows without saving.

Expected Behavior
Commute exclusion should not change the selected mileage rate. If splitting is offered, the split flow should preserve €1.00/mile and handle the commute deduction consistently.

Actual Behavior
The Split header shows “€0.00 from 0.00 mi @ €1.00 / mi,” but both split rows show “0.41 mi @ €0.00 / mi” with €0.00 amounts. The selected workspace rate was never changed.

RF-3.mp4

@marufsharifi marufsharifi left a comment

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.

Just two minor suggestions.

if (isDistanceRequest && splitExpense.customUnit) {
const {unit, rate} = resolveSplitItemRate({customUnit: splitExpense.customUnit, fallbackMileageRate: mileageRate, policy, policies});
if (rate && rate > 0) {
if (rate === 0) {

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.

No automated test covers this "split by date" $0-rate branch.

if (isEditingSplit && transaction) {
// A split keeps the readings of the whole trip while its distance is only its share of it, so readings
// saved unchanged must not replace the split's distance with the whole trip's distance
const hasReadingChanges = splitDraftTransaction?.comment?.odometerStart !== start || splitDraftTransaction?.comment?.odometerEnd !== end;

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 guard has no test

@marufsharifi

Copy link
Copy Markdown
Contributor

@NicolasBonet, all done on my end. Over to you. Thanks!

@FitseTLT

Copy link
Copy Markdown
Contributor

closing in favor of #104756

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.

6 participants