Skip to content

feat(condo): DOMA-13196 upgrade billing receipts table#7638

Open
dkoviazin wants to merge 15 commits into
mainfrom
feat/condo/DOMA-13196/upgrade_billing_receipt_table
Open

feat(condo): DOMA-13196 upgrade billing receipts table#7638
dkoviazin wants to merge 15 commits into
mainfrom
feat/condo/DOMA-13196/upgrade_billing_receipt_table

Conversation

@dkoviazin
Copy link
Copy Markdown
Contributor

@dkoviazin dkoviazin commented May 20, 2026

  1. Allow staff users to delete billingReceipts
  2. New Table UI component for billingReceiptsPage

BEFORE:
Screenshot 2026-05-21 at 16 49 25

AFTER:
Screenshot 2026-05-21 at 16 48 26

Summary by CodeRabbit

  • New Features

    • Select and bulk-delete receipts with confirmation; selection-aware action bar appears.
  • Improvements

    • Table replaced with shared, URL-synced implementation so filters, sorting, pagination and selection persist and are shareable.
    • Enhanced address lookup for filtering, refined column configs and cell rendering for clearer amounts and text.
    • Period selector updates table filter directly.
    • GraphQL receipt property now includes additional address metadata.
  • Bug Fixes

    • Shows clear empty/error state with Retry when table load fails.
    • Services modal closes cleanly and resets expanded state.
  • Tests

    • Added permission-focused tests covering soft-delete and multi-delete flows (employee/importer cases).

Review Change Stack

@dkoviazin dkoviazin added the 🔬 WIP Not intended to be merged right now, it is a work in progress label May 20, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

ReceiptsTable and supporting hooks were refactored to use the @open-condo/ui Table with URL-driven state. Filters and columns moved to typed hooks, address search became GraphQL-backed, data is loaded via a table dataSource, table state syncs to the URL, and bulk soft-delete actions plus access-check changes were added.

Changes

Receipts Table Refactoring

Layer / File(s) Summary
Filter contracts and address search
apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx
useReceiptTableFilters now accepts contextIds, performs GraphQL-backed billing property search for the address filter (async GQLSelect), deduplicates options by addressKey, and returns TableFiltersMeta entries.
Typed column definitions and renderers
apps/condo/domains/billing/hooks/useReceiptTableColumns.ts
useReceiptTableColumns rebuilt to return typed ReceiptColumn[] using a receiptDataKey map, centralized text/money renderers, and filterComponent wiring from filterMetas; router-driven sort/filter plumbing and the pdf column were removed.
Services modal behavior
apps/condo/domains/billing/components/BillingPageContent/ServicesModal.tsx
ServicesModal adds handleClose to clear expanded state and delegate onCancel, gates render by visible and services, wires Modal onCancel to handleClose, and enables destroyOnClose.
BillingReceipt GraphQL selection
apps/condo/domains/billing/gql.js
BillingReceiptForOrganization property selection expanded to include addressMeta subfields via ADDRESS_META_SUBFIELDS_QUERY_LIST.
Table setup, state and dataSource
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
Replaced legacy imports with @open-condo/ui table stack, derive initialTableState from URL, implement updateUrlQuery, wire onPeriodChange/onRowClick, add dataSource mapping table state to GraphQL where/sortBy and running GET_ALL_OBJS_WITH_COUNT_QUERY with pagination; added loadingError handling and tour-step update.
Render, selection and bulk-delete actions
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
Render guard shows BasicEmptyListView on loadingError. Table uses dataSource, initialTableState, onTableStateChange (URL sync), selection props (rowSelectionOptions, getRowId, onTableReady), and conditionally shows ActionBar with soft-delete (sets deletedAt via updates) for selected rows.
Authorization changes
apps/condo/domains/billing/access/BillingReceipt.js
canManageBillingReceipts updated with explicit branches: admins allowed, soft-deleted users denied, STAFF update limited to validated soft-delete-only shapes (single/batch) and checked against employed-organization canImportBillingReceipts; otherwise falls back to existing entity-context check.
Permission tests
apps/condo/domains/billing/schema/BillingReceipt.test.js
Tests add a billingReceiptsImporter client with canImportBillingReceipts/canReadBillingReceipts and assert employee-role permission behavior for soft-delete (single and multiple) and org-scoped delete checks.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

✋🙂 Review please

Suggested reviewers

  • Alexander-Turkin

Poem

🐰 I hopped through rows and filters bright,

URLs humming under moonlight,
Addresses found by GraphQL's call,
Columns tidy, money cells tall,
Soft-deletes tiptoe — neat and light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'upgrade billing receipts table' directly and specifically summarizes the main change—refactoring the ReceiptsTable component to use the shared @open-condo/ui Table with controlled URL state and enhanced features.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/condo/DOMA-13196/upgrade_billing_receipt_table

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 26-27: The import order is incorrect: place internal imports
before other same-group imports and alphabetize them; move the ServicesModal
import so it appears above the BillingReceiptForOrganizationGQL import in
ReceiptsTable.tsx (i.e., ensure ServicesModal is imported before
BillingReceiptForOrganizationGQL) and adjust surrounding newlines so imports
follow the project's groups (builtin → external → `@open-condo` → internal →
sibling → parent) and are alphabetized case-insensitively within their group.
- Around line 216-223: The bulk-delete loop using updateReceipt aborts on the
first rejected mutation and prevents the UI reset/refetch, causing a mixed
state; change the logic to run all mutations concurrently with
Promise.allSettled over selectedRows calling updateReceipt({ deletedAt }, { id
}), collect successes and failures, ensure selection reset
(tableRef.current?.api?.resetRowSelection()), setSelectedRowsCount(0), reset
pagination (tableRef.current?.api?.setPagination({ startRow: 0, endRow:
DEFAULT_PAGE_SIZE })), and always call tableRef.current?.api?.refetchData() even
if some mutations failed; surface or log failed IDs/errors so callers can show
an error message but do not skip the UI cleanup when there are partial failures
(refer to updateReceipt, selectedRows, tableRef, setSelectedRowsCount,
DEFAULT_PAGE_SIZE).
- Around line 100-116: Remove persisting row selection to URL: stop serializing
rowSelectionState into router.query by deleting the nextSelectedRows computation
and the branch that sets/deletes nextQuery.selectedRows; keep rowSelectionState
as ephemeral UI state only (do not add selectedRows to nextQuery), and ensure
only filters, sort and offset are written to router.query (references:
nextSelectedRows, rowSelectionState, nextQuery, ReceiptsTable.tsx).
- Around line 142-152: The onPeriodChange handler sets local state via setPeriod
but when value is cleared it incorrectly restores nextFilterState.period to
reportPeriod so the table remains filtered; update onPeriodChange (and its use
of tableRef?.api?.setFilterState) so that when value is null you set
nextFilterState.period = undefined (i.e., remove the effective period filter)
instead of assigning reportPeriod, ensuring the table filter matches the cleared
picker UI while leaving the reportPeriod logic unchanged when a value is
selected.

In `@apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx`:
- Around line 26-30: The current construction of the where object lets
caller-provided query properties override the guard because ...query is spread
last; to fix, ensure the context filter (context: { id_in: contextIds }) is
authoritative by spreading query first and then explicitly setting context and
the address_contains_i based on searchText (i.e., build where as { ...query,
context: { id_in: contextIds }, ...( !isEmpty(searchText) ? {
address_contains_i: searchText } : {} ) }), so neither query.context nor
query.address_contains_i can override the enforced billing context or the
searchText-derived address filter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 801e6c16-91a1-463b-9beb-8b6ca5f9796e

📥 Commits

Reviewing files that changed from the base of the PR and between 704a6ac and 8989f7f.

📒 Files selected for processing (3)
  • apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
  • apps/condo/domains/billing/hooks/useReceiptTableColumns.ts
  • apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx

Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Comment thread apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8989f7f045

ℹ️ 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".

Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx (1)

277-287: 💤 Low value

Add type annotation for the event parameter.

The event parameter has an implicit any type. Consider typing it based on the bridge event shape to improve type safety.

💡 Suggested fix
 useEffect(() => {
-    const handleRedirect = async (event) => {
+    const handleRedirect = async (event: { type?: string }) => {
         if (get(event, 'type') === 'condo-bridge') {
             await tableRef.current?.api?.refetchData()
         }
     }

Or use the proper event type from @open-condo/bridge if one is exported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 277 - 287, The handler handleRedirect currently declares event with
implicit any; add a proper type for its parameter (e.g., the exported
BridgeEvent type from `@open-condo/bridge` or a local interface matching { type:
string; [key: string]: any }) and update the signature of handleRedirect to use
that type so bridge.subscribe(handleRedirect) and
bridge.unsubscribe(handleRedirect) are type-safe; ensure imports include the
chosen type and that tableRef.current?.api?.refetchData usage remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/condo/domains/billing/hooks/useReceiptTableColumns.ts`:
- Around line 38-46: Rename the file containing JSX to use the .tsx extension so
TypeScript can parse JSX: change useReceiptTableColumns.ts to
useReceiptTableColumns.tsx; locate the renderTextCellWithoutTooltip function
(which returns <Typography.Paragraph> and uses ELLIPSIS_CONFIG/ELLIPSIS_STYLES
and getHighlightedContents) to confirm the JSX-containing module is renamed, and
update any import sites that explicitly reference the .ts filename so consumers
import the updated .tsx module.

---

Nitpick comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 277-287: The handler handleRedirect currently declares event with
implicit any; add a proper type for its parameter (e.g., the exported
BridgeEvent type from `@open-condo/bridge` or a local interface matching { type:
string; [key: string]: any }) and update the signature of handleRedirect to use
that type so bridge.subscribe(handleRedirect) and
bridge.unsubscribe(handleRedirect) are type-safe; ensure imports include the
chosen type and that tableRef.current?.api?.refetchData usage remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5428309f-4765-4ad4-8aee-826c5785da08

📥 Commits

Reviewing files that changed from the base of the PR and between 8989f7f and 0ca0700.

📒 Files selected for processing (3)
  • apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
  • apps/condo/domains/billing/components/BillingPageContent/ServicesModal.tsx
  • apps/condo/domains/billing/hooks/useReceiptTableColumns.ts

Comment thread apps/condo/domains/billing/hooks/useReceiptTableColumns.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx (2)

204-209: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

A transient load failure becomes unrecoverable here.

After the catch sets loadingError to true, the component returns the empty state and unmounts the table. Nothing in this render path resets that flag, so the user cannot trigger another fetch without a full page reload.

Also applies to: 307-315

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 204 - 209, The catch handler currently sets loadingError to true
and returns an empty result, which leaves the table unmounted with no way to
retry; update the data-fetch path (the function that catches errors and returns
{ rowData, rowCount } — reference setLoadingError and the fetch/load function
around the current catch and the similar block at 307-315) so that you clear or
reset loadingError before each new fetch attempt (call setLoadingError(false) at
the start of the loader or retry logic) and set setLoadingError(false) on
successful responses; alternatively expose a retry path (e.g., set a retry state
or provide a retry button that clears loadingError) so transient failures do not
become unrecoverable.

63-73: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the bulk-delete UI aligned with the new access rules.

The server-side access change still allows admins to soft-delete receipts, but canManageReceipts now only checks link.role.canImportBillingReceipts. That hides row selection and the ActionBar for admins who are still authorized by the mutation layer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 63 - 73, canManageReceipts only checks
link.role.canImportBillingReceipts so admins who can soft-delete on the server
still get hidden UI; update the canManageReceipts assignment in ReceiptsTable
(where useOrganization and useBillingAndAcquiringContexts are used) to include
the deletion/soft-delete permission as well (e.g. check
link.role.canDeleteBillingReceipts or link.role.canSoftDeleteBillingReceipts in
addition to link.role.canImportBillingReceipts) so row selection and the
ActionBar are shown for users allowed to soft-delete by the mutation layer.
♻️ Duplicate comments (3)
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx (3)

213-226: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle bulk-delete failures without leaving the UI half-reset.

The first rejected updateReceipt aborts the loop, so some receipts may already be soft-deleted while selection reset, pagination reset, and refetch never run. Run the mutations with Promise.allSettled and always perform UI cleanup/refetch afterward.

Suggested fix
-        for (const id of selectedRows) {
-            await updateReceipt({ deletedAt }, { id })
-        }
-
-        tableRef.current?.api?.resetRowSelection()
-        setSelectedRowsCount(0)
-        tableRef.current?.api?.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE })
-        await tableRef.current?.api?.refetchData()
+        const results = await Promise.allSettled(
+            selectedRows.map((id) => updateReceipt({ deletedAt }, { id })),
+        )
+
+        tableRef.current?.api?.resetRowSelection()
+        setSelectedRowsCount(0)
+        tableRef.current?.api?.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE })
+        await tableRef.current?.api?.refetchData()
+
+        const failedIds = selectedRows.filter((_, index) => results[index].status === 'rejected')
+        if (failedIds.length > 0) {
+            // surface/log failed ids so the user is not left guessing
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 213 - 226, The softDeleteSelectedReceipts handler currently awaits
each updateReceipt in a for-loop so a rejected mutation aborts the loop and
prevents the UI cleanup (tableRef.api.resetRowSelection, setSelectedRowsCount,
setPagination, refetchData) from running; change softDeleteSelectedReceipts to
collect promises for updateReceipt for each selectedRows ID, run them with
Promise.allSettled, and then always perform the cleanup/refetch
(tableRef.current?.api.resetRowSelection(), setSelectedRowsCount(0),
tableRef.current?.api.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE }),
await tableRef.current?.api.refetchData()) in a finally block (or after
allSettled) so partial failures don’t leave the UI half-reset while still
surfacing or logging per-id failures from the allSettled results.

143-153: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Clearing the month picker should also clear the effective filter.

When value is cleared, the control shows no month selected, but the handler restores nextFilterState.period to reportPeriod. The table stays filtered while the UI says otherwise.

Suggested fix
-        setPeriod(value)
-
         const currentFilterState = tableRef.current?.api?.getFilterState() || {}
         const nextFilterState = { ...currentFilterState }
-        if (value && dateString) {
-            nextFilterState.period = value.startOf('month').format('YYYY-MM-01')
-        } else {
-            nextFilterState.period = reportPeriod || undefined
-        }
+        const nextPeriod = value && dateString ? value.startOf('month') : null
+        setPeriod(nextPeriod)
+        nextFilterState.period = nextPeriod?.format('YYYY-MM-01')
         tableRef.current?.api?.setFilterState(nextFilterState)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 143 - 153, The onPeriodChange handler restores reportPeriod when
value is cleared which leaves the table filtered while the month picker shows
empty; update onPeriodChange (the function that calls setPeriod and uses
tableRef.current?.api?.setFilterState) so that when value is null you set
nextFilterState.period = undefined (clearing the effective filter) instead of
assigning reportPeriod, ensuring the UI and table filter state stay in sync.

88-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not persist selected row ids in the URL.

selectedRows is ephemeral UI state. Serializing it into router.query makes refresh/share/back-navigation brittle and can hit common URL length limits on bulk selection.

Suggested fix
-        const { startRow, filterState, sortState, rowSelectionState, globalFilter } = params
+        const { startRow, filterState, sortState, globalFilter } = params
@@
-        const nextSelectedRows = rowSelectionState?.length > 0 ? JSON.stringify(rowSelectionState) : undefined
-
         if (nextOffset) nextQuery.offset = nextOffset
         else delete nextQuery.offset
@@
         if (nextSortValue) nextQuery.sort = nextSortValue
         else delete nextQuery.sort
-
-        if (nextSelectedRows) nextQuery.selectedRows = nextSelectedRows
-        else delete nextQuery.selectedRows
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 88 - 117, The code is persisting ephemeral UI state
(rowSelectionState) into the URL via nextSelectedRows/nextQuery.selectedRows;
remove that behavior by deleting the creation of nextSelectedRows and the
conditional that sets nextQuery.selectedRows, and instead ensure
nextQuery.selectedRows is removed/unset (delete nextQuery.selectedRows) so
selected row ids are never serialized into router.query; update any references
to rowSelectionState/nextSelectedRows in ReceiptsTable (e.g., nextSelectedRows,
rowSelectionState) accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/condo/domains/billing/access/BillingReceipt.js`:
- Around line 70-75: The current isSoftDeleteInput function incorrectly requires
exactly three keys; change it to check that the payload contains a deletedAt
field and that every key present (from data or data.data) is included in
SOFT_DELETE_ALLOWED_UPDATE_FIELDS instead of enforcing keys.length === 3, so
isSoftDeleteInput allows `{ deletedAt }` and also validates any extra keys are
whitelisted.
- Around line 101-109: The current permission check uses
find('BillingIntegrationOrganizationContext', ...) and only rejects when no
contexts are returned, allowing batches to pass if some contextIds are missing;
change the logic in the function that uses find (the block that populates
contexts from BillingIntegrationOrganizationContext with contextIds) to validate
that every requested context was resolved by comparing contexts.length to
contextIds.length and return false (reject) if they differ (i.e., any context is
missing or soft-deleted) before calling checkPermissionsInEmployedOrganizations;
keep the rest of the flow (building organizationIds and calling
checkPermissionsInEmployedOrganizations) unchanged.

In `@apps/condo/domains/billing/schema/BillingReceipt.test.js`:
- Around line 382-391: The test incorrectly treats deletedReceipt as a single
receipt even though updateTestBillingReceipts returns an array; change the
assertion to inspect the array elements (or destructure the returned array into
e.g. [deletedReceipt1, deletedReceipt2]) from the call to
updateTestBillingReceipts and assert each element's deletedAt is set, and remove
the debug console.error; reference createTestBillingReceipt,
updateTestBillingReceipts and variables receiptToDelete / oneMoreReceiptToDelete
to locate the fix.

---

Outside diff comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 204-209: The catch handler currently sets loadingError to true and
returns an empty result, which leaves the table unmounted with no way to retry;
update the data-fetch path (the function that catches errors and returns {
rowData, rowCount } — reference setLoadingError and the fetch/load function
around the current catch and the similar block at 307-315) so that you clear or
reset loadingError before each new fetch attempt (call setLoadingError(false) at
the start of the loader or retry logic) and set setLoadingError(false) on
successful responses; alternatively expose a retry path (e.g., set a retry state
or provide a retry button that clears loadingError) so transient failures do not
become unrecoverable.
- Around line 63-73: canManageReceipts only checks
link.role.canImportBillingReceipts so admins who can soft-delete on the server
still get hidden UI; update the canManageReceipts assignment in ReceiptsTable
(where useOrganization and useBillingAndAcquiringContexts are used) to include
the deletion/soft-delete permission as well (e.g. check
link.role.canDeleteBillingReceipts or link.role.canSoftDeleteBillingReceipts in
addition to link.role.canImportBillingReceipts) so row selection and the
ActionBar are shown for users allowed to soft-delete by the mutation layer.

---

Duplicate comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 213-226: The softDeleteSelectedReceipts handler currently awaits
each updateReceipt in a for-loop so a rejected mutation aborts the loop and
prevents the UI cleanup (tableRef.api.resetRowSelection, setSelectedRowsCount,
setPagination, refetchData) from running; change softDeleteSelectedReceipts to
collect promises for updateReceipt for each selectedRows ID, run them with
Promise.allSettled, and then always perform the cleanup/refetch
(tableRef.current?.api.resetRowSelection(), setSelectedRowsCount(0),
tableRef.current?.api.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE }),
await tableRef.current?.api.refetchData()) in a finally block (or after
allSettled) so partial failures don’t leave the UI half-reset while still
surfacing or logging per-id failures from the allSettled results.
- Around line 143-153: The onPeriodChange handler restores reportPeriod when
value is cleared which leaves the table filtered while the month picker shows
empty; update onPeriodChange (the function that calls setPeriod and uses
tableRef.current?.api?.setFilterState) so that when value is null you set
nextFilterState.period = undefined (clearing the effective filter) instead of
assigning reportPeriod, ensuring the UI and table filter state stay in sync.
- Around line 88-117: The code is persisting ephemeral UI state
(rowSelectionState) into the URL via nextSelectedRows/nextQuery.selectedRows;
remove that behavior by deleting the creation of nextSelectedRows and the
conditional that sets nextQuery.selectedRows, and instead ensure
nextQuery.selectedRows is removed/unset (delete nextQuery.selectedRows) so
selected row ids are never serialized into router.query; update any references
to rowSelectionState/nextSelectedRows in ReceiptsTable (e.g., nextSelectedRows,
rowSelectionState) accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 61ecf15f-c8bc-4996-a08e-49bd3b07ff1a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ca0700 and f0dc01b.

📒 Files selected for processing (6)
  • apps/condo/domains/billing/access/BillingReceipt.js
  • apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
  • apps/condo/domains/billing/gql.js
  • apps/condo/domains/billing/hooks/useReceiptTableColumns.ts
  • apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx
  • apps/condo/domains/billing/schema/BillingReceipt.test.js

Comment thread apps/condo/domains/billing/access/BillingReceipt.js Outdated
Comment thread apps/condo/domains/billing/access/BillingReceipt.js Outdated
Comment thread apps/condo/domains/billing/schema/BillingReceipt.test.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx (1)

214-227: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the bulk delete resilient to partial failures.

If one updateReceipt call rejects, this loop exits immediately and skips row-selection reset, pagination reset, and refetch, even though some earlier receipts may already be soft-deleted.

Suggested fix
     const softDeleteSelectedReceipts = useCallback(async () => {
         if (!selectedRowIds.length) return

         const deletedAt = new Date().toISOString()

-        for (const id of selectedRowIds) {
-            await updateReceipt({ deletedAt }, { id })
-        }
-
-        tableRef.current?.api?.resetRowSelection()
-        setSelectedRowsCount(0)
-        setSelectedRowIds([])
-        tableRef.current?.api?.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE })
-        await tableRef.current?.api?.refetchData()
+        const results = await Promise.allSettled(
+            selectedRowIds.map((id) => updateReceipt({ deletedAt }, { id })),
+        )
+
+        tableRef.current?.api?.resetRowSelection()
+        setSelectedRowsCount(0)
+        setSelectedRowIds([])
+        tableRef.current?.api?.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE })
+        await tableRef.current?.api?.refetchData()
+
+        const failedIds = results.flatMap((result, index) => (
+            result.status === 'rejected' ? [selectedRowIds[index]] : []
+        ))
+        if (failedIds.length) {
+            throw new Error(`Failed to delete receipts: ${failedIds.join(', ')}`)
+        }
     }, [selectedRowIds, updateReceipt])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 214 - 227, The bulk-delete loop using selectedRowIds and
