Skip to content

Commit 79286b5

Browse files
committed
feat(condo): DOMA-13196 review fixes
1 parent 7a0fde8 commit 79286b5

4 files changed

Lines changed: 62 additions & 43 deletions

File tree

apps/condo/domains/billing/access/BillingReceipt.js

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,47 +65,54 @@ async function canReadSensitiveBillingReceiptData ({ authentication: { item: use
6565
return user.type !== RESIDENT
6666
}
6767

68-
const SOFT_DELETE_ALLOWED_UPDATE_FIELDS = ['dv', 'sender', 'deletedAt']
68+
const SOFT_DELETE_ALLOWED_UPDATE_FIELDS = new Set(['dv', 'sender', 'deletedAt'])
6969

7070
const isSoftDeleteInput = (data) => {
71-
if (typeof data !== 'object') {
72-
return false
73-
}
74-
const keys = Object.keys(data?.data || data)
75-
return keys.length === 3 && keys.every(key => SOFT_DELETE_ALLOWED_UPDATE_FIELDS.includes(key))
71+
const target = data?.data || data
72+
if (typeof target !== 'object' || target === null) return false
73+
74+
return Object.hasOwn(target, 'deletedAt') &&
75+
Object.keys(target).every(key => SOFT_DELETE_ALLOWED_UPDATE_FIELDS.has(key))
7676
}
7777

78-
const isSoftDeleteUpdateRequest = (originalInput) => {
78+
function isSoftDeleteUpdateRequest (originalInput) {
7979
if (Array.isArray(originalInput)) {
8080
return originalInput.every((itemInput) => isSoftDeleteInput(itemInput))
8181
}
8282
return isSoftDeleteInput(originalInput)
8383
}
8484

85+
async function getOrganizationIdsFromReceiptIds (ids) {
86+
if (!ids.length) return []
87+
const receipts = await find('BillingReceipt', { id_in: ids, deletedAt: null })
88+
// some receipts were already deleted
89+
if (receipts.length !== ids.length) return []
90+
const contextIds = Array.from(new Set(receipts.map(({ context }) => context)))
91+
const contexts = await find('BillingIntegrationOrganizationContext', {
92+
id_in: contextIds,
93+
deletedAt: null,
94+
organization: { deletedAt: null },
95+
})
96+
if (!contexts.length || contexts.length !== contextIds.length) {
97+
return []
98+
}
99+
return Array.from(new Set(contexts.map(({ organization }) => organization)))
100+
}
101+
85102
async function canManageBillingReceipts (args) {
86103
const { authentication: { item: user }, operation, context, itemId, itemIds, originalInput } = args
87104
if (!user) return throwAuthenticationError()
88105
if (user.deletedAt) return false
89106
if (user.isAdmin) return true
90-
if (user.isSupport) return false
91107
if (user.type === STAFF && operation === 'update') {
92108
if (!isSoftDeleteUpdateRequest(originalInput)) {
93109
return false
94110
}
95-
const idsToCheck = itemIds?.length ? itemIds : [itemId]
96-
if (!idsToCheck.length) return false
97-
const receipts = await find('BillingReceipt', { id_in: idsToCheck, deletedAt: null })
98-
// some receipts were already deleted
99-
if (receipts.length !== idsToCheck.length) return false
100-
const contextIds = Array.from(new Set(receipts.map(({ context }) => context)))
101-
const contexts = await find('BillingIntegrationOrganizationContext', {
102-
id_in: contextIds,
103-
deletedAt: null,
104-
})
105-
if (!contexts.length) {
111+
const receiptIds = itemIds?.length ? itemIds : [itemId]
112+
const organizationIds = await getOrganizationIdsFromReceiptIds(receiptIds)
113+
if (!organizationIds.length) {
106114
return false
107115
}
108-
const organizationIds = Array.from(new Set(contexts.map(({ organization }) => organization)))
109116
return checkPermissionsInEmployedOrganizations(context, user, organizationIds, 'canImportBillingReceipts')
110117
}
111118
return canManageBillingEntityWithContext(args)

apps/condo/domains/billing/components/BillingPageContent/ReceiptsTable.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BillingReceipt as BillingReceiptType, BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType, UserTypeType } from '@app/condo/schema'
1+
import { BillingReceipt as BillingReceiptType, BillingReceiptWhereInput, SortBillingReceiptsBy, TourStepTypeType } from '@app/condo/schema'
22
import { Col, Row, Space, Typography, type RowProps } from 'antd'
33
import dayjs, { Dayjs } from 'dayjs'
44
import get from 'lodash/get'
@@ -9,7 +9,6 @@ import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState
99

1010
import bridge from '@open-condo/bridge'
1111
import { useApolloClient } from '@open-condo/next/apollo'
12-
import { useAuth } from '@open-condo/next/auth'
1312
import { useIntl } from '@open-condo/next/intl'
1413
import { useOrganization } from '@open-condo/next/organization'
1514
import {
@@ -76,6 +75,7 @@ export const ReceiptsTable: React.FC = () => {
7675
const tableRef = useRef<TableRef | null>(null)
7776
const [search, handleSearchChange, setSearch] = useTableSearch(tableRef)
7877
const [selectedRowsCount, setSelectedRowsCount] = useState<number>(0)
78+
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([])
7979
const [loadingError, setLoadingError] = useState<boolean>(false)
8080
const [period, setPeriod] = useState<Dayjs | null>(() => reportPeriod ? dayjs(reportPeriod, 'YYYY-MM-DD') : null)
8181

@@ -211,20 +211,20 @@ export const ReceiptsTable: React.FC = () => {
211211
}, [apolloClient, contextIds, filtersToWhere, sortersToSortBy, updateStepIfNotCompleted])
212212

213213
const softDeleteSelectedReceipts = useCallback(async () => {
214-
const selectedRows = tableRef.current?.api?.getRowSelection() || []
215-
if (!selectedRows.length) return
214+
if (!selectedRowIds.length) return
216215

217216
const deletedAt = new Date().toISOString()
218217

219-
for (const id of selectedRows) {
218+
for (const id of selectedRowIds) {
220219
await updateReceipt({ deletedAt }, { id })
221220
}
222221

223222
tableRef.current?.api?.resetRowSelection()
224223
setSelectedRowsCount(0)
224+
setSelectedRowIds([])
225225
tableRef.current?.api?.setPagination({ startRow: 0, endRow: DEFAULT_PAGE_SIZE })
226226
await tableRef.current?.api?.refetchData()
227-
}, [updateReceipt])
227+
}, [selectedRowIds, updateReceipt])
228228

229229
const selectedReceiptsActionBarButtons: ActionBarProps['actions'] = useMemo(() => [
230230
<DeleteButtonWithConfirmModal
@@ -244,6 +244,7 @@ export const ReceiptsTable: React.FC = () => {
244244
onClick={() => {
245245
tableRef.current?.api?.resetRowSelection()
246246
setSelectedRowsCount(0)
247+
setSelectedRowIds([])
247248
}}
248249
>
249250
{CancelSelectionMessage}
@@ -260,19 +261,21 @@ export const ReceiptsTable: React.FC = () => {
260261
enableRowSelection: canManageReceipts,
261262
onRowSelectionChange: (rowSelectionState: RowSelectionState) => {
262263
setSelectedRowsCount(rowSelectionState.length)
264+
setSelectedRowIds(rowSelectionState)
263265
},
264266
}), [canManageReceipts])
265267

266268
const getRowId = useCallback((row: BillingReceiptType) => row.id, [])
267269
const onTableReady = useCallback((nextTableRef: TableRef) => {
268270
const tableSearch = nextTableRef.api.getGlobalFilter()
269271
setSearch(String(tableSearch || ''))
270-
setSelectedRowsCount(nextTableRef.api.getRowSelection().length)
272+
setSelectedRowsCount(initialTableState.rowSelectionState.length)
273+
setSelectedRowIds(initialTableState.rowSelectionState)
271274

272275
const tablePeriod = get(nextTableRef.api.getFilterState(), 'period')
273276
const nextPeriod = tablePeriod ? dayjs(String(tablePeriod), 'YYYY-MM-DD') : (reportPeriod ? dayjs(reportPeriod, 'YYYY-MM-DD') : null)
274277
setPeriod(nextPeriod)
275-
}, [reportPeriod, setSearch])
278+
}, [initialTableState.rowSelectionState, reportPeriod, setSearch])
276279

277280
useEffect(() => {
278281
const handleRedirect = async (event) => {
@@ -304,19 +307,27 @@ export const ReceiptsTable: React.FC = () => {
304307
)
305308
}, [onPeriodChange, period])
306309

307-
if (loadingError) {
308-
return (
309-
<BasicEmptyListView>
310-
<Typography.Title level={4}>
311-
{LoadingErrorMessage}
312-
</Typography.Title>
313-
</BasicEmptyListView>
314-
)
315-
}
316-
317310
return (
318311
<>
319312
<Row gutter={ITEMS_GUTTER}>
313+
{loadingError && (
314+
<Col span={24}>
315+
<BasicEmptyListView>
316+
<Typography.Title level={4}>
317+
{LoadingErrorMessage}
318+
</Typography.Title>
319+
<Button
320+
type='secondary'
321+
onClick={async () => {
322+
setLoadingError(false)
323+
await tableRef.current?.api?.refetchData()
324+
}}
325+
>
326+
Retry
327+
</Button>
328+
</BasicEmptyListView>
329+
</Col>
330+
)}
320331
<Col span={24}>
321332
<TableFiltersContainer>
322333
<Row gutter={FILTERS_GUTTER}>

apps/condo/domains/billing/hooks/useReceiptTableFilters.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ const searchBillingPropertyByContextIds = (contextIds: string[]): ISearchInputPr
4545
if (!contextIds?.length) return []
4646

4747
const where = {
48-
context: { id_in: contextIds },
4948
...!isEmpty(searchText) ? { address_contains_i: searchText } : {},
5049
...query,
50+
context: { id_in: contextIds },
5151
}
5252
const sortBy = ['address_ASC']
5353

apps/condo/domains/billing/schema/BillingReceipt.test.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -382,13 +382,14 @@ describe('BillingReceipt', () => {
382382
test('Employee with canImportBillingReceipts can delete receipts', async () => {
383383
const [receiptToDelete] = await createTestBillingReceipt(admin, context, property, account)
384384
const [oneMoreReceiptToDelete] = await createTestBillingReceipt(admin, context, property, account)
385-
const [deletedReceipt] = await updateTestBillingReceipts(billingReceiptsImporter, [
385+
const [deletedReceipts] = await updateTestBillingReceipts(billingReceiptsImporter, [
386386
{ id: receiptToDelete.id, data: { deletedAt: new Date().toISOString() } },
387387
{ id: oneMoreReceiptToDelete.id, data: { deletedAt: new Date().toISOString() } },
388388
])
389-
console.error(deletedReceipt)
390-
expect(deletedReceipt).toBeDefined()
391-
expect(deletedReceipt.deletedAt).not.toBeNull()
389+
expect(deletedReceipts).toEqual([
390+
expect.objectContaining({ deletedAt: expect.any(String) }),
391+
expect.objectContaining({ deletedAt: expect.any(String) }),
392+
])
392393
})
393394
test('Other users cannot', async () => {
394395
await expectToThrowAccessDeniedErrorToObjects(async () => {

0 commit comments

Comments
 (0)