Skip to content

[Due for payment 2026-06-23] [$125] Remove tax, total and billable above amount in single expense report #92951

Closed

Description

@rlinoz

Follow up from #90411 (comment)

Issue OwnerCurrent Issue Owner: @mallenexpensify
Upwork Automation - Do Not Edit

Activity

  1. rlinoz commented on Jun 8, 2026

    @rlinoz
    ContributorAuthor

    @mukhrr for some reason I am not able to assign you here.

  2. changed the title [-]Remove tax, total and billable above amount in single expense report[/-] [+][$125] Remove tax, total and billable above amount in single expense report[/+] on Jun 8, 2026
  3. melvin-bot commented on Jun 8, 2026

    @melvin-bot

    ⚠️ Could not update price automatically because there is no linked Upwork Job ID. The BZ team member will need to update the price manually in Upwork.

  4. David-2002065 commented on Jun 8, 2026

    @David-2002065
    Contributor

    Proposal

    What is the root cause of that problem?

    MoneyReportView treats only the non-reimbursable single-expense case as "single", so Total/Billable/Tax stay rendered for every other single-expense flavor:

    Separately, shouldShowReportField never checks policy.areReportFieldsEnabled. The render block guards on it at https://github.com/Expensify/App/blob/main/src/components/ReportActionItem/MoneyReportView.tsx#L187, but the gate at https://github.com/Expensify/App/blob/main/src/components/ReportActionItem/MoneyReportView.tsx#L155-L159 doesn't — so when fields are disabled the rows hide but the divider at https://github.com/Expensify/App/blob/main/src/components/ReportActionItem/MoneyReportView.tsx#L308 stays on. That's the stray HR @shawnborton flagged once the rows above it were removed.

    And since we'd be hiding the report-level Tax row, the per-expense Tax field loses its "Converted X" hint for foreign-currency taxed expenses — Amount already has this pattern at https://github.com/Expensify/App/blob/main/src/components/ReportActionItem/MoneyRequestView.tsx#L590-L595 and Tax needs the same.

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

    In src/components/ReportActionItem/MoneyReportView.tsx, broaden the single-expense check so it covers any one-transaction report, and gate Total/Billable/Tax + the divider on it:

    - const isSingleNonReimbursableExpense = isSingleTransactionReport(report, visibleTransactions) && visibleTransactions.at(0)?.reimbursable === false;
    - const shouldShowReimbursabilityBreakdown = !isSingleNonReimbursableExpense && !!nonReimbursableSpend;
    - const shouldShowBreakdown = shouldShowReimbursabilityBreakdown || !!billableTotal || (!!taxTotal && isTaxEnabled);
    + const isSingleExpenseReport = isSingleTransactionReport(report, visibleTransactions);
    + // For a one-expense report Total/Billable/Tax just repeat what the Amount row already shows (incl. the converted value).
    + const shouldShowReimbursabilityBreakdown = !isSingleExpenseReport && !!nonReimbursableSpend;
    + const shouldShowBreakdown = !isSingleExpenseReport && (shouldShowReimbursabilityBreakdown || !!billableTotal || (!!taxTotal && isTaxEnabled));
    + const shouldShowTotalRow = shouldShowTotal && !isSingleExpenseReport;

    Then swap the shouldShowTotal guard on the Total row and the divider for shouldShowTotalRow, and restore the missing field-enabled gate so the divider drops too:

      const shouldShowReportField =
          !isClosedExpenseReportWithNoExpenses &&
          (isPaidGroupPolicyExpenseReport || isInvoiceReport) &&
    +     !!policy?.areReportFieldsEnabled &&
          (!isCombinedReport || !isOnlyTitleFieldEnabled) &&
          !sortedPolicyReportFields.every(shouldHideSingleReportField);

    In src/components/ReportActionItem/MoneyRequestView.tsx, compute formattedConvertedTaxAmount next to formattedTaxAmount, build taxAmountDescription next to amountDescription using the same shouldShowConvertedAmount check (and skip the suffix when the converted tax is zero so tax-exempt expenses don't render "Converted 0.00"), and pass it as description on the Tax MenuItemWithTopDescription. This keeps the converted-tax info we used to get from the report-level Tax row reachable on the expense itself.

    @shawnborton clarified at #92042 (comment) that the intent is to remove all rows above Amount in any single-expense report, not just non-reimbursable ones. The change above generalizes the single-expense check, closes the divider gap from #44695, and keeps converted-tax visible per-expense.

    What alternative solutions did you explore? (Optional)

    I considered hiding only Total and keeping Billable/Tax, but those rows also just echo per-expense values for one-expense reports and design specifically called them out — partial hiding leaves the same visual noise. Threading isSingleExpenseReport up and overloading the existing shouldShowTotal prop is also tempting, but that prop is owned by callers like ReportActionItemThread and toggling it would silently affect unrelated screens; deriving the flag locally from visibleTransactions keeps the rule scoped to this component. For the divider, gating on sortedPolicyReportFields.length instead of areReportFieldsEnabled was an option, but it misses the case where the policy still has fields configured while the feature is disabled — which is exactly the repro.

  5. heroking777 commented on Jun 8, 2026

    @heroking777

    To address the issue of removing tax, total, and billable amounts above a single expense report, let's focus on the core problem area in the ExpenseReport.js file. The root cause likely involves unnecessary calculations or display logic that needs to be simplified.

    Proposed Solution

    1. Identify the Display Logic:
      Locate the section in ExpenseReport.js where tax, total, and billable amounts are calculated and displayed. This is typically found within a function responsible for rendering expense details.

    2. Simplify the Calculation:
      Remove or comment out the lines of code that calculate and display these values. Ensure that only the essential information (like the expense description and amount) remains visible.

    3. Update Tests:
      If there are associated tests in ExpenseReport.test.js, update them to reflect the changes. Ensure that tests still cover the necessary functionality without relying on the removed calculations.

    Diff Summary

    - const tax = calculateTax(expense.amount);
    - const total = expense.amount + tax;
    - const billable = isBillable(expense);
    
    // Display only essential information
    return (
      <div>
        <p>{expense.description}</p>
        <p>Amount: {expense.amount}</p>
      </div>
    );

    Conclusion

    By simplifying the display logic in ExpenseReport.js, we directly address the issue without introducing unnecessary complexity. This approach ensures a clean, efficient solution that aligns with best practices and maintains the integrity of the expense reporting system.

  6. mukhrr commented on Jun 8, 2026

    @mukhrr
    Contributor

    @mukhrr for some reason I am not able to assign you here.

    @rlinoz i think you can now

  7. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Jun 8, 2026
  8. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Jun 8, 2026
  9. melvin-bot commented on Jun 8, 2026

    @melvin-bot

    Current assignee @hoangzinh is eligible for the External assigner, not assigning anyone new.

  10. removed
    Help WantedApply this label when an issue is open to proposals by contributors
    on Jun 8, 2026
  11. KJ21-ENG commented on Jun 8, 2026

    @KJ21-ENG
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In a single-expense report, the report header still renders report-level rows above the expense amount: Total, Tax, and Billable. These rows duplicate information already available in the single expense detail and leave an unnecessary divider/header area.

    What is the root cause of that problem?

    MoneyReportView calculates the report-level totals from getMoneyRequestSpendBreakdown() and getBillableAndTaxTotal(), then renders the Total row and breakdown block whenever those values are truthy, regardless of whether the report contains only one transaction:

    const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = getMoneyRequestSpendBreakdown(report);
    const transactions = useReportTransactions(report?.reportID);
    const {billableTotal, taxTotal} = getBillableAndTaxTotal(report, transactions);
    const isTaxEnabled = isPolicyTaxEnabled(policy);
    const shouldShowBreakdown = nonReimbursableSpend || !!billableTotal || (!!taxTotal && isTaxEnabled);

    {shouldShowTotal && (
    <View style={[styles.flexRow, styles.pointerEventsNone, styles.containerWithSpaceBetween, styles.ph5, styles.pv2]}>
    <View style={[styles.flex1, styles.justifyContentCenter]}>
    <Text
    style={[styles.textLabelSupporting]}
    numberOfLines={1}
    >
    {translate('common.total')}
    </Text>
    </View>
    <View style={[styles.flexRow, styles.justifyContentCenter]}>
    {isSettled && !isPartiallyPaid && (
    <View style={[styles.defaultCheckmarkWrapper, styles.mh2]}>
    <Icon
    src={icons.Checkmark}
    fill={theme.success}
    />
    </View>
    )}
    {!isTotalUpdated && (!isOffline || isTotalPending) ? (
    <ActivityIndicator
    style={[styles.moneyRequestLoadingHeight]}
    color={theme.textSupporting}
    reasonAttributes={totalActivityReasonAttributes}
    />
    ) : (
    <Text
    numberOfLines={1}
    style={[styles.taskTitleMenuItem, styles.alignSelfCenter, !isTotalUpdated && styles.offlineFeedbackPending]}
    >
    {formattedTotalAmount}
    </Text>
    )}
    </View>
    </View>
    )}
    {!!shouldShowBreakdown && (
    <>
    {[
    {label: 'cardTransactions.outOfPocket', value: formattedOutOfPocketAmount, show: !!nonReimbursableSpend},
    {label: 'cardTransactions.companySpend', value: formattedCompanySpendAmount, show: !!nonReimbursableSpend},
    {label: 'common.billable', value: formattedBillableAmount, show: !!billableTotal},
    {label: 'common.tax', value: formattedTaxAmount, show: !!taxTotal && isTaxEnabled},
    ]
    .filter(({show}) => show)
    .map(({label, value}) => (
    <View
    key={label}
    style={[styles.flexRow, styles.pointerEventsNone, styles.containerWithSpaceBetween, styles.ph5, styles.pv1]}
    >
    <View style={[styles.flex1, styles.justifyContentCenter]}>
    <Text
    style={[styles.textLabelSupporting, hasPendingAction && styles.opacitySemiTransparent]}
    numberOfLines={1}
    >
    {translate(label as TranslationPaths)}
    </Text>
    </View>
    <View style={[styles.flexRow, styles.justifyContentCenter]}>
    <Text
    numberOfLines={1}
    style={[subAmountTextStyles, hasPendingAction && styles.opacitySemiTransparent]}
    >
    {value}
    </Text>
    </View>
    </View>
    ))}
    </>
    )}
    </>
    )}
    </View>
    {(shouldShowReportField || shouldShowBreakdown || shouldShowTotal) && renderThreadDivider}

    That works for multi-expense reports, but for a one-expense report those report-level rows repeat the same single expense information. The single-expense detection already exists in isSingleTransactionReport(), but MoneyReportView does not use it:

    function isSingleTransactionReport(report: OnyxEntry<Report>, transactions: Transaction[]) {
    if (transactions.length !== 1) {
    return false;
    }
    return transactions.at(0)?.reportID === report?.reportID;
    }

    There is also a related divider issue: shouldShowReportField can remain true based only on report type and report-field list state, even when report fields are disabled. The render block already checks policy?.areReportFieldsEnabled, but the divider gate does not, so the divider can remain visible with no row above it:

    const shouldShowReportField =
    !isClosedExpenseReportWithNoExpenses &&
    (isPaidGroupPolicyExpenseReport || isInvoiceReport) &&
    (!isCombinedReport || !isOnlyTitleFieldEnabled) &&
    !sortedPolicyReportFields.every(shouldHideSingleReportField);

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

    In MoneyReportView, import isSingleTransactionReport and compute the visible transactions consistently with the report UI. Use that to derive an isSingleExpenseReport flag.

    Then:

    • Hide the report-level Total row when isSingleExpenseReport is true.
    • Hide the entire report-level breakdown block when isSingleExpenseReport is true, including reimbursable/non-reimbursable, Billable, and Tax rows.
    • Keep the existing breakdown behavior for reports with more than one expense.
    • Use the same shouldShowTotalRow/shouldShowBreakdown values in the final renderThreadDivider condition, so removing the rows also removes the stray divider.
    • Add !!policy?.areReportFieldsEnabled to shouldShowReportField, matching the actual report-field render guard.

    For the Tax row specifically, move the useful converted-tax context to the expense-level Tax amount row in MoneyRequestView, because the report-level Tax row is removed for single-expense reports. The existing Amount row already appends converted amount details:

    if (shouldShowConvertedAmount) {
    amountDescription += ` ${CONST.DOT_SEPARATOR} ${translate('common.converted')} ${convertToDisplayString(transactionConvertedAmount, moneyRequestReport?.currency)}`;
    }

    Add a similar Tax amount description suffix using transaction.convertedTaxAmount and moneyRequestReport.currency, only when shouldShowConvertedAmount is true and the converted tax amount is non-zero. This preserves the useful converted value without keeping a redundant report-level Tax total.

    What specific scenarios should we cover in automated tests to prevent re-opening this issue?

    Add/extend UI tests for MoneyReportView:

    • A single-expense report hides Total.
    • A single billable expense hides the report-level Billable row.
    • A single taxed expense hides the report-level Tax row.
    • Multi-expense reports still show Total, Billable, and Tax rows when applicable.
    • Disabling report fields hides both report fields and the divider when no other report-level rows are present.

    Add/extend UI tests for MoneyRequestView:

    • A foreign-currency taxed expense appends the converted tax amount to the Tax amount description.
    • A zero converted tax amount does not render a redundant converted suffix.
    • Matching expense/report currencies do not render a converted tax suffix.

    What alternative solutions did you explore? (Optional)

    We could only hide Billable and Tax from the report-level breakdown while leaving Total, but the design discussion clarified that the goal is to remove all items above Amount in the single-expense view. Gating the whole report-level block for single-expense reports is also less brittle than special-casing each row independently.

  12. 14 remaining items

  13. melvin-bot commented on Jun 16, 2026

    @melvin-bot

    @hoangzinh Whoops! This issue is 2 days overdue. Let's get this updated quick!

  14. melvin-bot commented on Jun 18, 2026

    @melvin-bot

    @hoangzinh Huh... This is 4 days overdue. Who can take care of this?

  15. melvin-bot commented on Jun 22, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  16. melvin-bot commented on Jun 22, 2026

    @melvin-bot

    Payment Summary

    Upwork Job

    BugZero Checklist (@mallenexpensify)

    • I have verified the correct assignees and roles are listed above and updated the necessary manual offers
    • I have verified that there are no duplicate or incorrect contracts on Upwork for this job (https://www.upwork.com/ab/applicants/2064021249131997949/hired)
    • I have verified the PR was not reverted
    • I have applied any discounts due to bugs/regressions introduced by this PR
    • I have paid out the Upwork contracts or cancelled the ones that are incorrect
    • I have verified the payment summary above is correct
  17. hoangzinh commented on Jun 23, 2026

    @hoangzinh
    Contributor

    BugZero Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      No offending PR. It's a feature request

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: N/A

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    • [BugZero Assignee] Create a GH issue for creating/updating the regression test once above steps have been agreed upon.

      Link to issue: https://github.com/Expensify/Expensify/issues/652653

    Regression Test Proposal

    Test:

    1. Go to WS settings
    2. Enable Tax and Rules
    3. Go to Rules -> Billable-default
    4. Set to Billable
    5. Go to WS chat
    6. Create a non-imbursable expense with non-default currency (make sure it's billable as well)
    7. Verify there is no breakdown section above the Amount row in the single expense report

    Do we agree 👍 or 👎

    Zapier Logs Run ID: 00040eee-6a58-a110-46d3-7cdaad2e9b54
  18. mallenexpensify commented on Jun 25, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @mukhrr paid $125 via Upwork
    Contributor+: @hoangzinh due $125 via NewDot

    ^ Test case created. Thx

  19. laurenreidexpensify commented on Jun 29, 2026

    @laurenreidexpensify
    Contributor

    $125 approved @hoangzinh

  20. flaviadefaria commented on Jun 30, 2026

    @flaviadefaria
    Contributor

    Approved: @mukhrr paid $125 via Upwork

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

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionDailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions