diff --git a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx index 02c64a0242..511ef8fac1 100644 --- a/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx +++ b/apps/backoffice-v2/src/common/components/atoms/MultiSelect/MultiSelect.tsx @@ -1,4 +1,5 @@ -import { ReactNode, useCallback, useState } from 'react'; +import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'; +import { ReactNode, useCallback } from 'react'; import { Badge, Button, @@ -14,7 +15,7 @@ import { PopoverContent, PopoverTrigger, } from '@ballerine/ui'; -import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'; + import { Separator } from '@/common/components/atoms/Separator/Separator'; interface IMultiSelectProps< @@ -29,6 +30,19 @@ interface IMultiSelectProps< onSelect: (value: Array) => void; onClearSelect: () => void; options: TOption[]; + props?: { + content?: { + className?: string; + }; + trigger?: { + leftIcon?: JSX.Element; + rightIcon?: JSX.Element; + className?: string; + title?: { + className?: string; + }; + }; + }; } export const MultiSelect = < @@ -39,13 +53,12 @@ export const MultiSelect = < }, >({ title, - selectedValues, + selectedValues: selected, onSelect, onClearSelect, options, + props, }: IMultiSelectProps) => { - const [selected, setSelected] = useState(selectedValues); - const onSelectChange = useCallback( (value: TOption['value']) => { const isSelected = selected.some(selectedValue => selectedValue === value); @@ -53,18 +66,23 @@ export const MultiSelect = < ? selected.filter(selectedValue => selectedValue !== value) : [...selected, value]; - setSelected(nextSelected); onSelect(nextSelected); }, [onSelect, selected], ); + const TriggerLeftIcon = props?.trigger?.leftIcon ?? ; + return ( - - - + + (value.includes(search) ? 1 : 0)}> No results found. @@ -104,7 +123,11 @@ export const MultiSelect = < const isSelected = selected.some(value => value === option.value); return ( - onSelectChange(option.value)}> + onSelectChange(option.value)} + className={`cursor-pointer`} + >
{ - onClearSelect(); - setSelected([]); - }} - className="justify-center text-center" + onSelect={onClearSelect} + className="cursor-pointer justify-center text-center" > Clear filters diff --git a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx index c815881bb2..9cc27f0ebf 100644 --- a/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/DateRangePicker/DateRangePicker.tsx @@ -8,10 +8,16 @@ import { Calendar } from '../../organisms/Calendar/Calendar'; type TDateRangePickerProps = { onChange: NonNullable['onSelect']>; value: NonNullable['selected']>; + placeholder?: string; className?: ComponentProps<'div'>['className']; }; -export const DateRangePicker = ({ onChange, value, className }: TDateRangePickerProps) => { +export const DateRangePicker = ({ + onChange, + value, + placeholder, + className, +}: TDateRangePickerProps) => { return (
@@ -19,18 +25,18 @@ export const DateRangePicker = ({ onChange, value, className }: TDateRangePicker diff --git a/apps/backoffice-v2/src/common/components/molecules/Search/Search.tsx b/apps/backoffice-v2/src/common/components/molecules/Search/Search.tsx index 527e8a94fd..b7fab4c87a 100644 --- a/apps/backoffice-v2/src/common/components/molecules/Search/Search.tsx +++ b/apps/backoffice-v2/src/common/components/molecules/Search/Search.tsx @@ -3,18 +3,19 @@ import { FunctionComponent } from 'react'; export const Search: FunctionComponent<{ value: string; + placeholder?: string; onChange: (search: string) => void; -}> = ({ value, onChange }) => { +}> = ({ value, placeholder, onChange }) => { return (
-
+
onChange(e.target.value)} /> diff --git a/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx b/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx index 821ef713e5..5ce6126bea 100644 --- a/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx +++ b/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx @@ -61,4 +61,5 @@ export const Calendar = ({ /> ); }; + Calendar.displayName = 'Calendar'; diff --git a/apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx b/apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx index 86deef5c0f..bd5a3c01e8 100644 --- a/apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx +++ b/apps/backoffice-v2/src/common/hooks/useSearch/useSearch.tsx @@ -3,16 +3,8 @@ import { useDebounce } from '../useDebounce/useDebounce'; import { useSerializedSearchParams } from '@/common/hooks/useSerializedSearchParams/useSerializedSearchParams'; import { useIsMounted } from '@/common/hooks/useIsMounted/useIsMounted'; -export const useSearch = ( - { - initialSearch = '', - }: { - initialSearch?: string; - } = { - initialSearch: '', - }, -) => { - const [{ search = initialSearch }, setSearchParams] = useSerializedSearchParams(); +export const useSearch = () => { + const [{ search }, setSearchParams] = useSerializedSearchParams(); const [_search, setSearch] = useState(search); const debouncedSearch = useDebounce(_search, 240); const onSearchChange = useCallback((search: string) => { @@ -32,7 +24,8 @@ export const useSearch = ( }, [debouncedSearch]); return { - search: _search, + search: _search as string, + debouncedSearch: debouncedSearch as string, onSearch: onSearchChange, }; }; diff --git a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts index b498c67f4a..a5197c0d62 100644 --- a/apps/backoffice-v2/src/domains/business-reports/fetchers.ts +++ b/apps/backoffice-v2/src/domains/business-reports/fetchers.ts @@ -1,10 +1,13 @@ +import qs from 'qs'; import { z } from 'zod'; -import { apiClient } from '@/common/api-client/api-client'; +import { t } from 'i18next'; +import { toast } from 'sonner'; +import { UnknownRecord } from 'type-fest'; + import { Method } from '@/common/enums'; +import { apiClient } from '@/common/api-client/api-client'; +import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas'; import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; -import qs from 'qs'; -import { toast } from 'sonner'; -import { t } from 'i18next'; import { MERCHANT_REPORT_STATUSES, MERCHANT_REPORT_STATUSES_MAP, @@ -13,7 +16,6 @@ import { MerchantReportType, MerchantReportVersion, } from '@/domains/business-reports/constants'; -import { UnknownRecord } from 'type-fest'; export const BusinessReportSchema = z .object({ @@ -84,24 +86,20 @@ export const fetchLatestBusinessReport = async ({ return handleZodError(error, data); }; -export const fetchBusinessReports = async ({ - reportType, - ...params -}: { - reportType: MerchantReportType; +export const fetchBusinessReports = async (params: { + reportType?: MerchantReportType; + riskLevels: TRiskLevel[]; + statuses: TReportStatusValue[]; + findings: string[]; + from?: string; + to?: string; page: { number: number; size: number; }; orderBy: string; }) => { - const queryParams = qs.stringify( - { - ...params, - type: reportType, - }, - { encode: false }, - ); + const queryParams = qs.stringify(params, { encode: false }); const [data, error] = await apiClient({ endpoint: `../external/business-reports/?${queryParams}`, diff --git a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx index 35ccbad732..78480b4078 100644 --- a/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx +++ b/apps/backoffice-v2/src/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery.tsx @@ -1,8 +1,9 @@ -import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated'; import { useQuery } from '@tanstack/react-query'; -import { businessReportsQueryKey } from '@/domains/business-reports/query-keys'; -import { isString } from '@/common/utils/is-string/is-string'; + import { MerchantReportType } from '@/domains/business-reports/constants'; +import { businessReportsQueryKey } from '@/domains/business-reports/query-keys'; +import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas'; +import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated'; export const useBusinessReportsQuery = ({ reportType, @@ -11,26 +12,41 @@ export const useBusinessReportsQuery = ({ pageSize, sortBy, sortDir, + riskLevels, + statuses, + findings, + from, + to, }: { - reportType: MerchantReportType; + reportType?: MerchantReportType; search: string; page: number; pageSize: number; sortBy: string; sortDir: string; + riskLevels: TRiskLevel[]; + statuses: TReportStatusValue[]; + findings: string[]; + from?: string; + to?: string; }) => { const isAuthenticated = useIsAuthenticated(); return useQuery({ - ...businessReportsQueryKey.list({ reportType, search, page, pageSize, sortBy, sortDir }), - enabled: - isAuthenticated && - isString(reportType) && - !!reportType && - !!sortBy && - !!sortDir && - !!page && - !!pageSize, + ...businessReportsQueryKey.list({ + reportType, + search, + page, + pageSize, + sortBy, + sortDir, + riskLevels, + statuses, + findings, + from, + to, + }), + enabled: isAuthenticated && !!sortBy && !!sortDir && !!page && !!pageSize, staleTime: 100_000, refetchInterval: 1_000_000, }); diff --git a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts index f7c417ee52..b9aba396b4 100644 --- a/apps/backoffice-v2/src/domains/business-reports/query-keys.ts +++ b/apps/backoffice-v2/src/domains/business-reports/query-keys.ts @@ -6,6 +6,7 @@ import { fetchLatestBusinessReport, } from '@/domains/business-reports/fetchers'; import { MerchantReportType } from '@/domains/business-reports/constants'; +import { TReportStatusValue, TRiskLevel } from '@/pages/MerchantMonitoring/schemas'; export const businessReportsQueryKey = createQueryKeys('business-reports', { list: ({ @@ -15,12 +16,17 @@ export const businessReportsQueryKey = createQueryKeys('business-reports', { sortDir, ...params }: { - reportType: MerchantReportType; + reportType?: MerchantReportType; search: string; page: number; pageSize: number; sortBy: string; sortDir: string; + riskLevels: TRiskLevel[]; + statuses: TReportStatusValue[]; + findings: string[]; + from?: string; + to?: string; }) => ({ queryKey: [{ page, pageSize, sortBy, sortDir, ...params }], queryFn: () => { diff --git a/apps/backoffice-v2/src/pages/Entities/hooks/useEntities/useEntities.tsx b/apps/backoffice-v2/src/pages/Entities/hooks/useEntities/useEntities.tsx index b9e53e3a63..6cf8ead2fc 100644 --- a/apps/backoffice-v2/src/pages/Entities/hooks/useEntities/useEntities.tsx +++ b/apps/backoffice-v2/src/pages/Entities/hooks/useEntities/useEntities.tsx @@ -9,7 +9,8 @@ import { useWorkflowsQuery } from '../../../../domains/workflows/hooks/queries/u import { usePagination } from '@/common/hooks/usePagination/usePagination'; export const useEntities = () => { - const [{ filterId, filter, sortBy, sortDir, page, pageSize, search }, setSearchParams] = + const { search, onSearch } = useSearch(); + const [{ filterId, filter, sortBy, sortDir, page, pageSize }, setSearchParams] = useSearchParamsByEntity(); const { data, isLoading } = useWorkflowsQuery({ @@ -24,7 +25,6 @@ export const useEntities = () => { const cases = data?.data; const totalPages = data?.meta?.totalPages ?? 0; const entity = useEntityType(); - const { onSearch, search: searchValue } = useSearch(); const onSortDirToggle = useCallback(() => { setSearchParams({ @@ -90,7 +90,7 @@ export const useEntities = () => { onFilter: onFilterChange, onSortBy: onSortByChange, onSortDirToggle, - search: searchValue, + search, cases, caseCount: data?.meta?.totalItems || 0, isLoading, diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx index 77ef7582a3..f6c92e5773 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/MerchantMonitoring.page.tsx @@ -5,20 +5,32 @@ import { useMerchantMonitoringLogic } from '@/pages/MerchantMonitoring/hooks/use import { NoBusinessReports } from '@/pages/MerchantMonitoring/components/NoBusinessReports/NoBusinessReports'; import { MerchantMonitoringTable } from '@/pages/MerchantMonitoring/components/MerchantMonitoringTable/MerchantMonitoringTable'; import { buttonVariants } from '@/common/components/atoms/Button/Button'; -import { Plus, Table2 } from 'lucide-react'; +import { Loader2, Plus, SlidersHorizontal, Table2 } from 'lucide-react'; import { Link } from 'react-router-dom'; import { Search } from '@/common/components/molecules/Search'; -import { Skeleton } from '@ballerine/ui'; +import { + Button, + DropdownMenuTrigger, + DropdownMenu, + Skeleton, + DropdownMenuContent, + DropdownMenuCheckboxItem, + Badge, +} from '@ballerine/ui'; import { TooltipProvider } from '@/common/components/atoms/Tooltip/Tooltip.Provider'; import { Tooltip } from '@/common/components/atoms/Tooltip/Tooltip'; import { TooltipTrigger } from '@/common/components/atoms/Tooltip/Tooltip.Trigger'; import { TooltipContent } from '@/common/components/atoms/Tooltip/Tooltip.Content'; import { t } from 'i18next'; +import { MultiSelect } from '@/common/components/atoms/MultiSelect/MultiSelect'; +import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; +import { Separator } from '@/common/components/atoms/Separator/Separator'; export const MerchantMonitoring: FunctionComponent = () => { const { businessReports, isLoadingBusinessReports, + isLoadingFindings, search, onSearch, totalPages, @@ -28,9 +40,25 @@ export const MerchantMonitoring: FunctionComponent = () => { onLastPage, onPaginate, isLastPage, + dates, + onDatesChange, locale, createBusinessReport, createBusinessReportBatch, + reportType, + onReportTypeChange, + onClearAllFilters, + REPORT_TYPE_TO_DISPLAY_TEXT, + FINDINGS_FILTER, + RISK_LEVEL_FILTER, + STATUS_LEVEL_FILTER, + handleFilterChange, + handleFilterClear, + riskLevels, + statuses, + findings, + multiselectProps, + isClearAllButtonVisible, } = useMerchantMonitoringLogic(); return ( @@ -90,12 +118,98 @@ export const MerchantMonitoring: FunctionComponent = () => {
-
+
+ + + + + + + {Object.entries(REPORT_TYPE_TO_DISPLAY_TEXT).map(([type, displayText]) => ( + + onReportTypeChange(type as keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) + } + > + {displayText} + + ))} + + + + + + {isClearAllButtonVisible && ( + + )}
-
- {isNonEmptyArray(businessReports) && } - {Array.isArray(businessReports) && !businessReports.length && !isLoadingBusinessReports && ( +
+ {isLoadingBusinessReports && ( +
+ +
+ )} + {!isLoadingBusinessReports && isNonEmptyArray(businessReports) && ( + + )} + {!isLoadingBusinessReports && Array.isArray(businessReports) && !businessReports.length && ( )}
diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx index 3ba4b15a7b..c89044b370 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/columns.tsx @@ -26,7 +26,12 @@ const columnHelper = createColumnHelper(); const SCAN_TYPES = { ONBOARDING: 'Onboarding', MONITORING: 'Monitoring', -}; +} as const; + +const REPORT_STATUS_TO_DISPLAY_STATUS = { + [MERCHANT_REPORT_STATUSES_MAP.completed]: 'Ready for Review', + [MERCHANT_REPORT_STATUSES_MAP['quality-control']]: 'Quality Control', +} as const; const REPORT_TYPE_TO_SCAN_TYPE = { [MERCHANT_REPORT_TYPES_MAP.MERCHANT_REPORT_T1]: SCAN_TYPES.ONBOARDING, @@ -83,7 +88,7 @@ export const columns = [ const id = info.getValue(); return ( -
+
{id} @@ -102,7 +107,7 @@ export const columns = [ const id = info.getValue(); return ( -
+
{id} @@ -152,24 +157,24 @@ export const columns = [
); }, - header: 'Risk Score', + header: 'Risk Level', }), columnHelper.accessor('status', { cell: info => { const status = info.getValue(); - const statusToDisplayStatus = { - [MERCHANT_REPORT_STATUSES_MAP.completed]: 'Manual Review', - [MERCHANT_REPORT_STATUSES_MAP['quality-control']]: 'Quality Control', - } as const; return ( - {titleCase(statusToDisplayStatus[status as keyof typeof statusToDisplayStatus] ?? status)} + {titleCase( + REPORT_STATUS_TO_DISPLAY_STATUS[ + status as keyof typeof REPORT_STATUS_TO_DISPLAY_STATUS + ] ?? status, + )} ); }, diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/hooks/useMerchantMonitoringTableLogic/useMerchantMonitoringTableLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/hooks/useMerchantMonitoringTableLogic/useMerchantMonitoringTableLogic.tsx index efffe3fcf6..fbd3f92215 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/hooks/useMerchantMonitoringTableLogic/useMerchantMonitoringTableLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/components/MerchantMonitoringTable/hooks/useMerchantMonitoringTableLogic/useMerchantMonitoringTableLogic.tsx @@ -17,7 +17,7 @@ export const useMerchantMonitoringTableLogic = () => { return ( {children} diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/fetchers.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/fetchers.ts new file mode 100644 index 0000000000..59f51d961a --- /dev/null +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/fetchers.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +import { Method } from '@/common/enums'; +import { apiClient } from '@/common/api-client/api-client'; +import { FindingsSchema } from '@/pages/MerchantMonitoring/schemas'; +import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; + +export const fetchFindings = async () => { + const [data, error] = await apiClient({ + endpoint: `../external/business-reports/findings`, + method: Method.GET, + schema: z.object({ data: FindingsSchema }), + timeout: 300_000, + }); + + return handleZodError(error, data?.data); +}; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts deleted file mode 100644 index 2671b17e1c..0000000000 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/get-merchant-monitoring-search-schema.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validation-schemas'; -import { z } from 'zod'; -import { TBusinessReport } from '@/domains/business-reports/fetchers'; -import { BooleanishRecordSchema } from '@ballerine/ui'; - -export const getMerchantMonitoringSearchSchema = () => - BaseSearchSchema.extend({ - sortBy: z - .enum([ - 'createdAt', - 'updatedAt', - 'business.website', - 'business.companyName', - 'business.country', - 'riskScore', - 'status', - ] as const satisfies ReadonlyArray< - | Extract< - keyof NonNullable, - 'createdAt' | 'updatedAt' | 'riskScore' | 'status' - > - | 'business.website' - | 'business.companyName' - | 'business.country' - >) - .catch('createdAt'), - selected: BooleanishRecordSchema.optional(), - }); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useFindings/useFindings.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useFindings/useFindings.ts new file mode 100644 index 0000000000..7aede30613 --- /dev/null +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useFindings/useFindings.ts @@ -0,0 +1,39 @@ +import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated'; +import { useQuery } from '@tanstack/react-query'; +import { findingsQueryKey } from '@/pages/MerchantMonitoring/query-keys'; +import { FindingsSchema } from '@/pages/MerchantMonitoring/schemas'; + +export const useFindings = () => { + const isAuthenticated = useIsAuthenticated(); + + const { data, isLoading } = useQuery({ + ...findingsQueryKey.list(), + enabled: isAuthenticated, + staleTime: 100_000, + refetchInterval: 1_000_000, + }); + + if (data) { + localStorage.setItem('findings', JSON.stringify(data)); + } + + let findings: Array<{ value: string; label: string }> = []; + const findingsString = localStorage.getItem('findings'); + + try { + const findingsObject = findingsString ? JSON.parse(findingsString) : []; + + const parsedFindings = FindingsSchema.safeParse(findingsObject); + + if (parsedFindings.success) { + findings = parsedFindings.data; + } + } catch (error) { + findings = []; + } + + return { + findings, + isLoading, + }; +}; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx index b0392aecdf..6c67c37162 100644 --- a/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/hooks/useMerchantMonitoringLogic/useMerchantMonitoringLogic.tsx @@ -1,40 +1,142 @@ -import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; -import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams'; -import { getMerchantMonitoringSearchSchema } from '@/pages/MerchantMonitoring/get-merchant-monitoring-search-schema'; -import { usePagination } from '@/common/hooks/usePagination/usePagination'; +import dayjs from 'dayjs'; +import { SlidersHorizontal } from 'lucide-react'; +import React, { useCallback, ComponentProps, useMemo } from 'react'; + import { useLocale } from '@/common/hooks/useLocale/useLocale'; -import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; import { useSearch } from '@/common/hooks/useSearch/useSearch'; +import { usePagination } from '@/common/hooks/usePagination/usePagination'; +import { useFindings } from '@/pages/MerchantMonitoring/hooks/useFindings/useFindings'; +import { useZodSearchParams } from '@/common/hooks/useZodSearchParams/useZodSearchParams'; +import { DateRangePicker } from '@/common/components/molecules/DateRangePicker/DateRangePicker'; +import { useCustomerQuery } from '@/domains/customer/hooks/queries/useCustomerQuery/useCustomerQuery'; +import { useBusinessReportsQuery } from '@/domains/business-reports/hooks/queries/useBusinessReportsQuery/useBusinessReportsQuery'; +import { + DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE, + MerchantMonitoringSearchSchema, + REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTER, + STATUS_LEVEL_FILTER, + REPORT_STATUS_LABEL_TO_VALUE_MAP, +} from '@/pages/MerchantMonitoring/schemas'; export const useMerchantMonitoringLogic = () => { const locale = useLocale(); const { data: customer } = useCustomerQuery(); - const MerchantMonitoringSearchSchema = getMerchantMonitoringSearchSchema(); - - const [{ page, pageSize, sortBy, sortDir, search: searchParamValue }] = useZodSearchParams( - MerchantMonitoringSearchSchema, - ); + const { search, debouncedSearch, onSearch } = useSearch(); - const { search: searchTerm, onSearch } = useSearch({ - initialSearch: searchParamValue, - }); + const [ + { page, pageSize, sortBy, sortDir, reportType, riskLevels, statuses, from, to, findings }, + setSearchParams, + ] = useZodSearchParams(MerchantMonitoringSearchSchema); - const search = searchTerm as string; + const { findings: findingsOptions, isLoading: isLoadingFindings } = useFindings(); const { data, isLoading: isLoadingBusinessReports } = useBusinessReportsQuery({ - reportType: 'MERCHANT_REPORT_T1', - search, + ...(reportType !== 'All' && { + reportType: + DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE[ + reportType as keyof typeof DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE + ], + }), + search: debouncedSearch, page, pageSize, sortBy, sortDir, + findings, + riskLevels: riskLevels ?? [], + statuses: statuses + ?.map(status => REPORT_STATUS_LABEL_TO_VALUE_MAP[status]) + .flatMap(status => (status === 'quality-control' ? ['quality-control', 'failed'] : [status])), + from, + to: to ? dayjs(to).add(1, 'day').format('YYYY-MM-DD') : undefined, }); + const isClearAllButtonVisible = useMemo( + () => + !!( + search !== '' || + from || + to || + reportType !== 'All' || + statuses.length || + riskLevels.length || + findings.length + ), + [findings.length, from, reportType, riskLevels.length, search, statuses.length, to], + ); + + const onReportTypeChange = (reportType: keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT) => { + setSearchParams({ reportType: REPORT_TYPE_TO_DISPLAY_TEXT[reportType] }); + }; + + const handleFilterChange = useCallback( + (filterKey: string) => (selected: unknown) => { + setSearchParams({ + [filterKey]: Array.isArray(selected) ? selected : [selected], + page: '1', + }); + }, + [setSearchParams], + ); + + const handleFilterClear = useCallback( + (filterKey: string) => () => { + setSearchParams({ + [filterKey]: [], + page: '1', + }); + }, + [setSearchParams], + ); + + const onClearAllFilters = useCallback(() => { + setSearchParams({ + reportType: 'All', + riskLevels: [], + statuses: [], + findings: [], + from: undefined, + to: undefined, + page: '1', + }); + + onSearch(''); + }, [onSearch, setSearchParams]); + const { onPaginate, onPrevPage, onNextPage, onLastPage, isLastPage } = usePagination({ totalPages: data?.totalPages ?? 0, }); + const onDatesChange: ComponentProps['onChange'] = range => { + const from = dayjs(range?.from).format('YYYY-MM-DD'); + const to = dayjs(range?.to).format('YYYY-MM-DD'); + + setSearchParams({ from, to }); + }; + + const multiselectProps = useMemo( + () => ({ + trigger: { + leftIcon: , + title: { + className: `font-normal text-sm`, + }, + }, + }), + [], + ); + + const FINDINGS_FILTER = useMemo( + () => ({ + title: 'Findings', + accessor: 'findings', + options: findingsOptions, + }), + [findingsOptions], + ); + return { totalPages: data?.totalPages || 0, totalItems: data?.totalItems || 0, @@ -42,6 +144,8 @@ export const useMerchantMonitoringLogic = () => { createBusinessReportBatch: customer?.features?.createBusinessReportBatch, businessReports: data?.data || [], isLoadingBusinessReports, + isLoadingFindings, + isClearAllButtonVisible, search, onSearch, page, @@ -51,5 +155,20 @@ export const useMerchantMonitoringLogic = () => { onPaginate, isLastPage, locale, + reportType, + onReportTypeChange, + multiselectProps, + REPORT_TYPE_TO_DISPLAY_TEXT, + RISK_LEVEL_FILTER, + STATUS_LEVEL_FILTER, + FINDINGS_FILTER, + handleFilterChange, + handleFilterClear, + riskLevels, + statuses, + findings, + dates: { from, to }, + onDatesChange, + onClearAllFilters, }; }; diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/query-keys.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/query-keys.ts new file mode 100644 index 0000000000..e4c7d03c11 --- /dev/null +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/query-keys.ts @@ -0,0 +1,9 @@ +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import { fetchFindings } from '@/pages/MerchantMonitoring/fetchers'; + +export const findingsQueryKey = createQueryKeys('findings', { + list: () => ({ + queryKey: [{}], + queryFn: fetchFindings, + }), +}); diff --git a/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts new file mode 100644 index 0000000000..04cde41e77 --- /dev/null +++ b/apps/backoffice-v2/src/pages/MerchantMonitoring/schemas.ts @@ -0,0 +1,110 @@ +import { z } from 'zod'; +import { BaseSearchSchema } from '@/common/hooks/useSearchParamsByEntity/validation-schemas'; +import { TBusinessReport } from '@/domains/business-reports/fetchers'; +import { BooleanishRecordSchema } from '@ballerine/ui'; + +export const REPORT_TYPE_TO_DISPLAY_TEXT = { + All: 'All', + MERCHANT_REPORT_T1: 'Onboarding', + ONGOING_MERCHANT_REPORT_T1: 'Monitoring', +} as const; + +export const DISPLAY_TEXT_TO_MERCHANT_REPORT_TYPE = { + Onboarding: 'MERCHANT_REPORT_T1', + Monitoring: 'ONGOING_MERCHANT_REPORT_T1', +} as const; + +export const RISK_LEVELS_MAP = { + low: 'low', + medium: 'medium', + high: 'high', + critical: 'critical', +}; + +export const RISK_LEVELS = [ + RISK_LEVELS_MAP.low, + RISK_LEVELS_MAP.medium, + RISK_LEVELS_MAP.high, + RISK_LEVELS_MAP.critical, +] as const; + +export type TRiskLevel = (typeof RISK_LEVELS)[number]; + +export const RISK_LEVEL_FILTER = { + title: 'Risk Level', + accessor: 'riskLevels', + options: RISK_LEVELS.map(riskLevel => ({ + label: riskLevel.charAt(0).toUpperCase() + riskLevel.slice(1), + value: riskLevel, + })), +}; + +export const REPORT_STATUS_LABELS = ['In Progress', 'Quality Control', 'Ready for Review'] as const; + +export const REPORT_STATUS_LABEL_TO_VALUE_MAP = { + 'In Progress': 'in-progress', + 'Quality Control': 'quality-control', + 'Ready for Review': 'completed', + Failed: 'failed', +} as const; + +export type TReportStatusLabel = (typeof REPORT_STATUS_LABELS)[number]; + +export type TReportStatusValue = + (typeof REPORT_STATUS_LABEL_TO_VALUE_MAP)[keyof typeof REPORT_STATUS_LABEL_TO_VALUE_MAP]; + +export const STATUS_LEVEL_FILTER = { + title: 'Status', + accessor: 'statuses', + options: REPORT_STATUS_LABELS.map(status => ({ + label: status, + value: status, + })), +}; + +export const FindingsSchema = z.array(z.object({ value: z.string(), label: z.string() })); + +export const MerchantMonitoringSearchSchema = BaseSearchSchema.extend({ + sortBy: z + .enum([ + 'createdAt', + 'updatedAt', + 'business.website', + 'business.companyName', + 'business.country', + 'riskScore', + 'status', + 'reportType', + ] as const satisfies ReadonlyArray< + | Extract< + keyof NonNullable, + 'createdAt' | 'updatedAt' | 'riskScore' | 'status' | 'reportType' + > + | 'business.website' + | 'business.companyName' + | 'business.country' + >) + .catch('createdAt'), + selected: BooleanishRecordSchema.optional(), + reportType: z + .enum([ + ...(Object.values(REPORT_TYPE_TO_DISPLAY_TEXT) as [ + (typeof REPORT_TYPE_TO_DISPLAY_TEXT)['All'], + ...Array<(typeof REPORT_TYPE_TO_DISPLAY_TEXT)[keyof typeof REPORT_TYPE_TO_DISPLAY_TEXT]>, + ]), + ]) + .catch('All'), + riskLevels: z + .array(z.enum(RISK_LEVELS.map(riskLevel => riskLevel) as [TRiskLevel, ...TRiskLevel[]])) + .catch([]), + statuses: z + .array( + z.enum( + REPORT_STATUS_LABELS.map(status => status) as [TReportStatusLabel, ...TReportStatusLabel[]], + ), + ) + .catch([]), + findings: z.array(z.string()).catch([]), + from: z.string().date().optional(), + to: z.string().date().optional(), +}); diff --git a/apps/backoffice-v2/src/pages/Profiles/Individuals/hooks/useIndividualsLogic/useIndividualsLogic.tsx b/apps/backoffice-v2/src/pages/Profiles/Individuals/hooks/useIndividualsLogic/useIndividualsLogic.tsx index 7c906ef3d4..e86c7afdfa 100644 --- a/apps/backoffice-v2/src/pages/Profiles/Individuals/hooks/useIndividualsLogic/useIndividualsLogic.tsx +++ b/apps/backoffice-v2/src/pages/Profiles/Individuals/hooks/useIndividualsLogic/useIndividualsLogic.tsx @@ -5,11 +5,11 @@ import { useSearch } from '@/common/hooks/useSearch/useSearch'; import { useIndividualsProfilesQuery } from '@/domains/profiles/hooks/queries/useIndividualsProfilesQuery/useIndividualsProfilesQuery'; export const useIndividualsLogic = () => { - const [{ search: searchValue, filter, page, pageSize, sortBy, sortDir }] = - useZodSearchParams(ProfilesSearchSchema); + const { search, onSearch } = useSearch(); + const [{ filter, page, pageSize, sortBy, sortDir }] = useZodSearchParams(ProfilesSearchSchema); const { data: individualsProfiles, isLoading: isLoadingIndividualsProfiles } = useIndividualsProfilesQuery({ - search: searchValue, + search, filter, page, pageSize, @@ -21,9 +21,6 @@ export const useIndividualsLogic = () => { }); const isLastPage = (individualsProfiles?.length ?? 0) < pageSize || individualsProfiles?.length === 0; - const { search, onSearch } = useSearch({ - initialSearch: searchValue, - }); return { isLoadingIndividualsProfiles, diff --git a/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx b/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx index 9258e26d9d..d01798d4f6 100644 --- a/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx +++ b/apps/backoffice-v2/src/pages/TransactionMonitoringAlerts/components/AlertsFilters/AlertsFilters.tsx @@ -4,7 +4,6 @@ import { MultiSelect } from '@/common/components/atoms/MultiSelect/MultiSelect'; import { useFilter } from '@/common/hooks/useFilter/useFilter'; import { AlertStatuses } from '@/domains/alerts/fetchers'; import { titleCase } from 'string-ts'; -import { keyFactory } from '@/common/utils/key-factory/key-factory'; export const AlertsFilters: FunctionComponent<{ assignees: TUsers; @@ -79,7 +78,7 @@ export const AlertsFilters: FunctionComponent<{
{filters.map(({ title, accessor, options }) => ( { + const { search, onSearch } = useSearch(); const { data: session } = useAuthenticatedUserQuery(); const AlertsSearchSchema = getAlertsSearchSchema(); - const [{ filter, sortBy, sortDir, page, pageSize, search: searchValue }] = - useZodSearchParams(AlertsSearchSchema); + const [{ filter, sortBy, sortDir, page, pageSize }] = useZodSearchParams(AlertsSearchSchema); const { data: alerts, isLoading: isLoadingAlerts } = useAlertsQuery({ filter, page, pageSize, - search: searchValue, + search, sortDir, sortBy, }); @@ -36,9 +36,6 @@ export const useTransactionMonitoringAlertsLogic = () => { totalPages: 0, }); const isLastPage = (alerts?.length ?? 0) < pageSize || alerts?.length === 0; - const { search, onSearch } = useSearch({ - initialSearch: searchValue, - }); return { alerts, diff --git a/apps/kyb-app/src/pages/SignIn/SignIn.tsx b/apps/kyb-app/src/pages/SignIn/SignIn.tsx index 3b9b17d50b..010a01fc29 100644 --- a/apps/kyb-app/src/pages/SignIn/SignIn.tsx +++ b/apps/kyb-app/src/pages/SignIn/SignIn.tsx @@ -38,7 +38,7 @@ export const SignIn = () => {
-

+

Contact {customer?.displayName || 'PayLynk'} for support
example@example.com
(000) 123-4567 diff --git a/packages/react-pdf-toolkit/src/utils/get-risk-score-style.ts b/packages/react-pdf-toolkit/src/utils/get-risk-score-style.ts deleted file mode 100644 index 3bb10aaa61..0000000000 --- a/packages/react-pdf-toolkit/src/utils/get-risk-score-style.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const getRiskScoreStyle = (score: number | null = 0) => { - if (Number(score) <= 39) { - return 'success'; - } - - if (Number(score) <= 69) { - return 'moderate'; - } - - if (Number(score) <= 84) { - return 'warning'; - } - - return 'error'; -}; diff --git a/packages/react-pdf-toolkit/src/utils/index.ts b/packages/react-pdf-toolkit/src/utils/index.ts index 050971ac63..5c077a5dde 100644 --- a/packages/react-pdf-toolkit/src/utils/index.ts +++ b/packages/react-pdf-toolkit/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from './get-risk-score-style'; export * from './is-link'; export * from './merge-styles'; export * from './sanitize-string'; diff --git a/services/workflows-service/docker-compose.db.yml b/services/workflows-service/docker-compose.db.yml index 8ae3428933..b4e1b2ea1a 100644 --- a/services/workflows-service/docker-compose.db.yml +++ b/services/workflows-service/docker-compose.db.yml @@ -9,5 +9,6 @@ services: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres15:/var/lib/postgresql/data + restart: unless-stopped volumes: postgres15: ~ diff --git a/services/workflows-service/src/business-report/business-report-list.dto.ts b/services/workflows-service/src/business-report/business-report-list.dto.ts deleted file mode 100644 index 4f6c216988..0000000000 --- a/services/workflows-service/src/business-report/business-report-list.dto.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsOptional, IsString } from 'class-validator'; -import { PageDto } from '@/common/dto'; -import { z } from 'zod'; -import { BusinessReportDto } from '@/business-report/business-report.dto'; - -export class BusinessReportListRequestParamDto { - @IsOptional() - @IsString() - businessId?: string; - - @IsOptional() - @ApiProperty({ type: String, required: false }) - search?: string; - - @ApiProperty({ type: PageDto }) - page!: PageDto; -} - -export const ListBusinessReportsSchema = z.object({ - search: z.string().optional(), - page: z.object({ - number: z.coerce.number().int().positive(), - size: z.coerce.number().int().positive().max(100), - }), -}); - -export class BusinessReportListResponseDto { - @ApiProperty({ type: Number, example: 20 }) - totalItems!: number; - - @ApiProperty({ type: Number, example: 1 }) - totalPages!: number; - - @ApiProperty({ type: [BusinessReportDto] }) - data!: BusinessReportDto[]; -} diff --git a/services/workflows-service/src/business-report/business-report.controller.external.ts b/services/workflows-service/src/business-report/business-report.controller.external.ts index 4bebc551f3..b42615c099 100644 --- a/services/workflows-service/src/business-report/business-report.controller.external.ts +++ b/services/workflows-service/src/business-report/business-report.controller.external.ts @@ -23,19 +23,21 @@ import { BusinessReportListRequestParamDto, BusinessReportListResponseDto, ListBusinessReportsSchema, -} from '@/business-report/business-report-list.dto'; +} from '@/business-report/dtos/business-report-list.dto'; import { ZodValidationPipe } from '@/common/pipes/zod.pipe'; -import { CreateBusinessReportDto } from '@/business-report/dto/create-business-report.dto'; +import { CreateBusinessReportDto } from '@/business-report/dtos/create-business-report.dto'; import { Business } from '@prisma/client'; -import { BusinessReportDto } from '@/business-report/business-report.dto'; +import { BusinessReportDto } from '@/business-report/dtos/business-report.dto'; import { FileInterceptor } from '@nestjs/platform-express'; import { getDiskStorage } from '@/storage/get-file-storage-manager'; import { fileFilter } from '@/storage/file-filter'; import { RemoveTempFileInterceptor } from '@/common/interceptors/remove-temp-file.interceptor'; -import { CreateBusinessReportBatchBodyDto } from '@/business-report/dto/create-business-report-batch-body.dto'; +import { CreateBusinessReportBatchBodyDto } from '@/business-report/dtos/create-business-report-batch-body.dto'; import type { Response } from 'express'; import { PrismaService } from '@/prisma/prisma.service'; import { AdminAuthGuard } from '@/common/guards/admin-auth.guard'; +import { BusinessReportFindingsListResponseDto } from '@/business-report/dtos/business-report-findings.dto'; +import { MerchantMonitoringClient } from '@/business-report/merchant-monitoring-client'; @ApiBearerAuth() @swagger.ApiTags('Business Reports') @@ -46,7 +48,8 @@ export class BusinessReportControllerExternal { protected readonly logger: AppLoggerService, protected readonly customerService: CustomerService, protected readonly businessService: BusinessService, - private readonly prisma: PrismaService, + private readonly prismaService: PrismaService, + private readonly merchantMonitoringClient: MerchantMonitoringClient, ) {} @common.Get('/latest') @@ -74,7 +77,18 @@ export class BusinessReportControllerExternal { @common.UsePipes(new ZodValidationPipe(ListBusinessReportsSchema, 'query')) async listBusinessReports( @CurrentProject() currentProjectId: TProjectId, - @Query() { businessId, page, search }: BusinessReportListRequestParamDto, + @Query() + { + businessId, + page, + search, + from, + to, + reportType, + riskLevels, + statuses, + findings, + }: BusinessReportListRequestParamDto, ) { const { id: customerId } = await this.customerService.getByProjectId(currentProjectId); @@ -82,12 +96,25 @@ export class BusinessReportControllerExternal { withoutUnpublishedOngoingReports: true, limit: page.size, page: page.number, - customerId: customerId, + customerId, + from, + to, + riskLevels, + statuses, + findings, + ...(reportType ? { reportType } : {}), ...(businessId ? { businessId } : {}), ...(search ? { searchQuery: search } : {}), }); } + @common.Get('/findings') + @swagger.ApiOkResponse({ type: BusinessReportFindingsListResponseDto }) + @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException }) + async listFindings() { + return await this.merchantMonitoringClient.listFindings(); + } + @common.Post() @swagger.ApiOkResponse({}) @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException }) @@ -160,7 +187,7 @@ export class BusinessReportControllerExternal { @swagger.ApiForbiddenResponse({ type: errors.ForbiddenException }) @swagger.ApiExcludeEndpoint() async list() { - return await this.prisma.businessReport.findMany({ + return await this.prismaService.businessReport.findMany({ include: { project: { include: { diff --git a/services/workflows-service/src/business-report/constants.ts b/services/workflows-service/src/business-report/constants.ts index e3f59ae948..c2ad4a2144 100644 --- a/services/workflows-service/src/business-report/constants.ts +++ b/services/workflows-service/src/business-report/constants.ts @@ -10,6 +10,13 @@ export type MerchantReportStatus = keyof typeof MERCHANT_REPORT_STATUSES_MAP; export const MERCHANT_REPORT_STATUSES = Object.values(MERCHANT_REPORT_STATUSES_MAP); +export const MERCHANT_REPORT_RISK_LEVELS = { + low: 'low', + medium: 'medium', + high: 'high', + critical: 'critical', +} as const; + export const MERCHANT_REPORT_TYPES_MAP = { MERCHANT_REPORT_T1: 'MERCHANT_REPORT_T1', ONGOING_MERCHANT_REPORT_T1: 'ONGOING_MERCHANT_REPORT_T1', diff --git a/services/workflows-service/src/business-report/dto/get-business-report.dto.ts b/services/workflows-service/src/business-report/dto/get-business-report.dto.ts deleted file mode 100644 index 34542cd1a2..0000000000 --- a/services/workflows-service/src/business-report/dto/get-business-report.dto.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { PageDto } from '@/common/dto'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsIn, IsOptional, IsString } from 'class-validator'; -import { MERCHANT_REPORT_TYPES, type MerchantReportType } from '@/business-report/constants'; - -export class GetBusinessReportDto { - @IsOptional() - @IsString() - businessId?: string; - - @ApiProperty({ - required: true, - }) - @IsIn(MERCHANT_REPORT_TYPES) - type!: MerchantReportType; - - @IsOptional() - @ApiProperty({ - type: String, - required: false, - description: 'Column to sort by and direction separated by a colon', - examples: [{ value: 'createdAt:asc' }, { value: 'status:asc' }], - }) - orderBy?: `${string}:asc` | `${string}:desc`; - - @ApiProperty({ type: PageDto }) - page!: PageDto; -} diff --git a/services/workflows-service/src/business-report/dto/get-business-reports.dto.ts b/services/workflows-service/src/business-report/dto/get-business-reports.dto.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/services/workflows-service/src/business-report/dtos/business-report-findings.dto.ts b/services/workflows-service/src/business-report/dtos/business-report-findings.dto.ts new file mode 100644 index 0000000000..22ca024d96 --- /dev/null +++ b/services/workflows-service/src/business-report/dtos/business-report-findings.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FindingDto { + @ApiProperty({ type: String }) + value!: string; + + @ApiProperty({ type: String }) + title!: string; +} + +export class BusinessReportFindingsListResponseDto { + @ApiProperty({ type: [FindingDto] }) + data!: Array<{ value: string; title: string }>; +} diff --git a/services/workflows-service/src/business-report/dtos/business-report-list.dto.ts b/services/workflows-service/src/business-report/dtos/business-report-list.dto.ts new file mode 100644 index 0000000000..d8ec57d6ae --- /dev/null +++ b/services/workflows-service/src/business-report/dtos/business-report-list.dto.ts @@ -0,0 +1,106 @@ +import { z } from 'zod'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsOptional, IsString } from 'class-validator'; + +import { PageDto } from '@/common/dto'; +import { + MERCHANT_REPORT_RISK_LEVELS, + MERCHANT_REPORT_STATUSES_MAP, + MERCHANT_REPORT_TYPES_MAP, + type MerchantReportType, +} from '@/business-report/constants'; +import { BusinessReportDto } from '@/business-report/dtos/business-report.dto'; + +export class BusinessReportListRequestParamDto { + @IsOptional() + @IsString() + businessId?: string; + + @IsOptional() + @ApiProperty({ type: String, required: false }) + search?: string; + + @ApiProperty({ type: PageDto }) + page!: PageDto; + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + from?: string; + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + to?: string; + + @IsOptional() + @IsString() + @ApiProperty({ type: String, required: false }) + reportType?: MerchantReportType; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @ApiProperty({ type: [String], required: false }) + riskLevels?: Array<'low' | 'medium' | 'high' | 'critical'>; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @ApiProperty({ type: [String], required: false }) + statuses?: Array<'failed' | 'quality-control' | 'completed' | 'in-progress'>; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @ApiProperty({ type: [String], required: false }) + findings?: string[]; +} + +export const ListBusinessReportsSchema = z.object({ + from: z.string().optional(), + to: z.string().optional(), + reportType: z + .enum([ + MERCHANT_REPORT_TYPES_MAP.MERCHANT_REPORT_T1, + MERCHANT_REPORT_TYPES_MAP.ONGOING_MERCHANT_REPORT_T1, + ]) + .optional(), + riskLevels: z + .array( + z.enum([ + MERCHANT_REPORT_RISK_LEVELS.low, + MERCHANT_REPORT_RISK_LEVELS.medium, + MERCHANT_REPORT_RISK_LEVELS.high, + MERCHANT_REPORT_RISK_LEVELS.critical, + ]), + ) + .optional(), + statuses: z + .array( + z.enum([ + MERCHANT_REPORT_STATUSES_MAP.failed, + MERCHANT_REPORT_STATUSES_MAP.completed, + MERCHANT_REPORT_STATUSES_MAP['in-progress'], + MERCHANT_REPORT_STATUSES_MAP['quality-control'], + ]), + ) + .optional(), + findings: z.array(z.string()).optional(), + search: z.string().optional(), + page: z.object({ + number: z.coerce.number().int().positive(), + size: z.coerce.number().int().positive().max(100), + }), +}); + +export class BusinessReportListResponseDto { + @ApiProperty({ type: Number, example: 20 }) + totalItems!: number; + + @ApiProperty({ type: Number, example: 1 }) + totalPages!: number; + + @ApiProperty({ type: [BusinessReportDto] }) + data!: BusinessReportDto[]; +} diff --git a/services/workflows-service/src/business-report/business-report.dto.ts b/services/workflows-service/src/business-report/dtos/business-report.dto.ts similarity index 100% rename from services/workflows-service/src/business-report/business-report.dto.ts rename to services/workflows-service/src/business-report/dtos/business-report.dto.ts diff --git a/services/workflows-service/src/business-report/dto/create-business-report-batch-body.dto.ts b/services/workflows-service/src/business-report/dtos/create-business-report-batch-body.dto.ts similarity index 100% rename from services/workflows-service/src/business-report/dto/create-business-report-batch-body.dto.ts rename to services/workflows-service/src/business-report/dtos/create-business-report-batch-body.dto.ts diff --git a/services/workflows-service/src/business-report/dto/create-business-report.dto.ts b/services/workflows-service/src/business-report/dtos/create-business-report.dto.ts similarity index 100% rename from services/workflows-service/src/business-report/dto/create-business-report.dto.ts rename to services/workflows-service/src/business-report/dtos/create-business-report.dto.ts diff --git a/services/workflows-service/src/business-report/merchant-monitoring-client.ts b/services/workflows-service/src/business-report/merchant-monitoring-client.ts index d273f68b62..52e26786fd 100644 --- a/services/workflows-service/src/business-report/merchant-monitoring-client.ts +++ b/services/workflows-service/src/business-report/merchant-monitoring-client.ts @@ -74,7 +74,7 @@ export class MerchantMonitoringClient { headers: { Authorization: `Bearer ${env.UNIFIED_API_TOKEN ?? ''}`, }, - timeout: 30_000, + timeout: 300_000, }); } @@ -194,8 +194,13 @@ export class MerchantMonitoringClient { customerId, businessId, limit, + from, + to, page, reportType, + riskLevels, + statuses, + findings, withoutUnpublishedOngoingReports, searchQuery, }: { @@ -203,7 +208,12 @@ export class MerchantMonitoringClient { businessId?: string; limit: number; page: number; + from?: string; + to?: string; reportType?: MerchantReportType; + riskLevels?: Array<'low' | 'medium' | 'high' | 'critical'>; + statuses?: Array<'failed' | 'quality-control' | 'completed' | 'in-progress'>; + findings?: string[]; withoutUnpublishedOngoingReports?: boolean; searchQuery?: string; }) { @@ -212,7 +222,12 @@ export class MerchantMonitoringClient { customerId, ...(businessId && { merchantId: businessId }), limit, + from, + to, + riskLevels, page, + statuses, + findings, withoutUnpublishedOngoingReports, ...(searchQuery && { searchQuery }), ...(reportType && { reportType }), @@ -230,4 +245,14 @@ export class MerchantMonitoringClient { return response.totalItems; } + + public async listFindings() { + const response = await this.axios.get('external/findings', { + headers: { + Authorization: `Bearer ${env.UNIFIED_API_TOKEN}`, + }, + }); + + return response.data ?? []; + } } diff --git a/services/workflows-service/src/ui-definition/ui-definition.service.ts b/services/workflows-service/src/ui-definition/ui-definition.service.ts index 46a3e62583..f9098e58f2 100644 --- a/services/workflows-service/src/ui-definition/ui-definition.service.ts +++ b/services/workflows-service/src/ui-definition/ui-definition.service.ts @@ -65,13 +65,6 @@ export class UiDefinitionService { args: Omit, projectIds: TProjectIds, ) { - console.log({ - ...args, - where: { - id, - }, - }); - return await this.repository.update( { ...args,