Skip to content

[Due for payment 2026-11-03] [$175] Active users count are different on payment history and payment details page #104962

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.5.20-0
Reproducible in staging?: Yes
Reproducible in production?: N/A - new feature, doesn't exist in prod
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #104325
Email or phone of affected tester (no customers): emady.syed1234+400@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: windows 11 / Chrome
App Component: Other

Action Performed:

Precondition: An account with subscription payment history and payment history beta is enabled

Steps:

  1. Click Account > Subscription > View payment history
  2. Click any item

Expected Result:

The activity list with the active user count is shown

Actual Result:

Active users count are different on payment history and payment details page

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7295460_1792851050534.Recording__1265.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Julia Ponikarchuk
Bug ID: 7295460

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

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 26, 2026
  2. applause-bot commented on Oct 26, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/cf0b86292a4014c5c4c9e44908192043b50b5de2830f2e4f99e050af13688309

  3. MelvinBot commented on Oct 26, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The two pages count active users from different data.

    Nothing ties these two sources together. When the billed total and the activeUsers list differ, the pages show different numbers. [CFI] Payment history: Activity list with the active user count added the details-page count.

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

    Use one count on both pages: the billed count, because that is what the user paid for.

    1. Add an activeUserCount prop to PaymentActivityTable. Use it for the header label instead of activeUsers.length.
    2. In PaymentDetailsPage, pass row?.activeUserCount ?? activeUsers.length. row already comes from getPaymentHistoryRow, which uses the same helper as the history table.
    3. Update tests/ui/PaymentDetailsPageTest.tsx so that a purchase whose totalUniqueMembersCount differs from activeUsers.length shows the same count on both pages.

    After this change, the header count can differ from the number of rows listed. If the rows also need to match, the backend's activeUsers list must align with the billed totals. That is a backend change outside the App repo, so an internal engineer should handle it.

    What alternative solutions did you explore? (Optional)

    Use activeUsers.length on both pages. This does not work: the history page never loads activeUsers, because only OpenPaymentDetailsPage returns it.

    Confidence and related issues

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


    view run

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 26, 2026
  5. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @gijoe0295 (External)

  6. changed the title [-]Active users count are different on payment history and payment details page[/-] [+][$175] Active users count are different on payment history and payment details page[/+] on Oct 26, 2026
  7. melvin-bot commented on Oct 26, 2026

    @melvin-bot
  8. camesenin commented on Oct 26, 2026

    @camesenin

    Proposal

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

    Opening a subscription payment shows an active-user count that differs from its payment-history entry. Users cannot reliably reconcile the summary with the payment’s activity list.

    Audit verdict: MelvinBot’s proposal needs refinement before acceptance. Matching two labels does not establish that either value is correct.

    What is the root cause of that problem?

    The supplied proposal identifies two counting paths:

    • src/libs/PaymentHistoryUtils.ts:186–204: getActiveUserCount derives a count from billing totals or policy actor emails.
    • src/pages/settings/Subscription/PaymentHistoryPage/PaymentHistoryTable.tsx:82: displays that derived count.
    • src/libs/PaymentHistoryUtils.ts:207–209: getActiveUserAccountIDs deduplicates purchase.activeUsers.
    • src/components/Tables/PaymentActivityTable/index.tsx:42–43: displays the resulting list length.

    These locations are references supplied by MelvinBot, not independently verified source locations. The pinned files could not be retrieved, and this workspace contains no Expensify checkout.

    The supported diagnosis is inconsistent count derivation. The underlying cause remains unproven: there is no affected purchase payload establishing whether the billing total, account list, fallback calculation, or loading state is wrong.

    The supplied Jest fixture demonstrates intended account-ID deduplication, but contains no conflicting billing total. It therefore does not reproduce this issue.

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

    First capture the history response, details response, and resulting Onyx purchase for the same purchase ID, before and after details loading. Record the displayed counts and confirm the intended meaning of “active users.”

    The smallest correct patch depends on that evidence:

    • If the billing total is authoritative and the activity list is intentionally partial, Melvin’s proposed count prop is reasonable.
    • If the activity list must enumerate every counted user, changing its header alone leaves the defect unresolved.
    • If the count changes because details loading overwrites purchase fields, fix that mutation rather than adding a display override.

    An exact production TypeScript diff is not justified by the available evidence. The component declarations, billing-field types, helper fallback behavior, and Onyx update path are missing. Inventing those details would violate the requirement for verified code.

    What alternative solutions did you explore? (Optional)

    Melvin’s approach has these unresolved acceptance risks:

    1. Unsupported authority assumption. “That is what the user paid for” requires confirmation of the billing fields’ semantics.
    2. Acknowledged count/list mismatch. The proposal explicitly permits a header count different from the listed membership. That needs product-contract confirmation.
    3. Incomplete fallback reasoning. row?.activeUserCount ?? activeUsers.length falls back only for null or undefined. A derived zero remains zero even when the account list is populated. Whether that matters depends on the unverified helper implementation.
    4. Missing lifecycle coverage. The proposed test does not establish behavior during details loading, subsequent Onyx updates, or navigation between purchases.
    5. Premature backend attribution. A discrepancy alone does not establish that the backend requires modification.

    These are unresolved risks, not reproduced regressions. Melvin’s proposal already covers reusing the billed count; adding guards or tests would refine that proposal rather than establish a materially different solution. Expensify requires meaningful differences between competing proposals. Contributing guidelines

    What automated tests can you add to prevent this from re-occurring?

    Add a regression to tests/ui/PaymentDetailsPageTest.tsx using a sanitized payload from the reproduced discrepancy. Render both pages for the same purchase and assert the contractually correct count and actual listed identities, before and after the details update.

    Cover these cases:

    Case Required assertion
    Conflicting total and account list Both pages follow the confirmed count contract; membership expectations are explicit
    Duplicate account IDs Each account appears once
    Explicit zero versus missing total Zero is preserved; missing data follows the documented fallback
    Details response updates Onyx Count remains correct after the update
    Navigate to another purchase Neither count nor membership leaks from the previous purchase
    Search filters rows Total retains the intended meaning

    This exact additional test fits the supplied helper and checks a relevant mutation path:

    it('removes a duplicate when purchase membership updates', async () => {
        await setPurchase([BOB.accountID, TODD.accountID]);
        renderPage();
    
        expect(await screen.findByText('2 active users')).toBeOnTheScreen();
        expect(screen.getByText(TODD.displayName)).toBeOnTheScreen();
    
        await setPurchase([BOB.accountID, BOB.accountID]);
    
        await waitFor(() => {
            expect(screen.queryByText(TODD.displayName)).toBeNull();
            expect(screen.queryByText('2 active users')).toBeNull();
        });
        expect(screen.getAllByText(BOB.displayName)).toHaveLength(1);
    });

    Validation status: This test is proposed, not executed. No staging reproduction, network payload, verified production diff, or passing Jest result is available; claiming a production-ready fix would exceed the evidence.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~01b60d704ba377be83

  9. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. Enes2553 commented on Oct 26, 2026

    @Enes2553

    Contributor details
    Your Expensify account email: enesbilalolgun0@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~010415b62c29eb990f

    Proposal

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

    The active users count shown on the Payment History list differs from the active users count shown on the Payment Details page for the exact same transaction.

    What is the root cause of that problem?

    The two pages derive their active user count from two independent data sources:

    1. In src/pages/settings/Subscription/PaymentHistoryPage/PaymentHistoryTable.tsx (line 82), the list displays row.activeUserCount. In src/libs/PaymentHistoryUtils.ts (lines 185-204), getActiveUserCount() reads the official billed metric (purchase.message.totalUniqueMembersCount or totalActorCount).
    2. In src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx and src/components/Tables/PaymentActivityTable/index.tsx (line 42):
    const activeUsersLabel = translate('subscription.paymentHistory.activeUsers', {count: activeUsers.length});

    The details page derives the count purely from activeUsers.length (which comes from getActiveUserAccountIDs(purchase), deduping purchase.activeUsers returned by OpenPaymentDetailsPage).

    When the billed total (totalUniqueMembersCount) and the returned purchase.activeUsers array length diverge, the two pages display conflicting counts.

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

    The authoritative metric representing what the user was billed for is row.activeUserCount. We align both screens to use this single source of truth:

    1. In src/components/Tables/PaymentActivityTable/index.tsx, accept an optional activeUserCount?: number prop:
    type PaymentActivityTableProps = {
        activeUsers: PaymentActivityRowData[];
        activeUserCount?: number;
        headerComponent?: React.ReactElement;
    };
    
    function PaymentActivityTable({activeUsers, activeUserCount, headerComponent}: PaymentActivityTableProps) {
        const {translate, localeCompare} = useLocalize();
    
        const count = activeUserCount ?? activeUsers.length;
        const activeUsersLabel = translate('subscription.paymentHistory.activeUsers', {count});
    1. In src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx, pass row?.activeUserCount into PaymentActivityTable:
    <PaymentActivityTable
        activeUsers={activeUsers}
        activeUserCount={row?.activeUserCount}
        headerComponent={detailsContent}
    />

    Why this is the cleanest solution:

    • It establishes a single consistent count across both views without introducing breaking changes or requiring complex schema refactoring.
    • If row.activeUserCount is unavailable, it gracefully falls back to activeUsers.length.

    What alternative solutions did you explore? (Optional)

    • Using activeUsers.length on Payment History page: This was rejected because PaymentHistoryPage does not fetch individual activeUsers payloads across all historical billing periods; forcing it would add unnecessary network overhead.

    What automated tests can you add to prevent this from re-occurring?

    Add unit tests in tests/unit/PaymentActivityTableTest.tsx:

    • Render PaymentActivityTable with activeUsers containing 3 items and activeUserCount={5}.
    • Verify that the header renders "5 active users", matching the billed history row.
    • Verify fallback to activeUsers.length when activeUserCount is undefined.
  11. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  12. shilo1979jk commented on Oct 26, 2026

    @shilo1979jk

    Proposal

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

    On Settings > Subscription > View payment history, the Active users column for a billing row shows one count (e.g. 4), while opening that same row's Payment details page displays a different count in the table header (e.g. Active users (1)).

    What is the root cause of that problem?

    The two screens compute the active users count for the same purchase from different sources:

    1. On the Payment History table (src/components/Tables/PaymentHistoryTable.tsx:82), the row displays row.activeUserCount, which getPaymentHistoryRows (src/libs/PaymentHistoryUtils.ts:276) computes via getActiveUserCount(purchase) (src/libs/PaymentHistoryUtils.ts:186-204: purchase.message.totalUniqueMembersCount ?? purchase.message.totalActorCount ?? unique(billablePolicies[*].actorList)).
    2. On the Payment Details page (src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx:90 -> src/components/Tables/PaymentActivityTable/index.tsx:42), the header renders translate('subscription.paymentHistory.activeUsers', {count: activeUsers.length}), where activeUsers comes from getActiveUserAccountIDs(purchase) (purchase.activeUsers deduplicated, src/libs/PaymentHistoryUtils.ts:206-209).

    When purchase.activeUsers contains fewer entries than getActiveUserCount(purchase) (or when getActiveUserCount falls back to totalActorCount / billablePolicies while purchase.activeUsers only lists unique resolved accounts), the two counts disagree for the exact same purchaseID.

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

    Align the active user count across both views:

    1. In src/libs/PaymentHistoryUtils.ts (getActiveUserCount, lines 186-204), if purchase.activeUsers is populated (Array.isArray(purchase.activeUsers) && purchase.activeUsers.length > 0), use new Set(purchase.activeUsers).size so the summary table matches the exact deduplicated activeUsers list shown on the details page, falling back to totalUniqueMembersCount ?? totalActorCount ?? billablePolicies when purchase.activeUsers has not been loaded yet.
    2. In src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx and src/components/Tables/PaymentActivityTable/index.tsx, pass activeUserCount={row?.activeUserCount} and use activeUserCount ?? activeUsers.length when activeUsers is still loading (isLoading).

    What alternative solutions did you explore? (Optional)

    Passing row.activeUserCount directly into PaymentActivityTable's header without updating getActiveUserCount was considered, but if row.activeUserCount is 4 while the table below only renders 1 row from purchase.activeUsers, the header "Active users (4)" above a 1-row table would still look inconsistent once purchase.activeUsers is loaded. Preferring new Set(purchase.activeUsers).size when available and falling back to row.activeUserCount while loading keeps both the summary row and the details header consistent with the rendered user rows.

  13. dukenv0307 commented on Oct 26, 2026

    @dukenv0307
    Contributor

    I'll take it

  14. melvin-bot commented on Oct 26, 2026

    @melvin-bot

    Triggered auto assignment to @rlinoz, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

  15. changed the title [-][$175] Active users count are different on payment history and payment details page[/-] [+][Due for payment 2026-11-03] [$175] Active users count are different on payment history and payment details page[/+] on Oct 27, 2026
  16. melvin-bot commented on Oct 27, 2026

    @melvin-bot

    @nkdengineer

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.23-3 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-11-03. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions