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-08-07] [$250] "Potential duplicate" shown on second expense online after removing first offline #94533
Version Number: 9.4.19-1 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:#91196 Email or phone of affected tester (no customers): N/A Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: MacBook Air 26.5 Chrome, iPhone 15 iOS 26.5 App Component: Money Requests
Action Performed:
Sign in to ND with a new account, selecting "Manage" purpose
Create two expenses with the same amount, merchant, category, and date
Navigate to Spend/ Expenses
Click on the first expense, click on the Report field, select Remove from report - violations disappear
Repeat step 4, select the existing report - violations are displayed
Go offline, remove the first expense from the report again - violations disappear
Go online
Expected Result:
"Potential duplicate" violation is not displayed on the second expense, as the first one was removed from the report
Actual Result:
"Potential duplicate" violation appears on the second expense online, when the first one was removed from the report offline
When you remove an expense from a report, the "remove from report" action routes through changeTransactionsReport with the destination set to the unreported self-DM, so the isUnreported branch runs. That branch optimistically strips the DUPLICATED_TRANSACTION violation from each sibling duplicate — but it only pushes that change to optimisticData, never to successData. See Transaction.ts:1148-1161.
Because there is no successData re-apply for the siblings, when the app comes back online a queued/concurrent OpenReport (or the command's own server response) re-pushes the stale one-sided duplicate violation onto the remaining expense, and nothing re-asserts the cleaned value — so "Potential duplicate" reappears.
This is a known race: the dedicated helper removeTransactionFromDuplicateTransactionViolation (used by delete/edit flows) handles it correctly by also pushing the cleaned value to successData, with an inline comment describing this exact failure mode at TransactionUtils/index.ts:2429-2436. The changeTransactionsReport path hand-rolls its own sibling clearing and omits that step.
Why it only repros offline → online (and not in the plain online flow)
Online, the optimistic clear and the server response land almost together, so the visible flicker is brief or absent. Offline, the optimistic clear persists while the request sits queued; on reconnect the CHANGE_TRANSACTIONS_REPORT request flushes and a sibling OpenReport response (which still carries the pre-move violation) overwrites the unprotected optimistic clear. The moved transaction itself is unaffected because it does get matching successData/failureData (Transaction.ts:1162-1178) — only the siblings are left unprotected.
Note: the App does not compute duplicates itself; the set comes from the backend violation.data.duplicates list. This proposal assumes the server already clears the sibling's duplicate violation after the move and the only gap is the client overwrite. If production logs show the backend still re-emits DUPLICATED_TRANSACTION on the surviving expense post-move, a backend fix would also be needed.
What changes do you think we should make in order to solve the problem?
In the isUnreported branch of changeTransactionsReport, give the sibling violation updates the same lifecycle protection the moved transaction already has: for each sibling, push the cleaned violations value to successData (so it survives the online sync) and push the original violations to failureData (so it restores on error). The cleanest approach is to reuse the existing removeTransactionFromDuplicateTransactionViolation helper for the siblings instead of the inline optimisticData-only loop, since it already implements the correct three-way optimistic/success/failure pattern and trims data.duplicates precisely.
What alternative solutions did you explore? (Optional)
Adding only successData without failureData would fix the reported symptom but would leave siblings inconsistent if the move fails offline — reusing the helper covers both.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/94533) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
When a transaction is moved off a report (isUnreported), changeTransactionsReport clears the duplicate violation on the other member of the duplicate pair (the "sibling"). But that sibling clear is pushed to optimisticData only — unlike the moved transaction itself, which also gets matching successData and failureData:
Online (step 4) this is fine: the optimistic clear is never disturbed, and the server processes the removal immediately. Offline → online is different. While the write sits in the persisted queue, reconnecting triggers a fresh data pull from the server, where the sibling still carries the DUPLICATED_TRANSACTION violation (the queued removal hasn't run yet). That pull re-merges the stale violation onto the sibling, clobbering the optimistic-only clear. Because there is no successData entry for the sibling, nothing re-clears it once the queued request finally resolves — so the "Potential duplicate" violation reappears on the second expense.
In short: sibling violation clear is optimistic-only → offline reconnect refetch reapplies the server's stale duplicate violation → no success-data clear to override it → violation comes back.
What changes do you think we should make in order to solve the problem?
Make the sibling clear authoritative, the same way the moved transaction's clear already is. Inside the sibling loop here:
in addition to the optimisticData push, also push a matching successData entry that sets the sibling's ${TRANSACTION_VIOLATIONS}${id} to siblingViolations with the DUPLICATED_TRANSACTION violation filtered out, and a failureData entry restoring siblingViolations so an error rolls the sibling back. This mirrors what the moved transaction already does and guarantees the clear is reapplied after the request resolves, surviving any reconnect refetch.
What alternative solutions did you explore? (Optional)
Keep only optimistic data but rebuild the sibling's failureData/restore from allTransactionViolation — but without successData the clear is still not reasserted after a reconnect refetch, so this would not fix the offline case. Mirroring success + failure for the sibling is the minimal correct change.
Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
Please re-state the problem that we are trying to solve in this issue.
When a user removes the first of two duplicate expenses from a report while offline and then comes back online, the "Potential duplicate" violation reappears on the second expense even though the first expense was successfully removed from the report. The expected behavior is that the violation on the second expense should stay cleared once the first expense is removed.
What is the root cause of that problem?
In src/libs/actions/Transaction.ts, inside changeTransactionsReport, the block at lines 1148–1178 optimistically clears the DUPLICATED_TRANSACTION violation from sibling transactions (lines 1152–1160) when moving a transaction to the unreported state (isUnreported). However, the corresponding failureData entries (lines 1174–1178) only restore the removed transaction's own violations — they do not restore the sibling transactions' violations that were cleared in the optimistic update.
The same gap exists for successData: when the API call eventually succeeds, there is no successData entry for the sibling transactions either. This means that if a queued command (such as OpenReport resolving between the optimistic apply and the server response) re-introduces a one-sided duplicate violation on the sibling, there is nothing to re-apply the cleaned state on success. The server then re-derives duplicate violations from scratch and re-attaches them to the sibling, overwriting the now-stale optimistic clear.
The fix lives in the same isUnreported block: both the successData and failureData arrays must include a SET entry for each siblingID, mirroring the optimistic clear pushed at lines 1155–1159.
What changes do you think we should make in order to solve the problem?
src/libs/actions/Transaction.ts (lines 1152–1160) — After pushing the optimistic clear for each sibling, also push matching successData and failureData entries for that sibling:
// Inside the isUnreported block, after the optimisticData push for each sibling:successData.push({onyxMethod: Onyx.METHOD.SET,key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${siblingID}`,value: siblingViolationsWithoutDuplicate,// the same cleaned value used in optimisticData});failureData.push({onyxMethod: Onyx.METHOD.SET,key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${siblingID}`,value: siblingViolations,// the original violations before the optimistic clear});
The successData entry ensures that even if a stale server push re-introduces a one-sided violation between the optimistic apply and the API response, the final committed state reflects the cleared violation. The failureData entry restores the sibling's original violations if the API call fails.
src/libs/actions/Transaction.ts (lines 1168–1172) — Similarly confirm that any existing successData entry for the removed transaction's own violations is also present; the sibling entries above must be added alongside it, not replace it.
What alternative solutions did you explore? (Optional)
Calling removeTransactionFromDuplicateTransactionViolation (used in DeleteMoneyRequest.ts:738) inside changeTransactionsReport for the isUnreported path would also clear sibling violations symmetrically, but adding successData and failureData entries in the existing inline block is a smaller, more targeted change that avoids introducing a dependency between two separate action modules.
changed the title [-]"Potential duplicate" shown on second expense online after removing first offline[/-][+][$250] "Potential duplicate" shown on second expense online after removing first offline[/+]on Jun 25, 2026
Please re-state the problem that we are trying to solve in this issue.
When two expenses are duplicates of each other (both carry the duplicatedTransaction violation referencing the other) and the user removes the first expense from its report while offline, the "Potential duplicate" violation disappears optimistically on the second expense as expected. But the moment the device goes back online, the violation reappears on the second expense even though its only duplicate is gone. The optimistic clear is not durable across the online reconciliation.
What is the root cause of that problem?
The "Remove from report" action calls removeFromReport in useReportSelectionActions, which invokes changeTransactionsReport with no newReport, so reportID resolves to CONST.REPORT.UNREPORTED_REPORT_ID and isUnreported is true. Inside that branch, the action tries to clear the duplicate violation not only on the moved transaction but also on its siblings (the other transactions that listed the moved one in their data.duplicates). The moved transaction (A) gets its violations wiped across optimisticData, successData, and failureData, but the sibling clear (transaction B) is only written to optimisticData — there is no successData and no failureData for the sibling:
Because the sibling clear lives only in optimisticData, it is fragile. While offline, the request sits in the queue and B looks fine. When the device reconnects, the queued CHANGE_TRANSACTIONS_REPORT is flushed alongside other queued/refreshing commands (an OpenReport/Search refresh resolving between the optimistic apply and the server response). Those responses re-push B's TRANSACTION_VIOLATIONS entry from the server — which still contains the stale one-sided duplicatedTransaction violation, since the CHANGE_TRANSACTIONS_REPORT parameters never tell the backend which siblings to re-evaluate (the params only carry transactionList, reportID, and the report-action/thread map):
With no successData to re-apply the cleaned violation list after the server response lands, B's violation is resurrected. This is exactly the failure mode the codebase already documented and solved elsewhere: the shared helper removeTransactionFromDuplicateTransactionViolation in TransactionUtils writes the cleaned sibling list to optimisticData, successData, AND failureData, with a comment stating the successData exists precisely "so that stale data from a queued command (e.g. OpenReport resolving between optimistic apply and the server response) doesn't re-introduce one-sided duplicate violations." The inline loop in changeTransactionsReport is a hand-rolled version of that helper that simply forgot the successData (and failureData) leg.
What changes do you think we should make in order to solve the problem?
In the sibling-clear loop of changeTransactionsReport, mirror the established pattern from removeTransactionFromDuplicateTransactionViolation: push the same cleaned sibling value to successData (so the cleared state survives the online reconciliation) and to failureData (restoring the original siblingViolations if the move fails). The cleaned value is already computed for the optimistic write, so we just reuse it:
This is safe and narrowly scoped: it only affects transactions that already appeared in the moved transaction's data.duplicates, and only the duplicatedTransaction violation is stripped (all other sibling violations are preserved by the existing filter). Adding failureData actually closes a second latent bug — today, if the move fails, the sibling's violation is never restored. Ideally we would reuse removeTransactionFromDuplicateTransactionViolation directly since it also reduces (rather than fully strips) the sibling list when the sibling has more than one duplicate, but that helper needs the full OnyxCollection<Transaction> for getValidDuplicateTransactionIDs, which changeTransactionsReport is not currently passed; matching the helper's three-leg write is the minimal correct fix without widening the function signature.
What alternative solutions did you explore? (Optional)
Threading the full transaction collection into changeTransactionsReport so it can delegate entirely to removeTransactionFromDuplicateTransactionViolation (gaining its multi-duplicate "reduce instead of strip" handling), but that expands the prop signature and every caller for a scenario the inline loop already covers — adding the missing successData/failureData legs fixes the reported bug with far less surface area.
Contributor: @x-dev90 paid $250 via Upwork
Contributor+: @nyomanjyotisa due $250 via NewDot
@x-dev90 can you please accept the job below? Please reply here and tag me once you have.
If you're not sent an offer in Upwork when you're assigned to an issue, please tag me. If an Upwork job exists, please submit a proposal. https://www.upwork.com/jobs/~022086977015188086070
@nyomanjyotisa yes, if you haven't been paid, request payment now.
In the future, try to request payment in the same month as the job was finished (or as soon after as possible)
Activity
applause-bot commentedon Jun 25, 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/2083cbb1b26b22f7c1f588073aa7bf6c15b495613041b1dca7f7854e89205989
MelvinBot commentedon Jun 25, 2026
Proposal
What is the root cause of that problem?
When you remove an expense from a report, the "remove from report" action routes through
changeTransactionsReportwith the destination set to the unreported self-DM, so theisUnreportedbranch runs. That branch optimistically strips theDUPLICATED_TRANSACTIONviolation from each sibling duplicate — but it only pushes that change tooptimisticData, never tosuccessData. SeeTransaction.ts:1148-1161.Because there is no
successDatare-apply for the siblings, when the app comes back online a queued/concurrentOpenReport(or the command's own server response) re-pushes the stale one-sided duplicate violation onto the remaining expense, and nothing re-asserts the cleaned value — so "Potential duplicate" reappears.This is a known race: the dedicated helper
removeTransactionFromDuplicateTransactionViolation(used by delete/edit flows) handles it correctly by also pushing the cleaned value tosuccessData, with an inline comment describing this exact failure mode atTransactionUtils/index.ts:2429-2436. ThechangeTransactionsReportpath hand-rolls its own sibling clearing and omits that step.Why it only repros offline → online (and not in the plain online flow)
Online, the optimistic clear and the server response land almost together, so the visible flicker is brief or absent. Offline, the optimistic clear persists while the request sits queued; on reconnect the
CHANGE_TRANSACTIONS_REPORTrequest flushes and a siblingOpenReportresponse (which still carries the pre-move violation) overwrites the unprotected optimistic clear. The moved transaction itself is unaffected because it does get matchingsuccessData/failureData(Transaction.ts:1162-1178) — only the siblings are left unprotected.Note: the App does not compute duplicates itself; the set comes from the backend
violation.data.duplicateslist. This proposal assumes the server already clears the sibling's duplicate violation after the move and the only gap is the client overwrite. If production logs show the backend still re-emitsDUPLICATED_TRANSACTIONon the surviving expense post-move, a backend fix would also be needed.What changes do you think we should make in order to solve the problem?
In the
isUnreportedbranch ofchangeTransactionsReport, give the sibling violation updates the same lifecycle protection the moved transaction already has: for each sibling, push the cleaned violations value tosuccessData(so it survives the online sync) and push the original violations tofailureData(so it restores on error). The cleanest approach is to reuse the existingremoveTransactionFromDuplicateTransactionViolationhelper for the siblings instead of the inlineoptimisticData-only loop, since it already implements the correct three-wayoptimistic/success/failurepattern and trimsdata.duplicatesprecisely.What alternative solutions did you explore? (Optional)
Adding only
successDatawithoutfailureDatawould fix the reported symptom but would leave siblings inconsistent if the move fails offline — reusing the helper covers both.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/94533)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
lorretheboy commentedon Jun 25, 2026
Proposal
What is the root cause of that problem?
When a transaction is moved off a report (
isUnreported),changeTransactionsReportclears the duplicate violation on the other member of the duplicate pair (the "sibling"). But that sibling clear is pushed tooptimisticDataonly — unlike the moved transaction itself, which also gets matchingsuccessDataandfailureData:App/src/libs/actions/Transaction.ts
Lines 1148 to 1179 in 75f7b14
Online (step 4) this is fine: the optimistic clear is never disturbed, and the server processes the removal immediately. Offline → online is different. While the write sits in the persisted queue, reconnecting triggers a fresh data pull from the server, where the sibling still carries the
DUPLICATED_TRANSACTIONviolation (the queued removal hasn't run yet). That pull re-merges the stale violation onto the sibling, clobbering the optimistic-only clear. Because there is nosuccessDataentry for the sibling, nothing re-clears it once the queued request finally resolves — so the "Potential duplicate" violation reappears on the second expense.In short: sibling violation clear is optimistic-only → offline reconnect refetch reapplies the server's stale duplicate violation → no success-data clear to override it → violation comes back.
What changes do you think we should make in order to solve the problem?
Make the sibling clear authoritative, the same way the moved transaction's clear already is. Inside the sibling loop here:
App/src/libs/actions/Transaction.ts
Lines 1152 to 1161 in 75f7b14
in addition to the
optimisticDatapush, also push a matchingsuccessDataentry that sets the sibling's${TRANSACTION_VIOLATIONS}${id}tosiblingViolationswith theDUPLICATED_TRANSACTIONviolation filtered out, and afailureDataentry restoringsiblingViolationsso an error rolls the sibling back. This mirrors what the moved transaction already does and guarantees the clear is reapplied after the request resolves, surviving any reconnect refetch.What alternative solutions did you explore? (Optional)
Keep only optimistic data but rebuild the sibling's
failureData/restore fromallTransactionViolation— but withoutsuccessDatathe clear is still not reasserted after a reconnect refetch, so this would not fix the offline case. Mirroring success + failure for the sibling is the minimal correct change.Reminder: Please use plain English, be brief and avoid jargon. Feel free to use images, charts or pseudo-code if necessary. Do not post large multi-line diffs or write walls of text. Do not create PRs unless you have been hired for this job.
aswin-s commentedon Jun 25, 2026
Please re-state the problem that we are trying to solve in this issue.
When a user removes the first of two duplicate expenses from a report while offline and then comes back online, the "Potential duplicate" violation reappears on the second expense even though the first expense was successfully removed from the report. The expected behavior is that the violation on the second expense should stay cleared once the first expense is removed.
What is the root cause of that problem?
In
src/libs/actions/Transaction.ts, insidechangeTransactionsReport, the block at lines 1148–1178 optimistically clears theDUPLICATED_TRANSACTIONviolation from sibling transactions (lines 1152–1160) when moving a transaction to the unreported state (isUnreported). However, the correspondingfailureDataentries (lines 1174–1178) only restore the removed transaction's own violations — they do not restore the sibling transactions' violations that were cleared in the optimistic update.The same gap exists for
successData: when the API call eventually succeeds, there is nosuccessDataentry for the sibling transactions either. This means that if a queued command (such asOpenReportresolving between the optimistic apply and the server response) re-introduces a one-sided duplicate violation on the sibling, there is nothing to re-apply the cleaned state on success. The server then re-derives duplicate violations from scratch and re-attaches them to the sibling, overwriting the now-stale optimistic clear.The fix lives in the same
isUnreportedblock: both thesuccessDataandfailureDataarrays must include aSETentry for eachsiblingID, mirroring the optimistic clear pushed at lines 1155–1159.What changes do you think we should make in order to solve the problem?
src/libs/actions/Transaction.ts(lines 1152–1160) — After pushing the optimistic clear for each sibling, also push matchingsuccessDataandfailureDataentries for that sibling:The
successDataentry ensures that even if a stale server push re-introduces a one-sided violation between the optimistic apply and the API response, the final committed state reflects the cleared violation. ThefailureDataentry restores the sibling's original violations if the API call fails.src/libs/actions/Transaction.ts(lines 1168–1172) — Similarly confirm that any existingsuccessDataentry for the removed transaction's own violations is also present; the sibling entries above must be added alongside it, not replace it.Confirm that
useUndeleteTransactions(which also callschangeTransactionsReport) passes through the fulltransactionsarray — the PR Remove Onyx.connect() usage for ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS as allTransactionViolations from src/libs/actions/IOU/Transaction.ts #91196 diff already changed the parameter fromallTransactions: OnyxCollectiontotransactions: Transaction[], so verify that the sibling IDs are still resolvable fromallTransactionViolationat line 1154 without depending on the removedallTransactionscollection.What alternative solutions did you explore? (Optional)
Calling
removeTransactionFromDuplicateTransactionViolation(used inDeleteMoneyRequest.ts:738) insidechangeTransactionsReportfor theisUnreportedpath would also clear sibling violations symmetrically, but addingsuccessDataandfailureDataentries in the existing inline block is a smaller, more targeted change that avoids introducing a dependency between two separate action modules.melvin-bot commentedon Jun 25, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @nyomanjyotisa (
External)sumairq commentedon Jun 25, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
melvin-bot commentedon Jun 25, 2026
Job added to Upwork: https://www.upwork.com/jobs/~022070016297932142772
[-]"Potential duplicate" shown on second expense online after removing first offline[/-][+][$250] "Potential duplicate" shown on second expense online after removing first offline[/+]MobileMage commentedon Jun 25, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
When two expenses are duplicates of each other (both carry the
duplicatedTransactionviolation referencing the other) and the user removes the first expense from its report while offline, the "Potential duplicate" violation disappears optimistically on the second expense as expected. But the moment the device goes back online, the violation reappears on the second expense even though its only duplicate is gone. The optimistic clear is not durable across the online reconciliation.What is the root cause of that problem?
The "Remove from report" action calls
removeFromReportinuseReportSelectionActions, which invokeschangeTransactionsReportwith nonewReport, soreportIDresolves toCONST.REPORT.UNREPORTED_REPORT_IDandisUnreportedistrue. Inside that branch, the action tries to clear the duplicate violation not only on the moved transaction but also on its siblings (the other transactions that listed the moved one in theirdata.duplicates). The moved transaction (A) gets its violations wiped acrossoptimisticData,successData, andfailureData, but the sibling clear (transaction B) is only written tooptimisticData— there is nosuccessDataand nofailureDatafor the sibling:App/src/libs/actions/Transaction.ts
Lines 1150 to 1161 in 75f7b14
Because the sibling clear lives only in
optimisticData, it is fragile. While offline, the request sits in the queue and B looks fine. When the device reconnects, the queuedCHANGE_TRANSACTIONS_REPORTis flushed alongside other queued/refreshing commands (anOpenReport/Search refresh resolving between the optimistic apply and the server response). Those responses re-push B'sTRANSACTION_VIOLATIONSentry from the server — which still contains the stale one-sidedduplicatedTransactionviolation, since theCHANGE_TRANSACTIONS_REPORTparameters never tell the backend which siblings to re-evaluate (the params only carrytransactionList,reportID, and the report-action/thread map):App/src/libs/actions/Transaction.ts
Lines 1848 to 1855 in 75f7b14
With no
successDatato re-apply the cleaned violation list after the server response lands, B's violation is resurrected. This is exactly the failure mode the codebase already documented and solved elsewhere: the shared helperremoveTransactionFromDuplicateTransactionViolationinTransactionUtilswrites the cleaned sibling list tooptimisticData,successData, ANDfailureData, with a comment stating thesuccessDataexists precisely "so that stale data from a queued command (e.g. OpenReport resolving between optimistic apply and the server response) doesn't re-introduce one-sided duplicate violations." The inline loop inchangeTransactionsReportis a hand-rolled version of that helper that simply forgot thesuccessData(andfailureData) leg.What changes do you think we should make in order to solve the problem?
In the sibling-clear loop of
changeTransactionsReport, mirror the established pattern fromremoveTransactionFromDuplicateTransactionViolation: push the same cleaned sibling value tosuccessData(so the cleared state survives the online reconciliation) and tofailureData(restoring the originalsiblingViolationsif the move fails). The cleaned value is already computed for the optimistic write, so we just reuse it:This is safe and narrowly scoped: it only affects transactions that already appeared in the moved transaction's
data.duplicates, and only theduplicatedTransactionviolation is stripped (all other sibling violations are preserved by the existing filter). AddingfailureDataactually closes a second latent bug — today, if the move fails, the sibling's violation is never restored. Ideally we would reuseremoveTransactionFromDuplicateTransactionViolationdirectly since it also reduces (rather than fully strips) the sibling list when the sibling has more than one duplicate, but that helper needs the fullOnyxCollection<Transaction>forgetValidDuplicateTransactionIDs, whichchangeTransactionsReportis not currently passed; matching the helper's three-leg write is the minimal correct fix without widening the function signature.What alternative solutions did you explore? (Optional)
Threading the full transaction collection into
changeTransactionsReportso it can delegate entirely toremoveTransactionFromDuplicateTransactionViolation(gaining its multi-duplicate "reduce instead of strip" handling), but that expands the prop signature and every caller for a scenario the inline loop already covers — adding the missingsuccessData/failureDatalegs fixes the reported bug with far less surface area.68 remaining items
melvin-bot commentedon Aug 6, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Aug 6, 2026
Payment Summary
Resolving PRs:
Upwork Job
BugZero Checklist (@mallenexpensify)
x-dev90 commentedon Aug 10, 2026
@mallenexpensify, could you please help with the payment for this issue. thanks.
mallenexpensify commentedon Aug 11, 2026
Payment Summary
Contributor: @x-dev90 paid $250 via Upwork
Contributor+: @nyomanjyotisa due $250 via NewDot
@x-dev90 can you please accept the job below? Please reply here and tag me once you have.
If you're not sent an offer in Upwork when you're assigned to an issue, please tag me. If an Upwork job exists, please submit a proposal.
https://www.upwork.com/jobs/~022086977015188086070
x-dev90 commentedon Aug 11, 2026
@mallenexpensify, The offer accepted. thanks.
mallenexpensify commentedon Aug 12, 2026
@x-dev90 paid, summary updated above. Thx.
nyomanjyotisa commentedon Oct 19, 2026
@mallenexpensify I think I missed requesting this $250 payment. Can I create the NewDot expense now?
mallenexpensify commentedon Oct 19, 2026
@nyomanjyotisa yes, if you haven't been paid, request payment now.
In the future, try to request payment in the same month as the job was finished (or as soon after as possible)
trjExpensify commentedon Oct 19, 2026
approved $250 for @nyomanjyotisa