updateReceipt is brittle: a rejection aborts the loop and skips cleanup. Change
it to perform updates resiliently (e.g., use Promise.allSettled on
selectedRowIds.map(id => updateReceipt(...)) or wrap each await updateReceipt
call in a try/catch) so failures don’t stop processing other ids; collect/log
failed ids/errors, but always run the cleanup steps
(tableRef.current?.api?.resetRowSelection(), setSelectedRowsCount(0),
setSelectedRowIds([]), tableRef.current?.api?.setPagination(...), await
tableRef.current?.api?.refetchData()) in a finally block so selection,
pagination and refetch happen regardless of partial failures.
🧹 Nitpick comments (2)
apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx (1)

1-24: ⚡ Quick win

Move @app/condo/schema into the internal import group.

Line 1 should come after the external and @open-condo/* imports; keeping it at the top still violates the repo import-order rule and is likely to trip lint.

Suggested fix
-import { BillingReceipt as BillingReceiptType, BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType } from '`@app/condo/schema`'
 import { Col, Row, Space, Typography, type RowProps } from 'antd'
 import dayjs, { Dayjs } from 'dayjs'
 import get from 'lodash/get'
 import isEqual from 'lodash/isEqual'
 import getConfig from 'next/config'
 import { useRouter } from 'next/router'
 import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from 'react'

 import bridge from '`@open-condo/bridge`'
 import { useApolloClient } from '`@open-condo/next/apollo`'
 import { useIntl } from '`@open-condo/next/intl`'
 import { useOrganization } from '`@open-condo/next/organization`'
 import {
     ActionBar,
     ActionBarProps,
     Button,
     FullTableState,
     GetTableData,
     Input,
     RowSelectionState,
     Table,
     TableRef,
 } from '`@open-condo/ui`'

+import { BillingReceipt as BillingReceiptType, BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType } from '`@app/condo/schema`'
 import { ServicesModal } from '`@condo/domains/billing/components/BillingPageContent/ServicesModal`'
As per coding guidelines, "Enforce import order groups: builtin → external → `@open-condo` → internal → sibling → parent with newlines between groups" and "Alphabetize imports case-insensitively within each import group".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`
around lines 1 - 24, The import from '`@app/condo/schema`' (e.g., BillingReceipt,
BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType) is currently
in the wrong group; move that whole import statement into the internal import
group so it appears after external and `@open-condo/`* imports in
ReceiptsTable.tsx, and ensure imports are alphabetized case-insensitively within
each group (adjust surrounding imports such as bridge, useApolloClient, useIntl,
useOrganization, `@open-condo/ui` imports and the moved '`@app/condo/schema`' line
to maintain the repo's import-order rule).
apps/condo/domains/billing/schema/BillingReceipt.test.js (1)

385-392: ⚡ Quick win

Add a mixed-organization batch denial case next to this success path.

This covers the happy path, but the new access helper has batch-specific validation that rejects batches spanning unauthorized contexts. A regression test with one allowed receipt and one foreign receipt would pin the exact branch this PR added.

Suggested test shape
+                    test('Employee with canImportBillingReceipts cannot delete receipts from different organization contexts', async () => {
+                        const [receiptInOwnOrg] = await createTestBillingReceipt(admin, context, property, account)
+                        const [receiptInAnotherOrg] = await createTestBillingReceipt(admin, anotherContext, anotherProperty, anotherAccount)
+
+                        await expectToThrowAccessDeniedErrorToObjects(async () => {
+                            await updateTestBillingReceipts(billingReceiptsImporter, [
+                                { id: receiptInOwnOrg.id, data: { deletedAt: new Date().toISOString() } },
+                                { id: receiptInAnotherOrg.id, data: { deletedAt: new Date().toISOString() } },
+                            ])
+                        })
+                    })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/condo/domains/billing/schema/BillingReceipt.test.js` around lines 385 -
392, Add a negative test next to the current success path that verifies batch
rejection when receipts span organizations: call updateTestBillingReceipts with
billingReceiptsImporter using one id that belongs to the test org (e.g.,
receiptToDelete.id) and one id that belongs to a different organization (create
or reuse a foreign receipt fixture like foreignReceipt.id), then assert the call
fails (throws or returns an error) and that deletedReceipts was not updated;
target the batch validation branch introduced by the access helper by
referencing updateTestBillingReceipts, billingReceiptsImporter, receiptToDelete
and a foreign receipt identifier to pin the mixed-organization denial behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/condo/domains/billing/access/BillingReceipt.js`:
- Around line 70-75: The isSoftDeleteInput predicate currently treats
{deletedAt: null} as a soft-delete payload; update isSoftDeleteInput to only
return true when the payload contains a deletedAt field with a non-null value
(e.g., deletedAt !== null/undefined or typeof deletedAt === 'string' /
instanceof Date as appropriate) in addition to the existing checks against
SOFT_DELETE_ALLOWED_UPDATE_FIELDS so that STAFF with canImportBillingReceipts
cannot use this branch to restore receipts; keep the existing target extraction
(const target = data?.data || data) and overall object/key checks but add the
explicit non-null deletedAt value check before returning true.

---

Duplicate comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 214-227: The bulk-delete loop using selectedRowIds and
updateReceipt is brittle: a rejection aborts the loop and skips cleanup. Change
it to perform updates resiliently (e.g., use Promise.allSettled on
selectedRowIds.map(id => updateReceipt(...)) or wrap each await updateReceipt
call in a try/catch) so failures don’t stop processing other ids; collect/log
failed ids/errors, but always run the cleanup steps
(tableRef.current?.api?.resetRowSelection(), setSelectedRowsCount(0),
setSelectedRowIds([]), tableRef.current?.api?.setPagination(...), await
tableRef.current?.api?.refetchData()) in a finally block so selection,
pagination and refetch happen regardless of partial failures.

---

Nitpick comments:
In `@apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx`:
- Around line 1-24: The import from '`@app/condo/schema`' (e.g., BillingReceipt,
BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType) is currently
in the wrong group; move that whole import statement into the internal import
group so it appears after external and `@open-condo/`* imports in
ReceiptsTable.tsx, and ensure imports are alphabetized case-insensitively within
each group (adjust surrounding imports such as bridge, useApolloClient, useIntl,
useOrganization, `@open-condo/ui` imports and the moved '`@app/condo/schema`' line
to maintain the repo's import-order rule).

In `@apps/condo/domains/billing/schema/BillingReceipt.test.js`:
- Around line 385-392: Add a negative test next to the current success path that
verifies batch rejection when receipts span organizations: call
updateTestBillingReceipts with billingReceiptsImporter using one id that belongs
to the test org (e.g., receiptToDelete.id) and one id that belongs to a
different organization (create or reuse a foreign receipt fixture like
foreignReceipt.id), then assert the call fails (throws or returns an error) and
that deletedReceipts was not updated; target the batch validation branch
introduced by the access helper by referencing updateTestBillingReceipts,
billingReceiptsImporter, receiptToDelete and a foreign receipt identifier to pin
the mixed-organization denial behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6bcf48e2-4f01-4b7f-8617-1dd2fd2420f4

📥 Commits

Reviewing files that changed from the base of the PR and between a6cc592 and ba77458.

📒 Files selected for processing (4)
  • apps/condo/domains/billing/access/BillingReceipt.js
  • apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx
  • apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx
  • apps/condo/domains/billing/schema/BillingReceipt.test.js

Comment thread apps/condo/domains/billing/access/BillingReceipt.js
@dkoviazin dkoviazin force-pushed the feat/condo/DOMA-13196/upgrade_billing_receipt_table branch from c100926 to 680a0e1 Compare May 21, 2026 14:04
@dkoviazin dkoviazin added ✋🙂 Review please Comments are resolved, take a look, please 😎 Cool and removed 🔬 WIP Not intended to be merged right now, it is a work in progress labels May 21, 2026
Comment thread apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx Outdated
Comment thread apps/condo/domains/billing/gql.js
Comment thread apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx Outdated
Copy link
Copy Markdown
Member

@abshnko abshnko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well done 🔥

Comment thread apps/condo/domains/billing/hooks/useReceiptTableColumns.ts
Comment thread apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx
@dkoviazin dkoviazin force-pushed the feat/condo/DOMA-13196/upgrade_billing_receipt_table branch from f7990f3 to eb127b3 Compare May 29, 2026 10:43
@dkoviazin dkoviazin force-pushed the feat/condo/DOMA-13196/upgrade_billing_receipt_table branch from eb127b3 to 0f9186e Compare June 4, 2026 10:29
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 4, 2026

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

Labels

😎 Cool ✋🙂 Review please Comments are resolved, take a look, please

Development

Successfully merging this pull request may close these issues.

4 participants