You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-11-03] [$175] Active users count are different on payment history and payment details page #104962
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:
Click Account > Subscription > View payment history
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
The two pages count active users from different data.
Payment history shows row.activeUserCount. It comes from getActiveUserCount, which reads the billing totals: totalUniqueMembersCount, then totalActorCount, then the unique emails in billablePolicies[*].actorList. See PaymentHistoryUtils.ts:186-204 and PaymentHistoryTable.tsx:82.
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.
Add an activeUserCount prop to PaymentActivityTable. Use it for the header label instead of activeUsers.length.
In PaymentDetailsPage, pass row?.activeUserCount ?? activeUsers.length. row already comes from getPaymentHistoryRow, which uses the same helper as the history table.
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
Confidence: medium. The code clearly uses two different sources. I did not reproduce the bug, because the test account has no payment history. I also could not check which backend value is wrong for this tester's account.
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.
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
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/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:
Unsupported authority assumption. “That is what the user paid for” requires confirmation of the billing fields’ semantics.
Acknowledged count/list mismatch. The proposal explicitly permits a header count different from the listed membership. That needs product-contract confirmation.
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.
Missing lifecycle coverage. The proposed test does not establish behavior during details loading, subsequent Onyx updates, or navigation between purchases.
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()=>{awaitsetPurchase([BOB.accountID,TODD.accountID]);renderPage();expect(awaitscreen.findByText('2 active users')).toBeOnTheScreen();expect(screen.getByText(TODD.displayName)).toBeOnTheScreen();awaitsetPurchase([BOB.accountID,BOB.accountID]);awaitwaitFor(()=>{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.
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:
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).
In src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx and src/components/Tables/PaymentActivityTable/index.tsx (line 42):
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:
In src/components/Tables/PaymentActivityTable/index.tsx, accept an optional activeUserCount?: number prop:
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.
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:
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)).
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:
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.
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.
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
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]
Activity
applause-bot commentedon Oct 26, 2026
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
MelvinBot commentedon Oct 26, 2026
Proposal
What is the root cause of that problem?
The two pages count active users from different data.
row.activeUserCount. It comes fromgetActiveUserCount, which reads the billing totals:totalUniqueMembersCount, thentotalActorCount, then the unique emails inbillablePolicies[*].actorList. See PaymentHistoryUtils.ts:186-204 and PaymentHistoryTable.tsx:82.activeUsers.length. It comes fromgetActiveUserAccountIDs, which dedupes thepurchase.activeUsersaccount IDs returned byOpenPaymentDetailsPage. See PaymentHistoryUtils.ts:207-209 and PaymentActivityTable/index.tsx:42-43.Nothing ties these two sources together. When the billed total and the
activeUserslist 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.
activeUserCountprop toPaymentActivityTable. Use it for the header label instead ofactiveUsers.length.PaymentDetailsPage, passrow?.activeUserCount ?? activeUsers.length.rowalready comes fromgetPaymentHistoryRow, which uses the same helper as the history table.tests/ui/PaymentDetailsPageTest.tsxso that a purchase whosetotalUniqueMembersCountdiffers fromactiveUsers.lengthshows 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
activeUserslist 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.lengthon both pages. This does not work: the history page never loadsactiveUsers, because onlyOpenPaymentDetailsPagereturns 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
melvin-bot commentedon Oct 26, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @gijoe0295 (
External)[-]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[/+]melvin-bot commentedon Oct 26, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022114588323058468949
camesenin commentedon Oct 26, 2026
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:getActiveUserCountderives 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:getActiveUserAccountIDsdeduplicatespurchase.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:
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:
row?.activeUserCount ?? activeUsers.lengthfalls back only fornullorundefined. A derived zero remains zero even when the account list is populated. Whether that matters depends on the unverified helper implementation.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.tsxusing 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:
This exact additional test fits the supplied helper and checks a relevant mutation path:
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
melvin-bot commentedon Oct 26, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
Enes2553 commentedon Oct 26, 2026
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:
src/pages/settings/Subscription/PaymentHistoryPage/PaymentHistoryTable.tsx(line 82), the list displaysrow.activeUserCount. Insrc/libs/PaymentHistoryUtils.ts(lines 185-204),getActiveUserCount()reads the official billed metric (purchase.message.totalUniqueMembersCountortotalActorCount).src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsxandsrc/components/Tables/PaymentActivityTable/index.tsx(line 42):The details page derives the count purely from
activeUsers.length(which comes fromgetActiveUserAccountIDs(purchase), dedupingpurchase.activeUsersreturned byOpenPaymentDetailsPage).When the billed total (
totalUniqueMembersCount) and the returnedpurchase.activeUsersarray 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:src/components/Tables/PaymentActivityTable/index.tsx, accept an optionalactiveUserCount?: numberprop:src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx, passrow?.activeUserCountintoPaymentActivityTable:Why this is the cleanest solution:
row.activeUserCountis unavailable, it gracefully falls back toactiveUsers.length.What alternative solutions did you explore? (Optional)
activeUsers.lengthon Payment History page: This was rejected becausePaymentHistoryPagedoes not fetch individualactiveUserspayloads 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:PaymentActivityTablewithactiveUserscontaining 3 items andactiveUserCount={5}.activeUsers.lengthwhenactiveUserCountis undefined.melvin-bot commentedon Oct 26, 2026
✅ Contributor details stored successfully. Thank you for contributing to Expensify!
shilo1979jk commentedon Oct 26, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
On
Settings > Subscription > View payment history, theActive userscolumn for a billing row shows one count (e.g.4), while opening that same row'sPayment detailspage 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
purchasefrom different sources:src/components/Tables/PaymentHistoryTable.tsx:82), the row displaysrow.activeUserCount, whichgetPaymentHistoryRows(src/libs/PaymentHistoryUtils.ts:276) computes viagetActiveUserCount(purchase)(src/libs/PaymentHistoryUtils.ts:186-204:purchase.message.totalUniqueMembersCount ?? purchase.message.totalActorCount ?? unique(billablePolicies[*].actorList)).src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx:90->src/components/Tables/PaymentActivityTable/index.tsx:42), the header renderstranslate('subscription.paymentHistory.activeUsers', {count: activeUsers.length}), whereactiveUserscomes fromgetActiveUserAccountIDs(purchase)(purchase.activeUsersdeduplicated,src/libs/PaymentHistoryUtils.ts:206-209).When
purchase.activeUserscontains fewer entries thangetActiveUserCount(purchase)(or whengetActiveUserCountfalls back tototalActorCount/billablePolicieswhilepurchase.activeUsersonly lists unique resolved accounts), the two counts disagree for the exact samepurchaseID.What changes do you think we should make in order to solve the problem?
Align the active user count across both views:
src/libs/PaymentHistoryUtils.ts(getActiveUserCount, lines186-204), ifpurchase.activeUsersis populated (Array.isArray(purchase.activeUsers) && purchase.activeUsers.length > 0), usenew Set(purchase.activeUsers).sizeso the summary table matches the exact deduplicatedactiveUserslist shown on the details page, falling back tototalUniqueMembersCount ?? totalActorCount ?? billablePolicieswhenpurchase.activeUsershas not been loaded yet.src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsxandsrc/components/Tables/PaymentActivityTable/index.tsx, passactiveUserCount={row?.activeUserCount}and useactiveUserCount ?? activeUsers.lengthwhenactiveUsersis still loading (isLoading).What alternative solutions did you explore? (Optional)
Passing
row.activeUserCountdirectly intoPaymentActivityTable's header without updatinggetActiveUserCountwas considered, but ifrow.activeUserCountis4while the table below only renders1row frompurchase.activeUsers, the header"Active users (4)"above a 1-row table would still look inconsistent oncepurchase.activeUsersis loaded. Preferringnew Set(purchase.activeUsers).sizewhen available and falling back torow.activeUserCountwhile loading keeps both the summary row and the details header consistent with the rendered user rows.dukenv0307 commentedon Oct 26, 2026
I'll take it
melvin-bot commentedon Oct 26, 2026
Triggered auto assignment to @rlinoz, see https://stackoverflow.com/c/expensify/questions/7972 for more details.
[-][$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[/+]melvin-bot commentedon Oct 27, 2026
@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]