Show cash back credits in Bank Reconciliation - #97958
Conversation
Surface ACH cash back credits as their own non-expandable row in the Expensify Card Bank Reconciliation view, matching Expensify Classic. The backend flags these groups with isCashBack and returns the total already signed negative, so the row renders a Cash back badge, a blank Expenses cell and the credit amount with no client-side negation. The row is not expandable in either render path: TransactionGroupListItem on narrow and native, GroupHeader on wide web. Selection needs an explicit groupAmount because the footer falls back to -Math.abs(amount) when it is absent, which would make a credit add to the selected total instead of reducing it.
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
@Julesssss could you pls help generating translations? thanks |
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cce3db43f5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| queryJSON && withdrawalIDGroup.entryID ? buildSpecificGroupQuery(queryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID, withdrawalIDGroup.entryID) : undefined; | ||
|
|
||
| if (!withdrawalIDGroup.accountNumber) { | ||
| if (!withdrawalIDGroup.accountNumber && !withdrawalIDGroup.isCashBack) { |
There was a problem hiding this comment.
Preserve account-number guard for mobile cash-back rows
When the backend sends a cash-back group without accountNumber, this new exception lets the row reach WithdrawalIDListItemHeader, but the narrow/mobile rendering still evaluates withdrawalIDItem.accountNumber.slice(-4), which will throw before the cash-back row can render. Either keep filtering rows without an account number or make the mobile label use the already-safe maskedNumber/fallback path as the large-screen path does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
accountNumber comes from BE. #94133 (comment)
…unt number guard The cash back spacer passed an inline object literal to style, which allocates a new object each render and bypasses the shared style system. Use StyleUtils.getWidthAndHeightStyle instead, in both render paths. Also drop the isCashBack exception to the account number guard. The backend sends accountNumber on cash back groups, so the exception never fired, and relaxing the guard would have let a row through to a narrow-screen label that dereferences accountNumber without a fallback.
WithdrawalIDListItemHeader had no test file at all, so the cash back badge, the blank Expenses cell and the negative total were uncovered. Each case is paired with a settlement control so a regression that drops the cash back branch fails rather than silently rendering a plausible "Cleared" row. The expand-arrow cases run under narrow layout because that is the only layout where this component owns the arrow; on wide screens the parent supplies the toggle. Also covers isCashBackWithdrawalGroup directly, including a group of another type carrying the flag, which must not opt into cash back rendering.
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index a89d7c1f88f..4da1d962d7e 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -9181,13 +9181,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
},
},
settlement: {
- status: {
- pending: 'Ausstehend',
- cleared: 'Ausgeglichen',
- failed: 'Fehlgeschlagen',
- never: 'Nie',
- cashBack: 'Cashback',
- },
+ status: {pending: 'Ausstehend', cleared: 'Ausgeglichen', failed: 'Fehlgeschlagen', never: 'Nie', cashBack: 'Cashback'},
failedError: ({link}: {link: string}) => `Wir versuchen diese Abrechnung erneut, sobald du <a href="${link}">dein Konto entsperrst</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Auszahlungs-ID: ${withdrawalID}`,
},
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 7d4818ab075..fb0779a9f5e 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -9400,13 +9400,7 @@ ${reportName}`,
},
},
settlement: {
- status: {
- pending: 'Σε εκκρεμότητα',
- cleared: 'Εκκαθαρισμένο',
- failed: 'Απέτυχε',
- never: 'Ποτέ',
- cashBack: 'Επιστροφή χρημάτων',
- },
+ status: {pending: 'Σε εκκρεμότητα', cleared: 'Εκκαθαρισμένο', failed: 'Απέτυχε', never: 'Ποτέ', cashBack: 'Επιστροφή μετρητών'},
failedError: ({link}: {link: string}) => `Θα προσπαθήσουμε ξανά για αυτόν τον διακανονισμό όταν <a href="${link}">ξεκλειδώσετε τον λογαριασμό σας</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Αναγνωριστικό ανάληψης: ${withdrawalID}`,
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 3ad482bc841..d2cd57fa964 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -8974,13 +8974,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`,
},
},
settlement: {
- status: {
- pending: 'Pendiente',
- cleared: 'Liquidado',
- failed: 'Fallido',
- never: 'Nunca',
- cashBack: 'Reembolso',
- },
+ status: {pending: 'Pendiente', cleared: 'Liquidado', failed: 'Fallido', never: 'Nunca', cashBack: 'Devolución de dinero'},
failedError: ({link}: {link: string}) => `Reintentaremos esta liquidación cuando <a href="${link}">desbloquees tu cuenta</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID de retiro: ${withdrawalID}`,
},
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 4514860ba7d..f93be460763 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -9216,13 +9216,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
},
},
settlement: {
- status: {
- pending: 'En attente',
- cleared: 'Compensé',
- failed: 'Échec',
- never: 'Jamais',
- cashBack: 'Remise en argent',
- },
+ status: {pending: 'En attente', cleared: 'Compensé', failed: 'Échec', never: 'Jamais', cashBack: 'Remboursement en espèces'},
failedError: ({link}: {link: string}) => `Nous réessaierons ce règlement lorsque vous <a href="${link}">déverrouillerez votre compte</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID de retrait : ${withdrawalID}`,
},
diff --git a/src/languages/it.ts b/src/languages/it.ts
index ee8d06156c2..95ce99842a2 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -9155,13 +9155,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
},
},
settlement: {
- status: {
- pending: 'In sospeso',
- cleared: 'Compensato',
- failed: 'Non riuscito',
- never: 'Mai',
- cashBack: 'Cashback',
- },
+ status: {pending: 'In sospeso', cleared: 'Compensato', failed: 'Non riuscito', never: 'Mai', cashBack: 'Cashback'},
failedError: ({link}: {link: string}) => `Riproveremo a effettuare questa liquidazione quando <a href="${link}">sblocchi il tuo conto</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID prelievo: ${withdrawalID}`,
},
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index de583f58b81..991e534662f 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -9031,13 +9031,7 @@ ${reportName}`,
},
},
settlement: {
- status: {
- pending: '保留中',
- cleared: '支払済み',
- failed: '失敗しました',
- never: 'なし',
- cashBack: 'キャッシュバック',
- },
+ status: {pending: '保留中', cleared: '支払済み', failed: '失敗しました', never: 'なし', cashBack: 'キャッシュバック'},
failedError: ({link}: {link: string}) => `<a href="${link}">アカウントのロックを解除</a>すると、この精算を再試行します。`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date}・出金 ID:${withdrawalID}`,
},
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ad1a029c736..a86acd078af 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -9127,13 +9127,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
},
},
settlement: {
- status: {
- pending: 'In behandeling',
- cleared: 'Verrekend',
- failed: 'Mislukt',
- never: 'Nooit',
- cashBack: 'Cashback',
- },
+ status: {pending: 'In behandeling', cleared: 'Verrekend', failed: 'Mislukt', never: 'Nooit', cashBack: 'Cashback'},
failedError: ({link}: {link: string}) => `We proberen deze afrekening opnieuw zodra je <a href="${link}">je account ontgrendelt</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Opname-ID: ${withdrawalID}`,
},
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index f3ee77caba2..eff8ee13ec3 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -9105,13 +9105,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
},
},
settlement: {
- status: {
- pending: 'Oczekujące',
- cleared: 'Wyczyszczono',
- failed: 'Niepowodzenie',
- never: 'Nigdy',
- cashBack: 'Zwrot gotówki',
- },
+ status: {pending: 'Oczekujące', cleared: 'Wyczyszczono', failed: 'Niepowodzenie', never: 'Nigdy', cashBack: 'Zwrot gotówki'},
failedError: ({link}: {link: string}) => `Spróbujemy ponownie rozliczyć tę płatność, gdy <a href="${link}">odblokujesz swoje konto</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID wypłaty: ${withdrawalID}`,
},
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index c0922593104..cb2199eba92 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -9118,13 +9118,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
},
},
settlement: {
- status: {
- pending: 'Pendente',
- cleared: 'Compensado',
- failed: 'Falhou',
- never: 'Nunca',
- cashBack: 'Cashback',
- },
+ status: {pending: 'Pendente', cleared: 'Compensado', failed: 'Falhou', never: 'Nunca', cashBack: 'Cashback'},
failedError: ({link}: {link: string}) => `Tentaremos processar este acerto novamente quando você <a href="${link}">desbloquear sua conta</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID de saque: ${withdrawalID}`,
},
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 475bc865c39..cbbe39e9db1 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -8802,13 +8802,7 @@ ${reportName}`,
},
},
settlement: {
- status: {
- pending: '待处理',
- cleared: '已入账',
- failed: '失败',
- never: '从不',
- cashBack: '返现',
- },
+ status: {pending: '待处理', cleared: '已入账', failed: '失败', never: '从不', cashBack: '返现'},
failedError: ({link}: {link: string}) => `当你<a href="${link}">解锁你的账户</a>后,我们会重试此结算。`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • 提现 ID:${withdrawalID}`,
},
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
|
@situchan we are good to move now |
|
@situchan kindly bump |
# Conflicts: # tests/unit/TransactionGroupListItemTest.tsx
|
It's odd to me that cash back row is hoverable, clickable but no action when clicked Screen.Recording.2026-08-12.at.1.26.03.PM.mov |
|
@situchan I guess "not clicked" as expected. so we want no hoverable, no clickable? cc @trjExpensify @shawnborton |
|
Fair. I think we can make it such that just the checkbox part of the row shows the "clickable" cursor. As that's all that is for these cashback rows. |
|
Curious for @shawnborton's take on that. |
# Conflicts: # src/components/Search/SearchList/ListItem/TransactionGroupListItem.tsx # tests/unit/TransactionGroupListItemTest.tsx
The withdrawal status column sorted on the raw settlement state, and a cash back row carries state 8 while showing a badge that is not a settlement status. It tied with the cleared settlements and interleaved among them. Sort on a derived rank instead, so cash back ranks past every settlement state. Also drop the pointer cursor and hover highlight from a cash back row, which has nothing to open; its checkbox stays interactive.
|
@situchan could you pls have another look here? thanks |
|
Looks good. Just waiting for design feedback on #97958 (comment) |
|
Sorry for the delay - yes, I agree with what Tom is saying - only use a pointer cursor over the checkbox of those rows where the entire row cannot be clicked. |
|
Perf, let's do it. 👍 |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
Bug: 0 selected Screen.Recording.2026-08-13.at.4.06.57.AM.mov |
The header sums how many expenses each selected group holds. A cash back row holds none, so ticking it read "0 selected" with a dropdown that opened nothing. Count a cash back group as one item. The footer still reads 0 there, which is correct: it reports expenses, not selected rows.
@situchan fixed it |
|
oh wait.. |
|
@shawnborton @trjExpensify a cash back row has 0 expenses, so bulk actions dropdown opens with only "Current View" which doesn't work as well. should the row be selectable at all? it does contribute its credit to Total spend, which feels like the point on a reconciliation page, but there's no action you can take on it. |
|
@ShridharGoel mind helping here too?
the footer sums
sortBy is sent to the server, so the server still picks page membership by raw state. can the ranking match server side, or do you want me to leave the FE sort diverging and accept rows shifting as you paginate? |
Every loaded group was stamped -Math.abs(total), but a selected cash back row's live source is a positive groupAmount. The freshness check compares the two, so a cash back group stamped by the bulk path never went fresh and its cached conversion was refetched every time.
|
Hmm so if you select the single row, there is no action you could take from the "1 selected" button? If that's the case, I guess it would make sense to have it be unselectable (just keep the checkbox disabled?) |
|
@shawnborton okay, so if user selects all from top, we skip cashback row, right? |
|
That makes sense. Let's see what Tom thinks though too. |
|
Mhm, I wouldn't think we should skip the cashback and it would be included in the CSV export as it forms part of the rec. @joekaufmanexpensify do you have a test account with cashback to download from Classic to doublecheck that? |
hmm maybe that's why it didn't work for me. I mean on my account |
Backend sorts by |
Yep, I just confirmed we include cashback rows in both the CSV and statement downloads in OldDot. I would assume we would want the row to be selectable so it can be included in either a NewDot as well. |
|
Thanks, Joe! |
Explanation of Change
Shows ACH cash back credits as their own row in the Expensify Card Bank Reconciliation view, matching Expensify Classic.
isCashBackand already returnstotalsigned negative, so there is no client-side negationisCashBackis a flag rather than aSETTLEMENT_STATUSvalue, because cash back is orthogonal to settlement state and adding it to the enum would leak it into thewithdrawal-status:filterCash backbadge, a blank Expenses cell, and the credit amountTransactionGroupListItemon narrow and native,GroupHeaderon wide webgroupAmount, since the footer otherwise falls back to-Math.abs(amount)and would add the credit instead of subtracting itFixed Issues
$ #94133
PROPOSAL: #94133 (comment)
Tests
Requires an account on the
cashBackReconciliationbeta with an Expensify Card and at least one settled ACH cash back credit.type:expense groupBy:withdrawal-id withdrawalType:expensify-card)Withdrawal statuscolumn shows a greenCash backbadge, notPending/Cleared/FailedExpensescolumn is blank, not0Totalshows a negative amountOffline tests
The reconciliation view is read-only, so there is no optimistic write to verify. Going offline with the view open keeps the last fetched rows rendered, including the cash back row, and no new Search request is issued until the connection returns.
QA Steps
Same as tests. Note this needs an account enrolled in the
cashBackReconciliationbeta with real ACH cash back activity, since the backend only returns these groups to beta accounts.PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
android_app.mp4
Android: mWeb Chrome
android_web.mp4
iOS: Native
IOS_app.mp4
iOS: mWeb Safari
IOS-mWeb.mp4
MacOS: Chrome / Safari
94133-cashback-reconciliation-web.mp4