Skip to content

[HOLD #104135] Show an error when a UK fuel-band rate has no valid VAT tax rate #104646

Open

Description

@neil-marcellini

(Neil's AI agent)

Part of the UK Advisory Fuel Rates tracking issue

Depends on: #104135, for the fuel-band series helper. Expensify/Expensify#694186 adds the dismiss command and must deploy first.
Design: Workspace VAT tax rate
Discussion: Slack thread

Problem

A UK fuel-band rate can end up with no valid VAT tax rate:

  • the workspace's accounting integration has no 20% tax rate, so Auth adds the rate without one (Expensify/Expensify#694184);
  • a sync or an admin removes or disables the linked tax rate.

Expenses on that rate then get no VAT, or the workspace default tax rate. Nothing tells the admin. The workspace's red dot checks don't cover distance rates (menu item). The rate page shows a blank tax rate.

Solution

Check

  • Add a PolicyUtils helper that returns the workspace's enabled fuel-band rates whose tax rate isn't valid. It applies only when tax tracking and distance tax are on.

Red dot path

  • Workspace list row and Workspaces tab: add the check to getPolicyBrickRoadIndicatorStatus and a new status in usePolicyIndicatorChecks.
  • Workspace menu: give the Distance rates item a brickRoadIndicator.
  • Distance rates page: put the red dot on the More button and its Settings option.

Errors

  • Settings page: show an error under Track tax.
    • With an integration: "Please add a 20% tax rate in {integration name} to track tax on the UK Advisory Fuel Rates." Get the name from getCurrentConnectionName.
    • Without one: "Please add a 20% tax rate to track tax on the UK Advisory Fuel Rates."
  • Rate details page: on each affected rate, show a red dot and the same message on the tax rate field. Use the composable MenuItem red dot and help text, like CurrencyPicker.
  • Errors clear when Auth re-links the rate after a sync, or when the admin picks a tax rate.

Dismissing

Some admins may want fuel-band rates without VAT. They can dismiss the error.

  • Both errors show a dismiss X, using OfflineWithFeedback's onClose. Dismissing either one hides the error, and every red dot leading to it, for every admin of that workspace.
  • Save it on the policy, as customUnits[Distance].attributes.isUKFuelRateTaxErrorDismissed: true.
    • It's per workspace, not per rate, so next quarter's new rates don't bring the error back.
    • It arrives with the policy, so the check still only needs the policy.
  • App: a new dismissUKFuelRateTaxError action, shaped like enableDistanceRequestTax.
    • It calls the new DismissUKFuelRateTaxError API command from Expensify/Expensify#694186, added as WRITE_COMMANDS.DISMISS_UK_FUEL_RATE_TAX_ERROR.
    • Its parameters match EnableDistanceRequestTax: policyID, and customUnit, a JSON string with customUnitName, customUnitID, and the current attributes plus isUKFuelRateTaxErrorDismissed: true.
    • It sends optimistic and failure data.

Tests

Unit tests for the helper:

  • an empty, deleted, disabled or pending-delete tax code;
  • a valid code;
  • tax tracking or distance tax off;
  • AMAP and non-UK rates;
  • a non-admin;
  • a dismissed workspace, and another workspace that isn't dismissed.

Add a test that dismissUKFuelRateTaxError sets the attribute optimistically and hides the error.
Extend getWorkspaceMenuItemsTest and usePolicyIndicatorChecksTest.

Deploys after merge. Fuel-band rates don't exist until Expensify/Expensify#692151.

Issue OwnerCurrent Issue Owner: @Krishna2323

Activity

  1. neil-marcellini commented on Oct 21, 2026

    @neil-marcellini
    ContributorAuthor

    (Neil's AI agent)

    @Krishna2323, with @thelullabyy as C+: Neil would love for you two to take this one. Krishna2323 built the Release 1 distance rate UI and the auto-update toggle. thelullabyy reviewed that work and is the C+ on #104135, which this builds on.

    If you're interested, please comment here and Neil will assign you.

  2. Krishna2323 commented on Oct 21, 2026

    @Krishna2323
    Contributor

    Yes, interested!

  3. neil-marcellini commented on Oct 22, 2026

    @neil-marcellini
    ContributorAuthor

    Sweet. I'm going to add a rule to my agent now to not automatically assume @thelullabyy always wants to be C+ 😆

  4. neil-marcellini commented on Oct 22, 2026

    @neil-marcellini
    ContributorAuthor

    (Neil's AI agent)

    @Krishna2323 heads up: after a Slack discussion I updated the error copy in the issue to "Please add a 20% tax rate in {integration name} to track tax on the UK Advisory Fuel Rates." Without an integration, it's "Please add a 20% tax rate to track tax on the UK Advisory Fuel Rates."

  5. Krishna2323 commented on Oct 22, 2026

    @Krishna2323
    Contributor

    @MelvinBot could you please post a proposal keep this in mind:

    Depends on: #104135, for the fuel-band series helper. Expensify/Expensify#694186 adds the dismiss command and must deploy first.

  6. MelvinBot commented on Oct 22, 2026

    @MelvinBot
    Contributor

    Proposal

    Sequencing: This work is on hold until both of these land:

    1. UK fuel-band rates in the App's types, optimistic copy and lightning bolt check merges. Its PR, Copy UK fuel-band rates with their reclaimable fraction and check it for the lightning bolt, is still open. It adds the getGovernmentRateSeries and isFuelBandGovernmentRate helpers that this proposal uses.
    2. The backend DismissUKFuelRateTaxError command deploys. The App PR can be built and reviewed before then, but it must not merge until the command is live.

    What is the root cause of that problem?

    Nothing in the App checks whether a distance rate's tax code is still valid.

    What changes do you think we should make in order to solve the problem?

    1. Check helper. Add getUKFuelRatesWithInvalidTax(policy) to PolicyUtils/errors.ts. Export it from @libs/PolicyUtils. It returns the distance unit's enabled rates where all of these are true:

      • isFuelBandGovernmentRate(rate.attributes.governmentRate.sourceRateID) is true.
      • !isSelectableTaxCode(policy, rate.attributes.taxRateExternalID).

      It returns an empty array in any of these cases:

      • The user isn't an admin.
      • policy.tax.trackingEnabled or the unit's attributes.taxEnabled is off.
      • attributes.isUKFuelRateTaxErrorDismissed is true.

      Add shouldShowUKFuelRateTaxError(policy) as the boolean wrapper. The check reads only policy, so errors clear on their own when a sync re-links the rate or the admin picks a tax rate.

    2. Red dots.

      • Add shouldShowUKFuelRateTaxError to getPolicyBrickRoadIndicatorStatus for the workspace list row.
      • Add CONST.INDICATOR_STATUS.HAS_UK_FUEL_RATE_TAX_ERROR and a tuple in usePolicyIndicatorChecks for the Workspaces tab.
      • Give the Distance rates item a brickRoadIndicator, the same way Taxes does.
      • On the distance rates page, set brickRoadIndicator on the More button and on its Settings option. This copies DomainMembersPage.
    3. Settings page error. Below the Track tax row, add an OfflineWithFeedback that shows the error, with onClose set to dismiss it.

      • With an integration: Please add a 20% tax rate in {integration name} to track tax on the UK Advisory Fuel Rates. Get the name from getCurrentConnectionName.
      • Without one: Please add a 20% tax rate to track tax on the UK Advisory Fuel Rates.
      • Add both strings to every src/languages/*.ts file under workspace.distanceRates.
    4. Rate details error. If the rate is in the helper's result, build the tax field from composable parts, like CurrencyPicker: MenuItem.Root, MenuItemField.Row, MenuItem.BrickRoadIndicator, MenuItem.Chevron, and MenuItem.HelpText isError. The existing OfflineWithFeedback also gets the dismiss onClose.

    5. Dismiss action.

      • Add isUKFuelRateTaxErrorDismissed?: boolean to the custom unit Attributes type.
      • Add WRITE_COMMANDS.DISMISS_UK_FUEL_RATE_TAX_ERROR: 'DismissUKFuelRateTaxError' and DismissUKFuelRateTaxErrorParams (policyID, customUnit).
      • Add dismissUKFuelRateTaxError(policyID, customUnit) to Policy.ts, shaped like enableDistanceRequestTax.
        • Optimistic: merge attributes.isUKFuelRateTaxErrorDismissed: true and its pendingFields entry.
        • Success: clear the pending field.
        • Failure: restore the previous attributes and set a generic errorFields error.
        • The customUnit param is the JSON of customUnitName, customUnitID, and the current attributes plus the flag.
    6. Tests.

      • In PolicyUtilsTest, test the helper with each of these:
        • an empty, deleted, disabled or pending-delete code;
        • a valid code;
        • tax tracking or distance tax turned off;
        • AMAP and non-UK rates;
        • a non-admin;
        • a dismissed workspace next to one that isn't dismissed.
      • In PolicyTest, check that dismissUKFuelRateTaxError sets the flag optimistically and the helper then returns nothing.
      • Extend getWorkspaceMenuItemsTest and usePolicyIndicatorChecksTest.

    What alternative solutions did you explore? (Optional)

    You could dismiss the error per rate instead of per workspace. That would bring the error back each quarter when new band rates arrive, which the issue rules out.

    Implementation notes
    • Import cycle. After the dependency PR, PolicyDistanceRatesUtils imports ./PolicyUtils. If PolicyUtils/errors.ts imports isFuelBandGovernmentRate back from it, the two files import each other. Both helpers are pure string functions, so I'd move them into a small leaf module that both files import. PolicyDistanceRatesUtils can re-export them.
    • Tax eligibility. isTaxTrackingEnabled needs an expense-chat flag, so the helper reads policy.tax.trackingEnabled and the unit's taxEnabled directly. PolicyDistanceRatesSettingsPage does the same.
    • Dismiss X on rate details. MenuItem.HelpText has no close button. To get one, the message must also be in the wrapping OfflineWithFeedback's errors, and then it shows twice. My suggestion: keep the red dot and help text on the field, and add the dismiss X only on the Settings page error. The alternative is to show the message once, as an OfflineWithFeedback error row under the field. Please confirm which you want.
    • More button icon. ButtonWithDropdownMenu with shouldUseOptionIcon and one option may show the gear icon instead of the red dot. I'll check this on web during testing.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/104646) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

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

Metadata

Metadata

Labels

DailyKSv2InternalRequires API changes or must be handled by Expensify staff

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions