diff --git a/README.md b/README.md index 4702d91733..535ee869b1 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,7 @@ Once the process is complete, _2 tabs_ will open in your browser: We appreciate all types of contributions and believe that an active community is the secret to a rich and stable product. Here are some of the ways you can contribute: -- Give us feedback in our [Slack community](https://join.slack.com/t/ballerine-oss/shared_invite/zt-1iu6otkok-OqBF3TrcpUmFd9oUjNs2iw) +- Give us feedback in our [Slack community](https://join.slack.com/t/ballerine-oss/shared_invite/zt-1il7txerq-K0YrXtlzMttGgD3XXYxlfw) - Help with bugs and features on [our Issues page](https://github.com/ballerine-io/ballerine/issues) - Submit a [feature request](https://github.com/ballerine-io/ballerine/issues/new?assignees=&labels=enhancement%2C+feature&template=feature_request.md) or [bug report](https://github.com/ballerine-io/ballerine/issues/new?assignees=&labels=bug&template=bug_report.md) diff --git a/apps/backoffice-v2/.storybook/main.ts b/apps/backoffice-v2/.storybook/main.ts index 8fdbf2ccd4..f92dc6f2f1 100644 --- a/apps/backoffice-v2/.storybook/main.ts +++ b/apps/backoffice-v2/.storybook/main.ts @@ -18,5 +18,13 @@ const config: StorybookConfig = { docs: { autodocs: true, }, + viteFinal: config => { + config.optimizeDeps = { + ...config.optimizeDeps, + include: ['@ballerine/ui'], + }; + + return config; + }, }; export default config; diff --git a/apps/backoffice-v2/CHANGELOG.md b/apps/backoffice-v2/CHANGELOG.md index d8ffb11998..8c46633320 100644 --- a/apps/backoffice-v2/CHANGELOG.md +++ b/apps/backoffice-v2/CHANGELOG.md @@ -1,5 +1,38 @@ # @ballerine/backoffice-v2 +## 0.7.84 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + - @ballerine/ui@0.5.54 + - @ballerine/workflow-browser-sdk@0.6.79 + - @ballerine/react-pdf-toolkit@1.2.54 + - @ballerine/workflow-node-sdk@0.6.79 + +## 0.7.83 + +### Patch Changes + +- added command.loading +- Updated dependencies + - @ballerine/ui@0.5.53 + - @ballerine/react-pdf-toolkit@1.2.53 + +## 0.7.82 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/blocks@0.2.30 + - @ballerine/common@0.9.59 + - @ballerine/react-pdf-toolkit@1.2.51 + - @ballerine/ui@0.5.51 + - @ballerine/workflow-browser-sdk@0.6.78 + - @ballerine/workflow-node-sdk@0.6.78 + ## 0.7.81 ### Patch Changes diff --git a/apps/backoffice-v2/package.json b/apps/backoffice-v2/package.json index e72821f055..3e9aaa3c67 100644 --- a/apps/backoffice-v2/package.json +++ b/apps/backoffice-v2/package.json @@ -1,6 +1,6 @@ { "name": "@ballerine/backoffice-v2", - "version": "0.7.81", + "version": "0.7.84", "description": "Ballerine - Backoffice", "homepage": "https://github.com/ballerine-io/ballerine", "type": "module", @@ -51,12 +51,12 @@ "preview": "vite preview" }, "dependencies": { - "@ballerine/blocks": "0.2.29", - "@ballerine/common": "0.9.58", - "@ballerine/react-pdf-toolkit": "^1.2.50", - "@ballerine/ui": "^0.5.50", - "@ballerine/workflow-browser-sdk": "0.6.77", - "@ballerine/workflow-node-sdk": "0.6.77", + "@ballerine/blocks": "0.2.30", + "@ballerine/common": "0.9.60", + "@ballerine/react-pdf-toolkit": "^1.2.54", + "@ballerine/ui": "^0.5.54", + "@ballerine/workflow-browser-sdk": "0.6.79", + "@ballerine/workflow-node-sdk": "0.6.79", "@botpress/webchat": "^2.1.10", "@botpress/webchat-generator": "^0.2.9", "@fontsource/inter": "^4.5.15", @@ -148,8 +148,8 @@ "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config-react": "^2.0.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config-react": "^2.0.28", "@cspell/cspell-types": "^6.31.1", "@faker-js/faker": "^7.6.0", "@playwright/test": "^1.32.1", diff --git a/apps/backoffice-v2/public/locales/en/toast.json b/apps/backoffice-v2/public/locales/en/toast.json index e7aa7685c7..2c9d16b5f9 100644 --- a/apps/backoffice-v2/public/locales/en/toast.json +++ b/apps/backoffice-v2/public/locales/en/toast.json @@ -104,5 +104,13 @@ "update_details": { "success": "Details updated successfully.", "error": "Error occurred while updating details." + }, + "ubo_created": { + "success": "UBO successfully added", + "error": "Error adding UBO" + }, + "ubo_deleted": { + "success": "UBO successfully removed", + "error": "Error removing UBO" } } diff --git a/apps/backoffice-v2/src/common/api-client/interfaces.ts b/apps/backoffice-v2/src/common/api-client/interfaces.ts index 7d5b364bdf..aeb38dba2f 100644 --- a/apps/backoffice-v2/src/common/api-client/interfaces.ts +++ b/apps/backoffice-v2/src/common/api-client/interfaces.ts @@ -8,7 +8,7 @@ import { z, ZodSchema } from 'zod'; export interface IApiClient { (params: { endpoint: string; - method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH; + method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH | typeof Method.DELETE; body?: TBody; options?: Omit; timeout?: number; @@ -19,7 +19,7 @@ export interface IApiClient { (params: { url: string; - method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH; + method: typeof Method.POST | typeof Method.PUT | typeof Method.PATCH | typeof Method.DELETE; body?: TBody; options?: Omit; timeout?: number; @@ -30,7 +30,7 @@ export interface IApiClient { (params: { endpoint: string; - method: typeof Method.GET | typeof Method.DELETE; + method: typeof Method.GET; options?: Omit; timeout?: number; schema: TZodSchema; @@ -39,7 +39,7 @@ export interface IApiClient { (params: { url: string; - method: typeof Method.GET | typeof Method.DELETE; + method: typeof Method.GET; options?: Omit; timeout?: number; schema: TZodSchema; 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..b31216d18e 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, @@ -8,13 +9,14 @@ import { CommandInput, CommandItem, CommandList, + CommandLoading, CommandSeparator, ctw, Popover, PopoverContent, PopoverTrigger, } from '@ballerine/ui'; -import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'; + import { Separator } from '@/common/components/atoms/Separator/Separator'; interface IMultiSelectProps< @@ -25,10 +27,24 @@ interface IMultiSelectProps< }, > { title: string; + isLoading?: boolean; selectedValues: Array; 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 +55,13 @@ export const MultiSelect = < }, >({ title, - selectedValues, + isLoading, + 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 +69,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. - - {options.map(option => { - const isSelected = selected.some(value => value === option.value); + {isLoading && ( + + Loading... + + )} + {!isLoading && options.length === 0 && No results found.} + {!isLoading && options.length > 0 && ( + +
+ {options.map(option => { + const isSelected = selected.some(value => value === option.value); - return ( - onSelectChange(option.value)}> -
- -
- {option.icon} - {option.label} -
- ); - })} - - {selected?.length > 0 && ( - <> - - - { - onClearSelect(); - setSelected([]); - }} - className="justify-center text-center" - > - Clear filters - - - + return ( + onSelectChange(option.value)} + > +
+ +
+ {option.icon} + {option.label} +
+ ); + })} +
+
)}
+ + + + 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..617e801ff8 100644 --- a/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx +++ b/apps/backoffice-v2/src/common/components/organisms/Calendar/Calendar.tsx @@ -3,8 +3,10 @@ import { ChevronLeftIcon, ChevronRightIcon } from '@radix-ui/react-icons'; import { ctw } from '@/common/utils/ctw/ctw'; import { DayPicker, DayPickerRangeProps } from 'react-day-picker'; import { buttonVariants } from '../../atoms/Button/Button'; +import { Button } from '@ballerine/ui'; export type CalendarProps = DayPickerRangeProps; + export const Calendar = ({ className, classNames, @@ -12,53 +14,67 @@ export const Calendar = ({ ...props }: CalendarProps) => { return ( - .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md' - : '[&:has([aria-selected])]:rounded-md', - ), - day: ctw( - buttonVariants({ variant: 'ghost' }), - 'h-8 w-8 p-0 font-normal aria-selected:opacity-100', - ), - day_range_start: 'day-range-start', - day_range_end: 'day-range-end', - day_selected: - 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', - day_today: 'bg-accent text-accent-foreground', - day_outside: - 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30', - day_disabled: 'text-muted-foreground opacity-50', - day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground', - day_hidden: 'invisible', - ...classNames, - }} - components={{ - IconLeft: ({ ...props }) => , - IconRight: ({ ...props }) => , - }} - {...props} - /> +
+ .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md' + : '[&:has([aria-selected])]:rounded-md', + ), + day: ctw( + buttonVariants({ variant: 'ghost' }), + 'h-8 w-8 p-0 font-normal aria-selected:opacity-100', + ), + day_range_start: 'day-range-start', + day_range_end: 'day-range-end', + day_selected: + 'bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground', + day_today: 'bg-accent text-accent-foreground', + day_outside: + 'day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30', + day_disabled: 'text-muted-foreground opacity-50', + day_range_middle: 'aria-selected:bg-accent aria-selected:text-accent-foreground', + day_hidden: 'invisible', + ...classNames, + }} + components={{ + IconLeft: ({ ...props }) => , + IconRight: ({ ...props }) => , + }} + {...props} + /> +
+ +
+
); }; + 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/domains/ui-definition/fetchers.ts b/apps/backoffice-v2/src/domains/ui-definition/fetchers.ts new file mode 100644 index 0000000000..02ff4fa5fd --- /dev/null +++ b/apps/backoffice-v2/src/domains/ui-definition/fetchers.ts @@ -0,0 +1,27 @@ +import { apiClient } from '@/common/api-client/api-client'; + +import { Method } from '@/common/enums'; + +import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; +import { z } from 'zod'; + +export const translateUiDefinition = async ({ + id, + partialUiDefinition, + locale, +}: { + id: string; + partialUiDefinition: Record; + locale: string; +}) => { + const [data, error] = await apiClient({ + endpoint: `../case-management/ui-definition/${id}/translate/${locale}`, + method: Method.POST, + body: { + partialUiDefinition, + }, + schema: z.record(z.string(), z.unknown()), + }); + + return handleZodError(error, data); +}; diff --git a/apps/backoffice-v2/src/domains/ui-definition/hooks/queries/useTranslateUiDefinitionQuery/useTranslateUiDefinitionQuery.tsx b/apps/backoffice-v2/src/domains/ui-definition/hooks/queries/useTranslateUiDefinitionQuery/useTranslateUiDefinitionQuery.tsx new file mode 100644 index 0000000000..9972b24f4b --- /dev/null +++ b/apps/backoffice-v2/src/domains/ui-definition/hooks/queries/useTranslateUiDefinitionQuery/useTranslateUiDefinitionQuery.tsx @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import { uiDefinitionQueryKeys } from '@/domains/ui-definition/query-keys'; + +export const useTranslateUiDefinitionQuery = ({ + id, + partialUiDefinition, + locale, +}: { + id: string; + partialUiDefinition: Record; + locale: string; +}) => { + return useQuery({ + ...uiDefinitionQueryKeys.translate({ id, partialUiDefinition, locale }), + enabled: !!partialUiDefinition && !!id, + }); +}; diff --git a/apps/backoffice-v2/src/domains/ui-definition/query-keys.ts b/apps/backoffice-v2/src/domains/ui-definition/query-keys.ts new file mode 100644 index 0000000000..3e994ac1ae --- /dev/null +++ b/apps/backoffice-v2/src/domains/ui-definition/query-keys.ts @@ -0,0 +1,25 @@ +import { createQueryKeys } from '@lukemorales/query-key-factory'; +import { translateUiDefinition } from './fetchers'; + +export const uiDefinitionQueryKeys = createQueryKeys('ui-definition', { + translate: ({ + id, + partialUiDefinition, + locale, + }: { + id: string; + partialUiDefinition: Record; + locale: string; + }) => { + return { + queryKey: [ + { + id, + partialUiDefinition, + locale, + }, + ], + queryFn: () => translateUiDefinition({ id, partialUiDefinition, locale }), + }; + }, +}); diff --git a/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts b/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts index 16cb3de6f1..836c371ef3 100644 --- a/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflow-definitions/fetchers.ts @@ -56,6 +56,15 @@ export const WorkflowDefinitionConfigSchema = z .optional(), }) .optional(), + ubos: z + .object({ + create: z + .object({ + enabled: z.boolean().optional(), + }) + .optional(), + }) + .optional(), }) .passthrough() .nullable(); @@ -77,6 +86,10 @@ export const WorkflowDefinitionByIdSchema = ObjectWithIdSchema.extend({ }) .optional() .nullable(), + uiDefinitions: z + .array(z.object({ id: z.string(), uiContext: z.string() })) + .optional() + .nullable(), }); export type TWorkflowDefinitionById = z.infer; diff --git a/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useCreateUboMutation/useCreateUboMutation.tsx b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useCreateUboMutation/useCreateUboMutation.tsx new file mode 100644 index 0000000000..49466ce78f --- /dev/null +++ b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useCreateUboMutation/useCreateUboMutation.tsx @@ -0,0 +1,43 @@ +import { apiClient } from '@/common/api-client/api-client'; + +import { Method } from '@/common/enums'; + +import { z } from 'zod'; + +import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; + +import { toast } from 'sonner'; +import { useMutation } from '@tanstack/react-query'; +import { useQueryClient } from '@tanstack/react-query'; +import { t } from 'i18next'; + +export const useCreateUboMutation = ({ + workflowId, + onSuccess, +}: { + workflowId: string; + onSuccess: () => void; +}) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (ubo: Record) => { + const [data, error] = await apiClient({ + endpoint: `../case-management/workflows/${workflowId}/ubos`, + method: Method.POST, + body: ubo, + schema: z.undefined(), + }); + + return handleZodError(error, data); + }, + onSuccess: () => { + void queryClient.invalidateQueries(); + toast.success(t('toast:ubo_created.success')); + onSuccess(); + }, + onError: () => { + toast.error(t('toast:ubo_created.error')); + }, + }); +}; diff --git a/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useDeleteUbosByIdsMutation/useDeleteUbosByIdsMutation.tsx b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useDeleteUbosByIdsMutation/useDeleteUbosByIdsMutation.tsx new file mode 100644 index 0000000000..3201c1ef1c --- /dev/null +++ b/apps/backoffice-v2/src/domains/workflows/hooks/mutations/useDeleteUbosByIdsMutation/useDeleteUbosByIdsMutation.tsx @@ -0,0 +1,36 @@ +import { apiClient } from '@/common/api-client/api-client'; + +import { z } from 'zod'; + +import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; + +import { toast } from 'sonner'; +import { Method } from '@/common/enums'; +import { useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import { t } from 'i18next'; + +export const useDeleteUbosByIdsMutation = ({ workflowId }: { workflowId: string }) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (ids: string[]) => { + const [data, error] = await apiClient({ + endpoint: `../case-management/workflows/${workflowId}/ubos`, + method: Method.DELETE, + body: { ids }, + schema: z.undefined(), + }); + + return handleZodError(error, data); + }, + onSuccess: () => { + void queryClient.invalidateQueries(); + + toast.success(t('toast:ubo_deleted.success')); + }, + onError: () => { + toast.error(t('toast:ubo_deleted.error')); + }, + }); +}; diff --git a/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx b/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx index ae31ba0fc7..f09e26f964 100644 --- a/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx +++ b/apps/backoffice-v2/src/lib/blocks/components/BlockCell/BlockCell.tsx @@ -19,7 +19,7 @@ export const BlockCell: FunctionComponent = ({ value, props }) } return ( - + Approve @@ -335,13 +335,32 @@ export const useKycBlock = ({ .flat(1); }; + const { mutate: mutateInitiateKyc } = useEventMutation(); + + const getEvent = () => { + if (childWorkflow?.nextEvents?.includes('start')) { + return 'start'; + } + }; + const event = getEvent(); + const onInitiateKyc = useCallback(() => { + if (!event) { + return; + } + + mutateInitiateKyc({ + workflowId: childWorkflow?.id, + event, + }); + }, [mutateInitiateKyc, event, childWorkflow?.id]); + const headerCell = createBlocksTyped() .addBlock() .addCell({ id: 'header', type: 'container', props: { - className: 'items-start', + className: 'justify-between items-center pt-6', }, value: createBlocksTyped() .addBlock() @@ -350,6 +369,9 @@ export const useKycBlock = ({ value: `${valueOrNA(childWorkflow?.context?.entity?.data?.firstName)} ${valueOrNA( childWorkflow?.context?.entity?.data?.lastName, )}`, + props: { + className: 'mt-0', + }, }) .build() .flat(1), @@ -505,40 +527,83 @@ export const useKycBlock = ({ }) .addCell({ type: 'container', - value: createBlocksTyped() - .addBlock() - .addCell({ - id: 'header', - type: 'heading', - value: 'Document Extracted Data', - }) - .build() - .concat(documentExtractedData) - .flat(1), + value: documentExtractedData.length + ? createBlocksTyped() + .addBlock() + .addCell({ + id: 'header', + type: 'heading', + value: 'Document Extracted Data', + }) + .build() + .concat(documentExtractedData) + .flat(1) + : createBlocksTyped() + .addBlock() + .addCell({ + type: 'heading', + value: 'Document Extracted Data', + }) + .addCell({ + type: 'paragraph', + value: 'Initiate KYC for document extracted data to appear', + props: { + className: 'py-4 text-slate-500', + }, + }) + .addCell({ + type: 'callToAction', + value: { + text: 'Initiate KYC', + onClick: onInitiateKyc, + props: { + className: + 'px-2 py-0 text-xs aria-disabled:pointer-events-none aria-disabled:opacity-50 ms-3', + variant: 'outline', + disabled: !event, + }, + }, + }) + .buildFlat(), }) .addCell({ type: 'container', - value: createBlocksTyped() - .addBlock() - .addCell({ - id: 'header', - type: 'heading', - value: 'Document Verification Results', - }) - .addCell({ - id: 'decision', - type: 'details', - hideSeparator: true, - value: { - id: 1, - title: 'Decision', - data: decision, - }, - workflowId: childWorkflow?.id, - documents: childWorkflow?.context?.documents, - }) - .build() - .flat(1), + value: decision.length + ? createBlocksTyped() + .addBlock() + .addCell({ + id: 'header', + type: 'heading', + value: 'Document Verification Results', + }) + .addCell({ + id: 'decision', + type: 'details', + hideSeparator: true, + value: { + id: 1, + title: 'Decision', + data: decision, + }, + workflowId: childWorkflow?.id, + documents: childWorkflow?.context?.documents, + }) + .build() + .flat(1) + : createBlocksTyped() + .addBlock() + .addCell({ + type: 'heading', + value: 'Document Verification Results', + }) + .addCell({ + type: 'paragraph', + value: 'Initiate KYC for document verification results to appear', + props: { + className: 'py-4 text-slate-500', + }, + }) + .buildFlat(), }) .build() .flat(1), diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/ubos-form-json-definition.ts b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/ubos-form-json-definition.ts new file mode 100644 index 0000000000..0fd2391805 --- /dev/null +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/ubos-form-json-definition.ts @@ -0,0 +1,245 @@ +export const ubosFormJsonDefinition = { + type: 'json-form', + name: 'company-ownership-contacts-form', + valueDestination: 'entity.data.additionalInfo.contacts', + options: { + description: 'text.companyOwnership.page.description', + jsonFormDefinition: { + title: 'text.shareholder', + type: 'object', + required: [ + 'company-ownership-role-input', + 'company-ownership-first-name-input', + 'company-ownership-last-name-input', + 'company-ownership-ownership-percentage-input', + 'company-ownership-email-input', + 'company-ownership-phone-input', + 'company-ownership-authorized-signatory-checkbox', + 'company-ownership-passport-number-input', + 'company-ownership-date-of-birth-input', + 'company-ownership-nationality-input', + 'company-ownership-gender-input', + 'company-ownership-country-input', + 'company-ownership-city-input', + ], + }, + uiSchema: { + titleTemplate: 'text.companyOwnership.contactIndex', + }, + }, + elements: [ + { + name: 'company-ownership-role-input', + type: 'json-form:text', + valueDestination: 'role', + options: { + label: 'text.companyOwnership.organizationRole.label', + hint: 'text.companyOwnership.organizationRole.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-first-name-input', + type: 'json-form:text', + valueDestination: 'firstName', + options: { + label: 'text.companyOwnership.firstName.label', + hint: 'text.companyOwnership.firstName.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-last-name-input', + type: 'json-form:text', + valueDestination: 'lastName', + options: { + label: 'text.companyOwnership.lastName.label', + hint: 'text.companyOwnership.lastName.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-ownership-percentage-input', + type: 'json-form:text', + valueDestination: 'ownershipPercentage', + options: { + label: 'text.companyOwnership.ownershipPercentage.label', + hint: 'text.companyOwnership.ownershipPercentage.placeholder', + jsonFormDefinition: { + type: 'number', + }, + }, + }, + { + name: 'company-ownership-email-input', + type: 'json-form:text', + valueDestination: 'email', + options: { + label: 'text.companyOwnership.email.label', + hint: 'text.companyOwnership.email.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-phone-input', + type: 'json-form:text', + valueDestination: 'phone', + options: { + label: 'text.companyOwnership.phone.label', + hint: 'text.companyOwnership.phone.placeholder', + jsonFormDefinition: { + type: 'string', + }, + uiSchema: { + 'ui:field': 'PhoneInput', + }, + }, + }, + { + name: 'company-ownership-authorized-signatory-checkbox', + type: 'json-form:text', + valueDestination: 'isAuthorizedSignatory', + options: { + label: 'text.companyOwnership.authorizedSignatory.label', + jsonFormDefinition: { + type: 'boolean', + }, + uiSchema: { + 'ui:label': false, + }, + }, + }, + { + name: 'company-ownership-passport-number-input', + type: 'json-form:text', + valueDestination: 'passportNumber', + options: { + label: 'text.companyOwnership.passportNumber.label', + hint: 'text.companyOwnership.passportNumber.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-date-of-birth-input', + type: 'json-form:text', + valueDestination: 'dateOfBirth', + options: { + label: 'text.companyOwnership.dateOfBirth.label', + hint: 'text.companyOwnership.dateOfBirth.placeholder', + jsonFormDefinition: { + type: 'string', + }, + uiSchema: { + 'ui:field': 'DateInput', + disableFutureDate: true, + outputFormat: 'YYYY-MM-DD', + }, + }, + }, + { + name: 'company-ownership-placeofbirth-input', + type: 'json-form:text', + valueDestination: 'placeOfBirth', + options: { + label: 'text.companyOwnership.placeOfBirth.label', + hint: 'text.companyOwnership.placeOfBirth.placeholder', + jsonFormDefinition: { + type: 'string', + }, + uiSchema: { + 'ui:field': 'CountryPicker', + }, + }, + }, + { + name: 'company-ownership-nationality-input', + type: 'json-form:text', + valueDestination: 'nationality', + options: { + label: 'text.companyOwnership.nationality.label', + hint: 'text.companyOwnership.nationality.placeholder', + jsonFormDefinition: { + type: 'string', + }, + uiSchema: { + 'ui:field': 'NationalityPicker', + }, + }, + }, + { + name: 'company-ownership-gender-input', + type: 'json-form:text', + valueDestination: 'gender', + options: { + label: 'text.companyOwnership.gender.label', + hint: 'text.companyOwnership.gender.placeholder', + jsonFormDefinition: { + type: 'string', + oneOf: [ + { + title: 'text.companyOwnership.gender.male', + const: 'male', + }, + { + title: 'text.companyOwnership.gender.female', + const: 'female', + }, + { + title: 'text.companyOwnership.gender.other', + const: 'other', + }, + ], + }, + }, + }, + { + name: 'company-ownership-country-input', + type: 'json-form:text', + valueDestination: 'country', + options: { + label: 'text.companyOwnership.country.label', + hint: 'text.companyOwnership.country.placeholder', + jsonFormDefinition: { + type: 'string', + }, + uiSchema: { + 'ui:field': 'CountryPicker', + }, + }, + }, + { + name: 'company-ownership-city-input', + type: 'json-form:text', + valueDestination: 'city', + options: { + label: 'text.companyOwnership.city.label', + hint: 'text.companyOwnership.city.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + { + name: 'company-ownership-street-input', + type: 'json-form:text', + valueDestination: 'street', + options: { + label: 'text.companyOwnership.street.label', + hint: 'text.companyOwnership.street.placeholder', + jsonFormDefinition: { + type: 'string', + }, + }, + }, + ], +}; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/useManageUbosBlock.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/useManageUbosBlock.tsx new file mode 100644 index 0000000000..8ea0091896 --- /dev/null +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/useManageUbosBlock.tsx @@ -0,0 +1,292 @@ +import { useWorkflowDefinitionByIdQuery } from '@/domains/workflow-definitions/hooks/queries/useWorkflowDefinitionByQuery/useWorkflowDefinitionByIdQuery'; +import { useCurrentCaseQuery } from '@/pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery'; +import { ubosFormJsonDefinition } from './ubos-form-json-definition'; +import { useCallback } from 'react'; +import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState'; +import { useToggle } from '@/common/hooks/useToggle/useToggle'; +import { + baseLayouts, + DynamicForm, + FieldLayout, + ScrollArea, + TextWithNAFallback, +} from '@ballerine/ui'; +import { Button } from '@/common/components/atoms/Button/Button'; +import { createColumnHelper } from '@tanstack/react-table'; +import { createFormSchemaFromUIElements } from './utils/create-form-schema-from-ui-elements'; +import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery'; +import { useMemo } from 'react'; +import { ArrowLeft, Trash2Icon } from 'lucide-react'; +import { set } from 'lodash-es'; +import { Dialog } from '@/common/components/molecules/Dialog/Dialog'; +import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed'; +import { UrlDataTable } from '@/common/components/organisms/UrlDataTable/UrlDataTable'; +import { transformErrors } from '@/pages/Entities/components/CaseCreation/components/CaseCreationForm/utils/transform-errors'; +import { useTranslateUiDefinitionQuery } from '@/domains/ui-definition/hooks/queries/useTranslateUiDefinitionQuery/useTranslateUiDefinitionQuery'; +import { useDeleteUbosByIdsMutation } from '@/domains/workflows/hooks/mutations/useDeleteUbosByIdsMutation/useDeleteUbosByIdsMutation'; +import { useCreateUboMutation } from '@/domains/workflows/hooks/mutations/useCreateUboMutation/useCreateUboMutation'; +import { useLocale } from '@/common/hooks/useLocale/useLocale'; + +export const useManageUbosBlock = ({ + create, +}: { + create: { + enabled: boolean; + }; +}) => { + const { data: workflow } = useCurrentCaseQuery(); + const { data: workflowDefinition } = useWorkflowDefinitionByIdQuery({ + workflowDefinitionId: workflow?.workflowDefinition?.id ?? '', + }); + const uiDefinition = workflowDefinition?.uiDefinitions?.find( + uiDefinition => uiDefinition.uiContext === 'collection_flow', + ); + const locale = useLocale(); + const { data: translatedUbos } = useTranslateUiDefinitionQuery({ + id: uiDefinition?.id ?? '', + partialUiDefinition: ubosFormJsonDefinition, + locale, + }); + const { formSchema, uiSchema } = createFormSchemaFromUIElements(translatedUbos ?? {}); + const [isAddingUbo, _toggleIsAddingUbo, toggleOnIsAddingUbo, toggleOffIsAddingUbo] = useToggle(); + const [isManageUbosOpen, toggleIsManageUbosOpen] = useToggle(); + const { mutate: mutateCreateUbo } = useCreateUboMutation({ + workflowId: workflow?.id, + onSuccess: () => { + if (!isAddingUbo) { + return; + } + + toggleOffIsAddingUbo(); + }, + }); + const { mutate: mutateDeleteUbosByIds } = useDeleteUbosByIdsMutation({ + workflowId: workflow?.id, + }); + const onRemoveUboFromContext = useCallback( + (ids: string[]) => { + return () => mutateDeleteUbosByIds(ids); + }, + [mutateDeleteUbosByIds], + ); + const { data: session } = useAuthenticatedUserQuery(); + const caseState = useCaseState(session?.user, workflow); + const layouts = useMemo( + () => ({ + ...baseLayouts, + FieldTemplate: FieldLayout, + ButtonTemplates: { + ...baseLayouts.ButtonTemplates, + SubmitButton: () => ( +
+ +
+ ), + }, + }), + [caseState.writeEnabled], + ); + const columnHelper = createColumnHelper<{ + id: string; + firstName: string; + lastName: string; + ownershipPercentage: number; + }>(); + const columns = useMemo( + () => [ + columnHelper.accessor('firstName', { + header: 'First Name', + }), + columnHelper.accessor('lastName', { + header: 'Last Name', + }), + columnHelper.accessor('ownershipPercentage', { + header: '% of Ownership', + cell: ({ getValue }) => { + const value = getValue(); + + return ( + {value || value === 0 ? `${value}%` : value} + ); + }, + }), + columnHelper.display({ + id: 'remove', + header: '', + cell: ({ row }) => { + return ( + + + + } + title={'UBO removal confirmation'} + description={ +

+ Are you sure you want to remove this UBO? This action will be logged, and the + UBO's data will be removed from the case. +

+ } + content={null} + close={ +
+ + +
+ } + props={{ + content: { + className: 'mb-96', + }, + title: { + className: `text-2xl`, + }, + }} + /> + ); + }, + }), + ], + [caseState.writeEnabled, columnHelper, onRemoveUboFromContext], + ); + const onSubmit = useCallback( + (data: Record) => { + const ubo = Object.entries(data).reduce((acc, [key, value]) => { + const element = ubosFormJsonDefinition.elements.find(element => element.name === key); + + if (!element?.valueDestination) { + return acc; + } + + set(acc, element.valueDestination, value); + + return acc; + }, {} as Record); + + mutateCreateUbo(ubo); + }, + [mutateCreateUbo], + ); + const ubos = useMemo(() => { + return ( + workflow?.childWorkflows?.map(childWorkflow => ({ + id: childWorkflow.context.entity.ballerineEntityId, + firstName: childWorkflow.context.entity.data.firstName, + lastName: childWorkflow.context.entity.data.lastName, + ownershipPercentage: + childWorkflow.context.entity.data.percentageOfOwnership ?? + childWorkflow.context.entity.data.ownershipPercentage ?? + childWorkflow.context.entity.data.additionalInfo.percentageOfOwnership ?? + childWorkflow.context.entity.data.additionalInfo.ownershipPercentage, + })) ?? [] + ); + }, [workflow?.childWorkflows]); + + return createBlocksTyped() + .addBlock() + .addCell({ + type: 'node', + value: ( + + Manage UBOs + + } + content={ +
+ {(!create.enabled || !isAddingUbo) && ( +
+

Manage UBOs

+ row.id, + }} + props={{ + scroll: { + className: '[&>div]:max-h-[73vh]', + }, + }} + /> + {create.enabled && ( + + )} +
+ )} + {create.enabled && isAddingUbo && ( + <> + + +
+

Add UBO

+ + div>fieldset>div:first-of-type]:py-0 [&>div]:py-0'} + /> + +
+ + )} +
+ } + modal + /> + ), + }) + .build(); +}; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/utils/create-form-schema-from-ui-elements.ts b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/utils/create-form-schema-from-ui-elements.ts new file mode 100644 index 0000000000..e84d7b4301 --- /dev/null +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useManageUbosBlock/utils/create-form-schema-from-ui-elements.ts @@ -0,0 +1,91 @@ +import { AnyObject } from '@ballerine/ui'; +import { RJSFSchema, UiSchema } from '@rjsf/utils'; + +export const createFormSchemaFromUIElements = (formElement: any) => { + const formSchema: RJSFSchema = { + type: formElement.options?.jsonFormDefinition?.type === 'array' ? 'array' : 'object', + required: formElement.options?.jsonFormDefinition?.required ?? [], + }; + + const uiSchema: UiSchema = { + 'ui:submitButtonOptions': { + norender: true, + }, + titleTemplate: 'blah', + }; + + if (formSchema.type === 'object') { + formSchema.properties = {}; + + (formElement.elements as any[])?.forEach(uiElement => { + if (!uiElement.options?.jsonFormDefinition) return; + + const elementDefinition = { + ...uiElement.options.jsonFormDefinition, + title: uiElement.options.label, + description: uiElement.options.description, + }; + + if (!formSchema.properties) { + formSchema.properties = {}; + } + + formSchema.properties[uiElement.name] = elementDefinition; + + uiSchema[uiElement.name] = { + ...uiElement?.options?.uiSchema, + 'ui:label': + (uiElement.options?.uiSchema || {})['ui:label'] === undefined + ? Boolean(uiElement?.options?.label) + : (uiElement.options?.uiSchema || {})['ui:label'], + 'ui:placeholder': uiElement?.options?.hint, + }; + }); + } + + if (formSchema.type === 'array') { + uiSchema.titleTemplate = formElement.options?.uiSchema?.titleTemplate as string; + uiSchema.addText = (formElement.options?.uiSchema?.addText as string) || undefined; + formSchema.items = { + type: 'object', + required: formElement.options?.jsonFormDefinition?.required, + title: formElement.options?.jsonFormDefinition?.title, + properties: {}, + }; + + uiSchema.items = { + 'ui:label': false, + } as AnyObject; + + (formElement.elements as any[])?.forEach(uiElement => { + if (!uiElement.options?.jsonFormDefinition) return; + + const elementDefinition = { + ...uiElement.options.jsonFormDefinition, + title: uiElement.options.label, + description: uiElement.options.description, + }; + + if (!(formSchema.items as RJSFSchema)?.properties) { + (formSchema.items as RJSFSchema).properties = {}; + } + + // @ts-ignore + (formSchema.items as RJSFSchema).properties[uiElement.name] = elementDefinition; + + uiSchema.items[uiElement.name] = { + ...uiElement?.options?.uiSchema, + 'ui:label': + (uiElement.options?.uiSchema || {})['ui:label'] === undefined + ? Boolean(uiElement?.options?.label) + : (uiElement.options?.uiSchema || {})['ui:label'], + 'ui:placeholder': uiElement?.options?.hint, + }; + }); + } + + return { + formSchema, + uiSchema, + }; +}; diff --git a/apps/backoffice-v2/src/lib/blocks/hooks/useUbosUserProvidedBlock/useUbosUserProvidedBlock.tsx b/apps/backoffice-v2/src/lib/blocks/hooks/useUbosUserProvidedBlock/useUbosUserProvidedBlock.tsx index ef29fe01bd..2233a92b1b 100644 --- a/apps/backoffice-v2/src/lib/blocks/hooks/useUbosUserProvidedBlock/useUbosUserProvidedBlock.tsx +++ b/apps/backoffice-v2/src/lib/blocks/hooks/useUbosUserProvidedBlock/useUbosUserProvidedBlock.tsx @@ -1,8 +1,47 @@ import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed'; -import { omitPropsFromObject } from '@/pages/Entity/hooks/useEntityLogic/utils'; +import { TextWithNAFallback } from '@ballerine/ui'; +import { createColumnHelper } from '@tanstack/react-table'; import { useMemo } from 'react'; -export const useUbosUserProvidedBlock = ubosUserProvided => { +export const useUbosUserProvidedBlock = ( + ubosUserProvided: Array<{ + name: string; + nationality: string; + identityNumber: string; + percentageOfOwnership: number; + email: string; + address: string; + }>, +) => { + const columnHelper = createColumnHelper<(typeof ubosUserProvided)[number]>(); + const columns = [ + columnHelper.accessor('name', { + header: 'Name', + }), + columnHelper.accessor('nationality', { + header: 'Nationality', + }), + columnHelper.accessor('identityNumber', { + header: 'Identity number', + }), + columnHelper.accessor('percentageOfOwnership', { + header: '% of Ownership', + cell: ({ getValue }) => { + const value = getValue(); + + return ( + {value || value === 0 ? `${value}%` : value} + ); + }, + }), + columnHelper.accessor('email', { + header: 'Email', + }), + columnHelper.accessor('address', { + header: 'Address', + }), + ]; + return useMemo(() => { if (Object.keys(ubosUserProvided ?? {}).length === 0) { return []; @@ -28,50 +67,8 @@ export const useUbosUserProvidedBlock = ubosUserProvided => { .addCell({ type: 'table', value: { - columns: [ - { - accessorKey: 'name', - header: 'Name', - }, - { - accessorKey: 'nationality', - header: 'Nationality', - }, - { - accessorKey: 'identityNumber', - header: 'Identity number', - }, - { - accessorKey: 'percentageOfOwnership', - header: '% of Ownership', - }, - { - accessorKey: 'email', - header: 'Email', - }, - { - accessorKey: 'address', - header: 'Address', - }, - ], - data: ubosUserProvided?.map( - ({ - firstName, - lastName, - nationalId: identityNumber, - additionalInfo, - percentageOfOwnership, - ...rest - }) => ({ - ...rest, - name: [firstName, lastName].filter(Boolean).join(' '), - address: additionalInfo?.fullAddress, - nationality: additionalInfo?.nationality, - percentageOfOwnership: additionalInfo?.percentageOfOwnership, - identityNumber, - ...omitPropsFromObject(additionalInfo, 'fullAddress', 'nationality'), - }), - ), + columns, + data: ubosUserProvided, }, }) .build() diff --git a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/DefaultBlocks.tsx b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/DefaultBlocks.tsx index e531d3b99f..dff1f7c663 100644 --- a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/DefaultBlocks.tsx +++ b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/DefaultBlocks.tsx @@ -8,7 +8,6 @@ import { NoBlocks } from '@/lib/blocks/components/NoBlocks/NoBlocks'; import { Link } from 'react-router-dom'; import { ScrollArea } from '@/common/components/molecules/ScrollArea/ScrollArea'; import { TabsContent } from '@/common/components/organisms/Tabs/Tabs.Content'; -import React from 'react'; import { camelCase } from 'string-ts'; export const DefaultBlocks = () => { @@ -38,7 +37,7 @@ export const DefaultBlocks = () => { ); })} - + {tabs.map(tab => { const tabName = camelCase(tab.name); diff --git a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/get-tabs-block-map.tsx b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/get-tabs-block-map.tsx index 7cc8930dab..7d3980c14a 100644 --- a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/get-tabs-block-map.tsx +++ b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/get-tabs-block-map.tsx @@ -54,6 +54,7 @@ export const getTabsToBlocksMap = ({ customDataBlock, amlWithContainerBlock, merchantScreeningBlock, + manageUbosBlock, ] = blocks; const defaultTabsMap = { @@ -86,6 +87,7 @@ export const getTabsToBlocksMap = ({ ...ubosUserProvidedBlock, ...ubosRegistryProvidedBlock, ...amlWithContainerBlock, + ...manageUbosBlock, ...(createKycBlocks(blocksCreationParams?.workflow as TWorkflowById) || []), ], [Tab.ASSOCIATED_COMPANIES]: [ diff --git a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useDefaultBlocksLogic/useDefaultBlocksLogic.tsx b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useDefaultBlocksLogic/useDefaultBlocksLogic.tsx index 3cc9e5e5cc..b89381528c 100644 --- a/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useDefaultBlocksLogic/useDefaultBlocksLogic.tsx +++ b/apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useDefaultBlocksLogic/useDefaultBlocksLogic.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/common/components/atoms/Button/Button'; import { MotionButton } from '@/common/components/molecules/MotionButton/MotionButton'; import { ctw } from '@/common/utils/ctw/ctw'; import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery'; @@ -37,9 +36,7 @@ import { useCaseDecision } from '@/pages/Entity/components/Case/hooks/useCaseDec import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState'; import { selectDirectorsDocuments } from '@/pages/Entity/selectors/selectDirectorsDocuments'; import { Send } from 'lucide-react'; -import React, { useCallback, useMemo } from 'react'; -import { toast } from 'sonner'; -import { useCurrentCaseQuery } from '@/pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery'; +import { useMemo } from 'react'; import { useWebsiteMonitoringReportBlock } from '@/lib/blocks/variants/WebsiteMonitoringBlocks/hooks/useWebsiteMonitoringReportBlock/useWebsiteMonitoringReportBlock'; import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed'; import { useAddressBlock } from '@/lib/blocks/hooks/useAddressBlock/useAddressBlock'; @@ -52,9 +49,16 @@ import { useObjectEntriesBlock } from '@/lib/blocks/hooks/useObjectEntriesBlock/ import { useAmlBlock } from '@/lib/blocks/components/AmlBlock/hooks/useAmlBlock/useAmlBlock'; import { associatedCompanyToWorkflowAdapter } from '@/lib/blocks/hooks/useAssosciatedCompaniesBlock/associated-company-to-workflow-adapter'; import { useMerchantScreeningBlock } from '@/lib/blocks/hooks/useMerchantScreeningBlock/useMerchantScreeningBlock'; +import { TWorkflowById } from '@/domains/workflows/fetchers'; const registryInfoWhitelist = ['open_corporates'] as const; +import { Button } from '@ballerine/ui'; +import { useCurrentCaseQuery } from '@/pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery'; +import { toast } from 'sonner'; +import { useCallback } from 'react'; +import { useManageUbosBlock } from '@/lib/blocks/hooks/useManageUbosBlock/useManageUbosBlock'; + export const useDefaultBlocksLogic = () => { const [{ activeTab }] = useSearchParamsByEntity(); const { search } = useLocation(); @@ -120,7 +124,7 @@ export const useDefaultBlocksLogic = () => { const { store, bank: bankDetails, - ubos: ubosUserProvided = [], + ubos: _ubosUserProvided, directors: directorsUserProvided = [], mainRepresentative, mainContact, @@ -297,6 +301,33 @@ export const useDefaultBlocksLogic = () => { const companySanctionsBlock = useCompanySanctionsBlock(companySanctions); + const childWorkflowToUboAdapter = (childWorkflow: TWorkflowById) => { + return { + name: [ + childWorkflow?.context?.entity?.data?.firstName, + childWorkflow?.context?.entity?.data?.lastName, + ] + .filter(Boolean) + .join(' '), + nationality: childWorkflow?.context?.entity?.data?.additionalInfo?.nationality, + email: childWorkflow?.context?.entity?.data?.email, + identityNumber: childWorkflow?.context?.entity?.data?.nationalId, + percentageOfOwnership: + childWorkflow?.context?.entity?.data?.percentageOfOwnership ?? + childWorkflow?.context?.entity?.data?.ownershipPercentage ?? + childWorkflow?.context?.entity?.data?.additionalInfo?.percentageOfOwnership ?? + childWorkflow?.context?.entity?.data?.additionalInfo?.ownershipPercentage, + address: childWorkflow?.context?.entity?.data?.additionalInfo?.fullAddress, + } satisfies Parameters[0][number]; + }; + + const ubosUserProvided = useMemo(() => { + return ( + workflow?.childWorkflows + ?.filter(childWorkflow => childWorkflow?.context?.entity?.variant === 'ubo') + ?.map(childWorkflowToUboAdapter) ?? [] + ); + }, [workflow?.childWorkflows]); const ubosUserProvidedBlock = useUbosUserProvidedBlock(ubosUserProvided); const ubosRegistryProvidedBlock = useUbosRegistryProvidedBlock({ @@ -308,6 +339,13 @@ export const useDefaultBlocksLogic = () => { isRequestTimedOut: workflow?.context?.pluginsOutput?.ubo?.isRequestTimedOut, }); + const manageUbosBlock = useManageUbosBlock({ + create: { + ...workflow?.workflowDefinition?.config?.ubos?.create, + enabled: workflow?.workflowDefinition?.config?.ubos?.create?.enabled ?? false, + }, + }); + const directorsUserProvidedBlock = useDirectorsUserProvidedBlock(directorsUserProvided); const directorsDocumentsBlocks = useDirectorsBlocks({ @@ -470,6 +508,7 @@ export const useDefaultBlocksLogic = () => { customDataBlock, amlWithContainerBlock, merchantScreeningBlock, + manageUbosBlock, ]; }, [ associatedCompaniesBlock, @@ -501,6 +540,7 @@ export const useDefaultBlocksLogic = () => { amlWithContainerBlock, merchantScreeningBlock, workflow?.context?.entity, + manageUbosBlock, ]); const { blocks, tabs } = useCaseBlocks({ 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/Entity/components/Case/components/CaseOverview/CaseOverview.tsx b/apps/backoffice-v2/src/pages/Entity/components/Case/components/CaseOverview/CaseOverview.tsx index fc50964494..f297ba3272 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/components/CaseOverview/CaseOverview.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/components/CaseOverview/CaseOverview.tsx @@ -54,7 +54,7 @@ export const CaseOverview = ({ processes }: { processes: string[] }) => { } return ( -
+
{workflow?.workflowDefinition?.config?.isCaseRiskOverviewEnabled && ( { 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,99 @@ 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..f74902bd8a 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 = range?.from ? dayjs(range.from).format('YYYY-MM-DD') : undefined; + const to = range?.to ? dayjs(range?.to).format('YYYY-MM-DD') : undefined; + + 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/CHANGELOG.md b/apps/kyb-app/CHANGELOG.md index 84b3716849..1437d7bc61 100644 --- a/apps/kyb-app/CHANGELOG.md +++ b/apps/kyb-app/CHANGELOG.md @@ -1,5 +1,46 @@ # kyb-app +## 0.3.98 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.55 + +## 0.3.97 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + - @ballerine/ui@0.5.54 + - @ballerine/workflow-browser-sdk@0.6.79 + +## 0.3.96 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.53 + +## 0.3.95 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.52 + +## 0.3.94 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/blocks@0.2.30 + - @ballerine/common@0.9.59 + - @ballerine/ui@0.5.51 + - @ballerine/workflow-browser-sdk@0.6.78 + ## 0.3.93 ### Patch Changes diff --git a/apps/kyb-app/package.json b/apps/kyb-app/package.json index 1ad628ec17..36d142a226 100644 --- a/apps/kyb-app/package.json +++ b/apps/kyb-app/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/kyb-app", "private": true, - "version": "0.3.93", + "version": "0.3.98", "type": "module", "scripts": { "dev": "vite", @@ -15,10 +15,10 @@ "test:dev": "vitest" }, "dependencies": { - "@ballerine/blocks": "0.2.29", - "@ballerine/common": "^0.9.58", - "@ballerine/ui": "0.5.50", - "@ballerine/workflow-browser-sdk": "0.6.77", + "@ballerine/blocks": "0.2.30", + "@ballerine/common": "^0.9.60", + "@ballerine/ui": "0.5.55", + "@ballerine/workflow-browser-sdk": "0.6.79", "@lukemorales/query-key-factory": "^1.0.3", "@radix-ui/react-icons": "^1.3.0", "@rjsf/core": "^5.9.0", @@ -64,8 +64,8 @@ "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config-react": "^2.0.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config-react": "^2.0.28", "@jest/globals": "^29.7.0", "@sentry/vite-plugin": "^2.9.0", "@testing-library/jest-dom": "^6.1.4", 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/apps/workflows-dashboard/CHANGELOG.md b/apps/workflows-dashboard/CHANGELOG.md index b241ebdf69..cb64c467ac 100644 --- a/apps/workflows-dashboard/CHANGELOG.md +++ b/apps/workflows-dashboard/CHANGELOG.md @@ -1,5 +1,14 @@ # @ballerine/workflows-dashboard +## 0.2.28 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + - @ballerine/ui@0.5.51 + ## 0.2.27 ### Patch Changes diff --git a/apps/workflows-dashboard/package.json b/apps/workflows-dashboard/package.json index 29e8cd441a..43731897ed 100644 --- a/apps/workflows-dashboard/package.json +++ b/apps/workflows-dashboard/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/workflows-dashboard", "private": false, - "version": "0.2.27", + "version": "0.2.28", "type": "module", "scripts": { "spellcheck": "cspell \"*\"", @@ -15,8 +15,8 @@ "test": "NODE_ENV=test jest" }, "dependencies": { - "@ballerine/common": "^0.9.58", - "@ballerine/ui": "^0.5.50", + "@ballerine/common": "^0.9.59", + "@ballerine/ui": "^0.5.51", "@lukemorales/query-key-factory": "^1.0.3", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-dialog": "1.0.4", @@ -63,8 +63,8 @@ "zod": "^3.22.3" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config-react": "^2.0.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config-react": "^2.0.28", "@cspell/cspell-types": "^6.31.1", "@types/axios": "^0.14.0", "@types/classnames": "^2.3.1", diff --git a/apps/workflows-dashboard/src/components/molecules/FacetedFilter/FacetedFilter.tsx b/apps/workflows-dashboard/src/components/molecules/FacetedFilter/FacetedFilter.tsx index 819b6bb7b3..cbddef246e 100644 --- a/apps/workflows-dashboard/src/components/molecules/FacetedFilter/FacetedFilter.tsx +++ b/apps/workflows-dashboard/src/components/molecules/FacetedFilter/FacetedFilter.tsx @@ -105,7 +105,7 @@ export function FacetedFilter({ title, options, value, onChange }: Props) { onChange([])}> - Clear filters + Clear Case List diff --git a/apps/workflows-dashboard/src/components/organisms/Header/header-navigation-links.ts b/apps/workflows-dashboard/src/components/organisms/Header/header-navigation-links.ts index 2ed6924140..3a106f148f 100644 --- a/apps/workflows-dashboard/src/components/organisms/Header/header-navigation-links.ts +++ b/apps/workflows-dashboard/src/components/organisms/Header/header-navigation-links.ts @@ -15,7 +15,7 @@ export const headerNavigationLinks: NavigationLink[] = [ }, { path: '/filters', - label: 'Filters', + label: 'Case Lists', }, { path: '/ui-definitions', diff --git a/apps/workflows-dashboard/src/pages/Filters/Filters.tsx b/apps/workflows-dashboard/src/pages/Filters/Filters.tsx index f59428f852..b4f2b5b19a 100644 --- a/apps/workflows-dashboard/src/pages/Filters/Filters.tsx +++ b/apps/workflows-dashboard/src/pages/Filters/Filters.tsx @@ -129,20 +129,20 @@ export const Filters = withFilters, Filter }; return ( - +

- + - Create New Filter + Create New Case List
setFilterName(e.target.value)} /> diff --git a/apps/workflows-dashboard/src/pages/WorkflowDefinition/WorkflowDefinition.tsx b/apps/workflows-dashboard/src/pages/WorkflowDefinition/WorkflowDefinition.tsx index d8d20b40cf..a985071841 100644 --- a/apps/workflows-dashboard/src/pages/WorkflowDefinition/WorkflowDefinition.tsx +++ b/apps/workflows-dashboard/src/pages/WorkflowDefinition/WorkflowDefinition.tsx @@ -18,35 +18,314 @@ import { useState } from 'react'; import { Button } from '@/components/atoms/Button'; export const VENDOR_DETAILS = { - 'dow-jones': { - logoUrl: 'https://cdn.ballerine.io/logos/Dow_Jones_Logo.png', - description: 'Dow Jones provides sanctions screening and risk data for individuals', + 'api-plugins': { + 'registry-information': { + title: 'Registry Information', + description: 'Company registry and business information services', + vendors: { + 'asia-verify': { + logoUrl: 'https://cdn.ballerine.io/logos/AsiaVerify_Logo.png', + description: + 'Company screening, UBO verification and registry information services focused on APAC region', + configExample: { + name: 'asiaVerifyRegistryInfo', + vendor: 'asia-verify', + pluginKind: 'registry-information', + stateNames: ['run_vendor_data'], + displayName: 'Asia Verify Registry Information', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + kyckr: { + logoUrl: 'https://cdn.ballerine.io/logos/kyckr-logo.png', + description: 'UBO verification and company registry information services', + configExample: { + name: 'kyckrRegistryInfo', + vendor: 'kyckr', + pluginKind: 'registry-information', + stateNames: ['run_vendor_data'], + displayName: 'Kyckr Registry Information', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + test: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Test vendor for development purposes', + configExample: { + name: 'testRegistryInfo', + vendor: 'test', + pluginKind: 'registry-information', + stateNames: ['run_vendor_data'], + displayName: 'Test Registry Information', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + }, + }, + 'individual-sanctions': { + title: 'Individual Sanctions', + description: 'Individual sanctions screening and risk assessment', + vendors: { + 'dow-jones': { + logoUrl: 'https://cdn.ballerine.io/logos/Dow_Jones_Logo.png', + description: 'Sanctions screening and risk data for individuals', + configExample: { + name: 'dowJonesSanctions', + vendor: 'dow-jones', + pluginKind: 'individual-sanctions', + stateNames: ['run_vendor_data'], + displayName: 'Dow Jones Individual Sanctions', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + 'comply-advantage': { + logoUrl: 'https://cdn.ballerine.io/logos/comply-advantage-logo.png', + description: 'AI-driven sanctions screening and monitoring for individuals', + configExample: { + name: 'complyAdvantageSanctions', + vendor: 'comply-advantage', + pluginKind: 'individual-sanctions', + stateNames: ['run_vendor_data'], + displayName: 'ComplyAdvantage Individual Sanctions', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + }, + }, + 'company-sanctions': { + title: 'Company Sanctions', + description: 'Company sanctions screening and monitoring', + vendors: { + test: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Test vendor for company sanctions screening', + configExample: { + name: 'companySanctions', + vendor: 'test', + pluginKind: 'company-sanctions', + stateNames: ['run_vendor_data'], + displayName: 'Company Sanctions', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + }, + }, + ubo: { + title: 'UBO Verification', + description: 'Ultimate Beneficial Owner verification services', + vendors: { + test: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Test vendor for UBO verification', + configExample: { + name: 'uboVerification', + vendor: 'test', + pluginKind: 'ubo', + stateNames: ['run_vendor_data'], + displayName: 'UBO Check', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + }, + }, + 'merchant-monitoring': { + title: 'Merchant Monitoring', + description: 'Ongoing merchant monitoring and risk assessment', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Merchant monitoring and risk assessment service', + configExample: { + name: 'merchantMonitoring', + vendor: 'ballerine', + pluginKind: 'merchant-monitoring', + stateNames: ['run_merchant_monitoring'], + displayName: 'Merchant Monitoring', + errorAction: 'MERCHANT_MONITORING_FAILED', + successAction: 'MERCHANT_MONITORING_SUCCESS', + merchantMonitoringQualityControl: false, + }, + }, + }, + }, + 'mastercard-merchant-screening': { + title: 'Mastercard Merchant Screening', + description: 'Merchant screening via Mastercard services', + vendors: { + mastercard: { + logoUrl: 'https://cdn.ballerine.io/logos/Mastercard%20logo.svg', + description: 'Mastercard merchant screening service', + configExample: { + name: 'merchantScreening', + vendor: 'mastercard', + pluginKind: 'mastercard-merchant-screening', + stateNames: ['run_vendor_data'], + displayName: 'Merchant Screening', + errorAction: 'VENDOR_DONE', + successAction: 'VENDOR_DONE', + }, + }, + }, + }, + 'kyc-session': { + title: 'KYC Session', + description: 'Identity verification and KYC services', + vendors: { + veriff: { + logoUrl: 'https://cdn.ballerine.io/logos/Veriff_logo.svg.png', + description: 'KYC verification and identity proofing services', + configExample: { + name: 'veriffKyc', + vendor: 'veriff', + pluginKind: 'kyc-session', + stateNames: ['run_kyc'], + displayName: 'Veriff KYC Session', + errorAction: 'KYC_FAILED', + successAction: 'KYC_SUCCESS', + }, + }, + }, + }, }, - 'comply-advantage': { - logoUrl: 'https://cdn.ballerine.io/logos/comply-advantage-logo.png', - description: - 'ComplyAdvantage offers AI-driven sanctions screening and monitoring for individuals', - }, - 'asia-verify': { - logoUrl: 'https://cdn.ballerine.io/logos/AsiaVerify_Logo.png', - description: - 'AsiaVerify provides company screening, UBO verification and registry information services focused on APAC region', - }, - veriff: { - logoUrl: 'https://cdn.ballerine.io/logos/Veriff_logo.svg.png', - description: 'Veriff provides KYC verification and identity proofing services', - }, - ballerine: { - logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', - description: 'Ballerine provides merchant monitoring services', - }, - kyckr: { - logoUrl: 'https://cdn.ballerine.io/logos/kyckr-logo.png', - description: 'Kyckr provides UBO verification and company registry information services', - }, - test: { - logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', - description: 'Test vendor for development purposes', + 'common-plugins': { + 'template-email': { + title: 'Email Templates', + description: 'Email template services', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Email template service', + configExample: { + name: 'invitation-email', + template: 'invitation', + pluginKind: 'template-email', + stateNames: ['collection_invite'], + errorAction: 'INVITATION_FAILURE', + successAction: 'INVITATION_SENT', + }, + }, + }, + }, + 'risk-rules': { + title: 'Risk Rules', + description: 'Risk assessment rules engine', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Risk rules engine service', + configExample: { + name: 'riskEvaluation', + pluginKind: 'riskRules', + stateNames: ['manual_review', 'run_vendor_data'], + rulesSource: { + source: 'notion', + databaseId: 'd29390ac964b45b1a79ef45eed735a77', + }, + }, + }, + }, + }, + 'child-workflow': { + title: 'Child Workflows', + description: 'Child workflow management', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Child workflow service', + configExample: { + name: 'veriff_kyc_child_plugin', + initEvent: 'start', + pluginKind: 'child', + definitionId: 'kyc_email_session_example', + }, + }, + }, + }, + 'dispatch-event': { + title: 'Event Dispatch', + description: 'Event dispatch service', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Event dispatch service', + configExample: { + name: 'dispatchEvent', + pluginKind: 'dispatch-event', + stateNames: ['dispatch_event'], + eventName: 'CUSTOM_EVENT', + }, + }, + }, + }, + iterative: { + title: 'Iterative', + description: 'Iterative plugin service', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Iterative plugin service', + configExample: { + name: 'ubos_iterative', + pluginKind: 'iterative', + stateNames: ['run_ubos'], + iterateOn: [ + { + mapping: 'entity.data.additionalInfo.contacts', + transformer: 'jmespath', + }, + ], + errorAction: 'FAILED_EMAIL_SENT_TO_UBOS', + successAction: 'EMAIL_SENT_TO_UBOS', + }, + }, + }, + }, + transformer: { + title: 'Transformer', + description: 'Data transformation service', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'Data transformation service', + configExample: { + name: 'transformData', + pluginKind: 'transformer', + stateNames: ['transform_data'], + transformers: [ + { + mapping: '{transformed: @}', + transformer: 'jmespath', + }, + ], + }, + }, + }, + }, + 'attach-ui-definition': { + title: 'UI Definition', + description: 'UI definition attachment service', + vendors: { + ballerine: { + logoUrl: 'https://cdn.ballerine.io/logos/ballerine-logo.png', + description: 'UI definition attachment service', + configExample: { + name: 'Attach APAC Flow UI', + pluginKind: 'attach-ui-definition', + stateNames: ['collection_flow'], + errorAction: 'INVITATION_FAILURE', + uiDefinitionId: 'cm500fmsi000grukeo31qdigh', + expireInMinutes: 21600, + }, + }, + }, + }, }, } as const; @@ -63,7 +342,7 @@ export const WorkflowDefinition = () => { const [isIntegrationCatalogOpen, setIsIntegrationCatalogOpen] = useState(false); const copyToClipboard = (text: string) => { - navigator.clipboard.writeText(text); + void navigator.clipboard.writeText(text); }; if (isLoading) { @@ -157,7 +436,7 @@ export const WorkflowDefinition = () => {
{isIntegrationCatalogOpen && ( @@ -169,7 +448,7 @@ export const WorkflowDefinition = () => {
-

Integration Catalog

+

Integrations Catalog

-
- {Object.entries(VENDOR_DETAILS).map(([vendorKey, vendorInfo]) => ( -
-
-
- {vendorKey} { - e.currentTarget.src = - 'https://cdn.ballerine.io/logos/ballerine-logo.png'; - }} - /> -
-
-
-

- {vendorKey - .split('-') - .map( - word => - word.charAt(0).toUpperCase() + word.slice(1), - ) - .join(' ')} -

- +
+ {Object.entries(VENDOR_DETAILS['api-plugins']).map( + ([pluginKind, pluginInfo]) => { + return ( +
+
+
+
+

+ {pluginInfo.title} +

+

+ {pluginInfo.description} +

+
+
+
+ {Object.entries(pluginInfo.vendors).map( + ([vendorKey, vendorInfo]) => ( +
+
+
+ {vendorKey} { + e.currentTarget.src = + 'https://cdn.ballerine.io/logos/ballerine-logo.png'; + }} + /> +
+
+
+

+ {vendorKey + .split('-') + .map( + word => + word.charAt(0).toUpperCase() + + word.slice(1), + ) + .join(' ')} +

+ +
+

+ {vendorInfo.description} +

+
+
+ +
+
+ + Configuration Example + + +
+
+                                                    
+                                                      {JSON.stringify(
+                                                        vendorInfo.configExample,
+                                                        null,
+                                                        2,
+                                                      )}
+                                                    
+                                                  
+
+ + {vendorInfo.configExample.stateNames?.length > + 0 && ( +
+ {vendorInfo.configExample.stateNames?.map( + (state: string) => ( + + {state} + + ), + )} +
+ )} + +
+ {vendorInfo.configExample.successAction && ( +
+ + Success: + + + {vendorInfo.configExample.successAction} + +
+ )} + + {vendorInfo.configExample.errorAction && ( +
+ + Error: + + + {vendorInfo.configExample.errorAction} + +
+ )} +
+
+ ), + )}
-

- {vendorInfo.description} -

-
-
- ))} + ); + }, + )}
@@ -292,7 +682,9 @@ export const WorkflowDefinition = () => { ) : plugin.vendor ? ( {plugin.vendor}", - "version": "0.2.29", + "version": "0.2.30", "description": "blocks", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -42,8 +42,8 @@ "@babel/preset-env": "7.16.11", "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config": "^1.1.28", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", @@ -91,6 +91,6 @@ "vitest": "^0.33.0" }, "dependencies": { - "@ballerine/common": "^0.9.58" + "@ballerine/common": "^0.9.59" } } diff --git a/packages/common/CHANGELOG.md b/packages/common/CHANGELOG.md index 3b649c5132..4e0fbe6ab9 100644 --- a/packages/common/CHANGELOG.md +++ b/packages/common/CHANGELOG.md @@ -1,5 +1,17 @@ # @ballerine/common +## 0.9.60 + +### Patch Changes + +- Updated button with disabled state + +## 0.9.59 + +### Patch Changes + +- core + ## 0.9.58 ### Patch Changes diff --git a/packages/common/package.json b/packages/common/package.json index 1c738eb968..9d837e1cef 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -2,7 +2,7 @@ "private": false, "name": "@ballerine/common", "author": "Ballerine ", - "version": "0.9.58", + "version": "0.9.60", "description": "common", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -38,8 +38,8 @@ "@babel/core": "7.17.9", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", diff --git a/packages/common/src/schemas/documents/merchant-screening-plugin-schema.ts b/packages/common/src/schemas/documents/merchant-screening-plugin-schema.ts index 08782e1260..4d23431bab 100644 --- a/packages/common/src/schemas/documents/merchant-screening-plugin-schema.ts +++ b/packages/common/src/schemas/documents/merchant-screening-plugin-schema.ts @@ -74,11 +74,13 @@ const AddressSchema = Type.Object({ }); const DriversLicenseSchema = Type.Object({ - Number: Type.String({ - description: 'The drivers license number of a principal owner.', - example: 'M15698025', - maxLength: 25, - }), + Number: Type.Optional( + Type.String({ + description: 'The drivers license number of a principal owner.', + example: 'M15698025', + maxLength: 25, + }), + ), CountrySubdivision: Type.String({ description: 'The abbreviated state or province code for a merchant location (only supported for US and Canada merchants).', diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 0308545f0e..c0ddf1a648 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,11 @@ # @ballerine/config +## 1.1.28 + +### Patch Changes + +- core + ## 1.1.27 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 34febe082b..ca693ec25e 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@ballerine/config", - "version": "1.1.27", + "version": "1.1.28", "description": "", "main": "index.js", "scripts": {}, diff --git a/packages/eslint-config-react/CHANGELOG.md b/packages/eslint-config-react/CHANGELOG.md index c90b954605..52ba9532b8 100644 --- a/packages/eslint-config-react/CHANGELOG.md +++ b/packages/eslint-config-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @ballerine/eslint-config-react +## 2.0.28 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/eslint-config@1.1.28 + ## 2.0.27 ### Patch Changes diff --git a/packages/eslint-config-react/package.json b/packages/eslint-config-react/package.json index b459b25f27..db1fa01468 100644 --- a/packages/eslint-config-react/package.json +++ b/packages/eslint-config-react/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@ballerine/eslint-config-react", - "version": "2.0.27", + "version": "2.0.28", "description": "", "main": "index.js", "scripts": {}, @@ -10,7 +10,7 @@ "license": "ISC", "peerDependencies": { "eslint-plugin-react": "^7.33.2", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/eslint-config": "^1.1.28", "eslint-plugin-react-hooks": "^4.6.0" } } diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md index 552473a0cf..a77910c376 100644 --- a/packages/eslint-config/CHANGELOG.md +++ b/packages/eslint-config/CHANGELOG.md @@ -1,5 +1,11 @@ # @ballerine/eslint-config +## 1.1.28 + +### Patch Changes + +- core + ## 1.1.27 ### Patch Changes diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 712462dcc3..46b0c9179e 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@ballerine/eslint-config", - "version": "1.1.27", + "version": "1.1.28", "description": "", "main": "index.js", "scripts": {}, diff --git a/packages/react-pdf-toolkit/CHANGELOG.md b/packages/react-pdf-toolkit/CHANGELOG.md index 685a9a1fe4..b86f999341 100644 --- a/packages/react-pdf-toolkit/CHANGELOG.md +++ b/packages/react-pdf-toolkit/CHANGELOG.md @@ -1,5 +1,42 @@ # @ballerine/react-pdf-toolkit +## 1.2.55 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.55 + +## 1.2.54 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.54 + +## 1.2.53 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.53 + +## 1.2.52 + +### Patch Changes + +- Updated dependencies + - @ballerine/ui@0.5.52 + +## 1.2.51 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/config@1.1.28 + - @ballerine/ui@0.5.51 + ## 1.2.50 ### Patch Changes diff --git a/packages/react-pdf-toolkit/package.json b/packages/react-pdf-toolkit/package.json index 1d6861ccaa..ab638f9861 100644 --- a/packages/react-pdf-toolkit/package.json +++ b/packages/react-pdf-toolkit/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/react-pdf-toolkit", "private": false, - "version": "1.2.50", + "version": "1.2.55", "types": "./dist/build.d.ts", "main": "./dist/react-pdf-toolkit.js", "module": "./dist/react-pdf-toolkit.mjs", @@ -26,8 +26,8 @@ "build-storybook": "storybook build" }, "dependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/ui": "0.5.50", + "@ballerine/config": "^1.1.28", + "@ballerine/ui": "0.5.55", "@react-pdf/renderer": "^3.1.14", "@sinclair/typebox": "^0.31.7", "ajv": "^8.12.0", 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/packages/rules-engine/CHANGELOG.md b/packages/rules-engine/CHANGELOG.md index fd2c171a9e..dfbd85003d 100644 --- a/packages/rules-engine/CHANGELOG.md +++ b/packages/rules-engine/CHANGELOG.md @@ -1,5 +1,11 @@ # @ballerine/rules-engine-lib +## 0.5.28 + +### Patch Changes + +- core + ## 0.5.27 ### Patch Changes diff --git a/packages/rules-engine/package.json b/packages/rules-engine/package.json index 8e82449925..df8123bc75 100644 --- a/packages/rules-engine/package.json +++ b/packages/rules-engine/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/rules-engine-lib", "author": "Ballerine ", - "version": "0.5.27", + "version": "0.5.28", "description": "rules-engine-lib", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -34,9 +34,9 @@ "@babel/core": "7.17.9", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", + "@ballerine/config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/eslint-config": "^1.1.28", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-node-resolve": "13.2.1", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index c03b9ee462..840c22b243 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,39 @@ # @ballerine/ui +## 0.5.55 + +### Patch Changes + +- Fixed phone input styling + +## 0.5.54 + +### Patch Changes + +- Updated button with disabled state +- Updated dependencies + - @ballerine/common@0.9.60 + +## 0.5.53 + +### Patch Changes + +- added command.loading + +## 0.5.52 + +### Patch Changes + +- add href attribute to anchor-if-url component + +## 0.5.51 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + ## 0.5.50 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index f6337f48ec..bc50b531e5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/ui", "private": false, - "version": "0.5.50", + "version": "0.5.55", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -26,7 +26,7 @@ "test": "vitest run" }, "dependencies": { - "@ballerine/common": "^0.9.58", + "@ballerine/common": "^0.9.60", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@mui/material": "^5.14.2", @@ -65,8 +65,8 @@ "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config-react": "^2.0.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config-react": "^2.0.28", "@cspell/cspell-types": "^6.31.1", "@storybook/addon-essentials": "^7.0.26", "@storybook/addon-interactions": "^7.0.26", diff --git a/packages/ui/src/components/atoms/AnchorIfUrl/AnchorIfUrl.tsx b/packages/ui/src/components/atoms/AnchorIfUrl/AnchorIfUrl.tsx index b8ed48194f..87a77229da 100644 --- a/packages/ui/src/components/atoms/AnchorIfUrl/AnchorIfUrl.tsx +++ b/packages/ui/src/components/atoms/AnchorIfUrl/AnchorIfUrl.tsx @@ -21,7 +21,7 @@ export const AnchorIfUrl: TAnchorIfUrl = forwardRef( if (checkIsUrl(children)) { return ( - + {children} ); diff --git a/packages/ui/src/components/atoms/Button/Button.tsx b/packages/ui/src/components/atoms/Button/Button.tsx index f4263366e2..7c2bbd66ca 100644 --- a/packages/ui/src/components/atoms/Button/Button.tsx +++ b/packages/ui/src/components/atoms/Button/Button.tsx @@ -4,7 +4,7 @@ import { cva, type VariantProps } from 'class-variance-authority'; import { ctw } from '@/common/utils/ctw'; const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50', { variants: { variant: { diff --git a/packages/ui/src/components/atoms/Command/Command.tsx b/packages/ui/src/components/atoms/Command/Command.tsx index c7e4664d75..d1f73950a0 100644 --- a/packages/ui/src/components/atoms/Command/Command.tsx +++ b/packages/ui/src/components/atoms/Command/Command.tsx @@ -20,6 +20,7 @@ const Command = React.forwardRef< {...props} /> )); + Command.displayName = CommandPrimitive.displayName; type CommandDialogProps = DialogProps; @@ -103,6 +104,7 @@ const CommandSeparator = React.forwardRef< {...props} /> )); + CommandSeparator.displayName = CommandPrimitive.Separator.displayName; const CommandItem = React.forwardRef< @@ -129,8 +131,13 @@ const CommandShortcut = ({ className, ...props }: React.HTMLAttributes ); }; + CommandShortcut.displayName = 'CommandShortcut'; +export const CommandLoading = ({ children, ...props }: React.HTMLAttributes) => { + return {children}; +}; + export { Command, CommandDialog, diff --git a/packages/ui/src/components/atoms/inputs/PhoneNumberInput/PhoneNumberInput.tsx b/packages/ui/src/components/atoms/inputs/PhoneNumberInput/PhoneNumberInput.tsx index cbe559d057..1d906e123b 100644 --- a/packages/ui/src/components/atoms/inputs/PhoneNumberInput/PhoneNumberInput.tsx +++ b/packages/ui/src/components/atoms/inputs/PhoneNumberInput/PhoneNumberInput.tsx @@ -27,12 +27,12 @@ export const PhoneNumberInput = (props: PhoneNumberInputProps) => { {...restProps} disabled={disabled} disableSearchIcon={disableSearchIcon} - containerClass="flex items-center border border-input h-9 focus-within:ring-ring focus-within:ring-1 rounded-md font-inter disabled:cursor-not-allowed disabled:opacity-50" - inputClass="w-full h-8 border-none outline-none disabled:cursor-not-allowed disabled:opacity-50" + containerClass="flex items-center border border-input focus-within:ring-ring focus-within:ring-1 rounded-md font-inter disabled:cursor-not-allowed disabled:opacity-50" + inputClass="w-full h-8 !border-none outline-none disabled:cursor-not-allowed disabled:opacity-50" searchClass={styles.searchInput} inputProps={{ ...restProps.inputProps, 'data-testid': testId }} buttonClass={clsx( - 'border-none rounded-l-md', + '!border-none rounded-l-md', { 'cursor-not-allowed opacity-50': disabled }, styles.hiddenArrow, styles.flagCenter, diff --git a/packages/ui/src/components/templates/report/components/AdsAndSocialMedia/AdsAndSocialMedia.tsx b/packages/ui/src/components/templates/report/components/AdsAndSocialMedia/AdsAndSocialMedia.tsx index 9f4c9a95c4..c033385f22 100644 --- a/packages/ui/src/components/templates/report/components/AdsAndSocialMedia/AdsAndSocialMedia.tsx +++ b/packages/ui/src/components/templates/report/components/AdsAndSocialMedia/AdsAndSocialMedia.tsx @@ -2,7 +2,6 @@ import React, { FunctionComponent } from 'react'; import { Card, CardContent, CardHeader } from '@/components'; import { TextWithNAFallback } from '@/components/atoms/TextWithNAFallback'; import { ctw } from '@/common'; -import { AdExample } from '../AdExample'; import { AdImageWithLink } from '../AdImageWithLink'; import { toTitleCase } from 'string-ts'; import { AnchorIfUrl } from '@/components/atoms/AnchorIfUrl'; @@ -87,18 +86,19 @@ export const AdsAndSocialMedia: FunctionComponent<{
- - Related Ads - -
- {!!relatedAdsImages?.length && - relatedAdsImages.map(({ src, link }, index) => ( - - ))} - {!relatedAdsImages?.length && <>No ads detected.} -
-
-
+ {/*Hiding this for now, will be added back in later*/} + {/**/} + {/* Related Ads*/} + {/* */} + {/*
*/} + {/* {!!relatedAdsImages?.length &&*/} + {/* relatedAdsImages.map(({ src, link }, index) => (*/} + {/* */} + {/* ))}*/} + {/* {!relatedAdsImages?.length && <>No ads detected.}*/} + {/*
*/} + {/*
*/} + {/*
*/}
); }; diff --git a/packages/workflow-core/CHANGELOG.md b/packages/workflow-core/CHANGELOG.md index 76f7577b0f..19e48bf832 100644 --- a/packages/workflow-core/CHANGELOG.md +++ b/packages/workflow-core/CHANGELOG.md @@ -1,5 +1,20 @@ # @ballerine/workflow-core +## 0.6.79 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + +## 0.6.78 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + ## 0.6.77 ### Patch Changes diff --git a/packages/workflow-core/package.json b/packages/workflow-core/package.json index 39ac9176cd..df3344ef2b 100644 --- a/packages/workflow-core/package.json +++ b/packages/workflow-core/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/workflow-core", "author": "Ballerine ", - "version": "0.6.77", + "version": "0.6.79", "description": "workflow-core", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -31,7 +31,7 @@ "node": ">=12" }, "dependencies": { - "@ballerine/common": "0.9.58", + "@ballerine/common": "0.9.60", "ajv": "^8.12.0", "country-state-city": "^3.1.4", "i18n-iso-countries": "^7.6.0", @@ -48,8 +48,8 @@ "@babel/core": "7.17.9", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81cbcc56a4..8f83642efc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,22 +73,22 @@ importers: apps/backoffice-v2: dependencies: '@ballerine/blocks': - specifier: 0.2.29 + specifier: 0.2.30 version: link:../../packages/blocks '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../../packages/common '@ballerine/react-pdf-toolkit': - specifier: ^1.2.50 + specifier: ^1.2.54 version: link:../../packages/react-pdf-toolkit '@ballerine/ui': - specifier: ^0.5.50 + specifier: ^0.5.54 version: link:../../packages/ui '@ballerine/workflow-browser-sdk': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../sdks/workflow-browser-sdk '@ballerine/workflow-node-sdk': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../sdks/workflow-node-sdk '@botpress/webchat': specifier: ^2.1.10 @@ -359,10 +359,10 @@ importers: version: 3.23.4 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config-react': - specifier: ^2.0.27 + specifier: ^2.0.28 version: link:../../packages/eslint-config-react '@cspell/cspell-types': specifier: ^6.31.1 @@ -518,16 +518,16 @@ importers: apps/kyb-app: dependencies: '@ballerine/blocks': - specifier: 0.2.29 + specifier: 0.2.30 version: link:../../packages/blocks '@ballerine/common': - specifier: ^0.9.58 + specifier: ^0.9.60 version: link:../../packages/common '@ballerine/ui': - specifier: 0.5.50 + specifier: 0.5.55 version: link:../../packages/ui '@ballerine/workflow-browser-sdk': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../sdks/workflow-browser-sdk '@lukemorales/query-key-factory': specifier: ^1.0.3 @@ -660,10 +660,10 @@ importers: version: 3.23.4 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config-react': - specifier: ^2.0.27 + specifier: ^2.0.28 version: link:../../packages/eslint-config-react '@jest/globals': specifier: ^29.7.0 @@ -768,10 +768,10 @@ importers: apps/workflows-dashboard: dependencies: '@ballerine/common': - specifier: ^0.9.58 + specifier: ^0.9.59 version: link:../../packages/common '@ballerine/ui': - specifier: ^0.5.50 + specifier: ^0.5.51 version: link:../../packages/ui '@lukemorales/query-key-factory': specifier: ^1.0.3 @@ -907,10 +907,10 @@ importers: version: 3.22.4 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config-react': - specifier: ^2.0.27 + specifier: ^2.0.28 version: link:../../packages/eslint-config-react '@cspell/cspell-types': specifier: ^6.31.1 @@ -1000,10 +1000,10 @@ importers: examples/headless-example: dependencies: '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../../packages/common '@ballerine/workflow-browser-sdk': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../sdks/workflow-browser-sdk '@felte/reporter-svelte': specifier: ^1.1.5 @@ -1097,7 +1097,7 @@ importers: examples/report-generation-example: dependencies: '@ballerine/react-pdf-toolkit': - specifier: ^1.2.50 + specifier: ^1.2.51 version: link:../../packages/react-pdf-toolkit react: specifier: ^18.2.0 @@ -1140,7 +1140,7 @@ importers: packages/blocks: dependencies: '@ballerine/common': - specifier: ^0.9.58 + specifier: ^0.9.59 version: link:../common devDependencies: '@babel/core': @@ -1156,10 +1156,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../eslint-config '@rollup/plugin-babel': specifier: 5.3.1 @@ -1337,10 +1337,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -1489,7 +1489,7 @@ importers: packages/eslint-config-react: dependencies: '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../eslint-config eslint-plugin-react: specifier: ^7.33.2 @@ -1501,10 +1501,10 @@ importers: packages/react-pdf-toolkit: dependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/ui': - specifier: 0.5.50 + specifier: 0.5.55 version: link:../ui '@react-pdf/renderer': specifier: ^3.1.14 @@ -1623,10 +1623,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -1728,7 +1728,7 @@ importers: packages/ui: dependencies: '@ballerine/common': - specifier: ^0.9.58 + specifier: ^0.9.60 version: link:../common '@emotion/react': specifier: ^11.11.1 @@ -1840,10 +1840,10 @@ importers: version: 3.23.4 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/eslint-config-react': - specifier: ^2.0.27 + specifier: ^2.0.28 version: link:../eslint-config-react '@cspell/cspell-types': specifier: ^6.31.1 @@ -1951,7 +1951,7 @@ importers: packages/workflow-core: dependencies: '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../common ajv: specifier: ^8.12.0 @@ -1997,10 +1997,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -2141,7 +2141,7 @@ importers: sdks/web-ui-sdk: dependencies: '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../../packages/common '@zerodevx/svelte-toast': specifier: ^0.8.0 @@ -2268,10 +2268,10 @@ importers: sdks/workflow-browser-sdk: dependencies: '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../../packages/common '@ballerine/workflow-core': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../packages/workflow-core xstate: specifier: ^4.37.0 @@ -2287,10 +2287,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -2410,7 +2410,7 @@ importers: sdks/workflow-node-sdk: dependencies: '@ballerine/workflow-core': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../packages/workflow-core json-logic-js: specifier: ^2.0.2 @@ -2429,10 +2429,10 @@ importers: specifier: 7.16.7 version: 7.16.7(@babel/core@7.17.9) '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -2655,13 +2655,13 @@ importers: specifier: 3.347.1 version: 3.347.1 '@ballerine/common': - specifier: 0.9.58 + specifier: 0.9.60 version: link:../../packages/common '@ballerine/workflow-core': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../packages/workflow-core '@ballerine/workflow-node-sdk': - specifier: 0.6.77 + specifier: 0.6.79 version: link:../../sdks/workflow-node-sdk '@faker-js/faker': specifier: ^7.6.0 @@ -2839,10 +2839,10 @@ importers: version: 3.23.4 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/eslint-config '@cspell/cspell-types': specifier: ^6.31.1 @@ -3004,7 +3004,7 @@ importers: specifier: ^4.0.0 version: 4.0.0(astro@3.3.3)(tailwindcss@3.3.5)(ts-node@10.9.1) '@ballerine/common': - specifier: ^0.9.58 + specifier: ^0.9.60 version: link:../../packages/common astro: specifier: 3.3.3 @@ -3017,10 +3017,10 @@ importers: version: 0.14.5 devDependencies: '@ballerine/config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/config '@ballerine/eslint-config': - specifier: ^1.1.27 + specifier: ^1.1.28 version: link:../../packages/eslint-config eslint: specifier: ^8.46.0 @@ -4815,7 +4815,7 @@ packages: '@babel/traverse': 7.25.6 '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.6 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -7332,7 +7332,7 @@ packages: '@babel/parser': 7.25.6 '@babel/template': 7.25.0 '@babel/types': 7.25.6 - debug: 4.3.6 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -9676,7 +9676,7 @@ packages: deprecated: Use @eslint/config-array instead dependencies: '@humanwhocodes/object-schema': 2.0.2 - debug: 4.3.6 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -20287,7 +20287,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.3) '@types/babel__core': 7.20.4 react-refresh: 0.14.0 - vite: 4.5.3(@types/node@20.9.2) + vite: 4.5.3(@types/node@18.17.19) transitivePeerDependencies: - supports-color dev: true @@ -24023,7 +24023,6 @@ packages: optional: true dependencies: ms: 2.1.3 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -24598,7 +24597,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-easy-sort: 1.6.0(react-dom@18.2.0)(react@18.2.0) - tailwind-merge: 2.5.5 + tailwind-merge: 2.6.0 tsup: 6.7.0(postcss@8.4.41)(ts-node@10.9.1)(typescript@5.1.6) transitivePeerDependencies: - '@swc/core' @@ -24796,6 +24795,7 @@ packages: /es-module-lexer@1.4.1: resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + dev: false /es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} @@ -25388,7 +25388,7 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.3) debug: 3.2.7 eslint: 8.54.0 eslint-import-resolver-node: 0.3.9 @@ -25553,7 +25553,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.5) + '@typescript-eslint/parser': 5.62.0(eslint@8.54.0)(typescript@4.9.3) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -26229,7 +26229,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.6 + debug: 4.4.0 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -36367,8 +36367,8 @@ packages: resolution: {integrity: sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==} dev: false - /tailwind-merge@2.5.5: - resolution: {integrity: sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==} + /tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} dev: false /tailwindcss-animate@1.0.5(tailwindcss@3.3.5): @@ -38384,7 +38384,7 @@ packages: strip-ansi: 6.0.1 tiny-invariant: 1.3.1 typescript: 5.1.6 - vite: 4.5.3(@types/node@20.9.2) + vite: 4.5.3(@types/node@18.17.19) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 @@ -38515,7 +38515,7 @@ packages: kolorist: 1.8.0 sirv: 2.0.3 ufo: 1.3.2 - vite: 4.5.3(@types/node@20.9.2) + vite: 4.5.3(@types/node@18.17.19) transitivePeerDependencies: - rollup - supports-color @@ -38594,7 +38594,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) globrex: 0.1.2 tsconfck: 2.1.2(typescript@5.1.6) - vite: 4.5.3(@types/node@20.9.2) + vite: 4.5.3(@types/node@18.17.19) transitivePeerDependencies: - supports-color - typescript @@ -39327,7 +39327,7 @@ packages: browserslist: 4.23.3 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 - es-module-lexer: 1.4.1 + es-module-lexer: 1.5.4 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 diff --git a/sdks/web-ui-sdk/CHANGELOG.md b/sdks/web-ui-sdk/CHANGELOG.md index a9c09f33a4..ae9749a9a3 100644 --- a/sdks/web-ui-sdk/CHANGELOG.md +++ b/sdks/web-ui-sdk/CHANGELOG.md @@ -1,5 +1,20 @@ # web-ui-sdk +## 1.5.61 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + +## 1.5.60 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + ## 1.5.59 ### Patch Changes diff --git a/sdks/web-ui-sdk/package.json b/sdks/web-ui-sdk/package.json index 38e11d3052..26c1967532 100644 --- a/sdks/web-ui-sdk/package.json +++ b/sdks/web-ui-sdk/package.json @@ -21,7 +21,7 @@ "types": "dist/index.d.ts", "name": "@ballerine/web-ui-sdk", "private": false, - "version": "1.5.59", + "version": "1.5.61", "type": "module", "files": [ "dist" @@ -96,7 +96,7 @@ "vitest": "^0.24.5" }, "dependencies": { - "@ballerine/common": "0.9.58", + "@ballerine/common": "0.9.60", "@zerodevx/svelte-toast": "^0.8.0", "compressorjs": "^1.1.1", "deepmerge": "^4.3.0", diff --git a/sdks/workflow-browser-sdk/CHANGELOG.md b/sdks/workflow-browser-sdk/CHANGELOG.md index 98664fc202..a5ee246a21 100644 --- a/sdks/workflow-browser-sdk/CHANGELOG.md +++ b/sdks/workflow-browser-sdk/CHANGELOG.md @@ -1,5 +1,22 @@ # @ballerine/workflow-browser-sdk +## 0.6.79 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + - @ballerine/workflow-core@0.6.79 + +## 0.6.78 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + - @ballerine/workflow-core@0.6.78 + ## 0.6.77 ### Patch Changes diff --git a/sdks/workflow-browser-sdk/package.json b/sdks/workflow-browser-sdk/package.json index 192eb83656..211691286d 100644 --- a/sdks/workflow-browser-sdk/package.json +++ b/sdks/workflow-browser-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/workflow-browser-sdk", "author": "Ballerine ", - "version": "0.6.77", + "version": "0.6.79", "description": "workflow-browser-sdk", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -33,17 +33,17 @@ "node": ">=12" }, "dependencies": { - "@ballerine/common": "0.9.58", - "@ballerine/workflow-core": "0.6.77", + "@ballerine/common": "0.9.60", + "@ballerine/workflow-core": "0.6.79", "xstate": "^4.37.0" }, "devDependencies": { "@babel/core": "7.17.9", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", + "@ballerine/config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/eslint-config": "^1.1.28", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", diff --git a/sdks/workflow-node-sdk/CHANGELOG.md b/sdks/workflow-node-sdk/CHANGELOG.md index 6eb421c38c..69e2a50ed0 100644 --- a/sdks/workflow-node-sdk/CHANGELOG.md +++ b/sdks/workflow-node-sdk/CHANGELOG.md @@ -1,5 +1,19 @@ # @ballerine/workflow-node-sdk +## 0.6.79 + +### Patch Changes + +- @ballerine/workflow-core@0.6.79 + +## 0.6.78 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/workflow-core@0.6.78 + ## 0.6.77 ### Patch Changes diff --git a/sdks/workflow-node-sdk/package.json b/sdks/workflow-node-sdk/package.json index 2eaaf6ef8c..ceda3cb29e 100644 --- a/sdks/workflow-node-sdk/package.json +++ b/sdks/workflow-node-sdk/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/workflow-node-sdk", "author": "Ballerine ", - "version": "0.6.77", + "version": "0.6.79", "description": "workflow-node-sdk", "module": "./dist/esm/index.js", "main": "./dist/cjs/index.js", @@ -28,7 +28,7 @@ "node": ">=12" }, "dependencies": { - "@ballerine/workflow-core": "0.6.77", + "@ballerine/workflow-core": "0.6.79", "json-logic-js": "^2.0.2", "xstate": "^4.36.0" }, @@ -36,9 +36,9 @@ "@babel/core": "7.17.9", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@ballerine/config": "^1.1.27", + "@ballerine/config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/eslint-config": "^1.1.28", "@rollup/plugin-babel": "5.3.1", "@rollup/plugin-commonjs": "^24.0.1", "@rollup/plugin-json": "^6.0.0", diff --git a/services/websocket-service/CHANGELOG.md b/services/websocket-service/CHANGELOG.md index ad77bb20ae..a08b1c436e 100644 --- a/services/websocket-service/CHANGELOG.md +++ b/services/websocket-service/CHANGELOG.md @@ -1,5 +1,11 @@ # @ballerine/websocket-service +## 0.1.28 + +### Patch Changes + +- core + ## 0.1.27 ### Patch Changes diff --git a/services/websocket-service/package.json b/services/websocket-service/package.json index 85e87b97b4..fdaf6bfbad 100644 --- a/services/websocket-service/package.json +++ b/services/websocket-service/package.json @@ -1,6 +1,6 @@ { "name": "@ballerine/websocket-service", - "version": "0.1.27", + "version": "0.1.28", "description": "websocket-service", "private": false, "scripts": { diff --git a/services/workflows-service/CHANGELOG.md b/services/workflows-service/CHANGELOG.md index 46b74e9bb8..01fa81a3ad 100644 --- a/services/workflows-service/CHANGELOG.md +++ b/services/workflows-service/CHANGELOG.md @@ -1,5 +1,24 @@ # @ballerine/workflows-service +## 0.7.83 + +### Patch Changes + +- Updated dependencies + - @ballerine/common@0.9.60 + - @ballerine/workflow-core@0.6.79 + - @ballerine/workflow-node-sdk@0.6.79 + +## 0.7.82 + +### Patch Changes + +- core +- Updated dependencies + - @ballerine/common@0.9.59 + - @ballerine/workflow-core@0.6.78 + - @ballerine/workflow-node-sdk@0.6.78 + ## 0.7.81 ### Patch Changes 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/package.json b/services/workflows-service/package.json index ca48a9ebe8..a1e0908d16 100644 --- a/services/workflows-service/package.json +++ b/services/workflows-service/package.json @@ -1,7 +1,7 @@ { "name": "@ballerine/workflows-service", "private": false, - "version": "0.7.81", + "version": "0.7.83", "description": "workflow-service", "scripts": { "spellcheck": "cspell \"*\"", @@ -49,9 +49,9 @@ "@aws-sdk/client-secrets-manager": "^3.620.1", "@aws-sdk/lib-storage": "3.347.1", "@aws-sdk/s3-request-presigner": "3.347.1", - "@ballerine/common": "0.9.58", - "@ballerine/workflow-core": "0.6.77", - "@ballerine/workflow-node-sdk": "0.6.77", + "@ballerine/common": "0.9.60", + "@ballerine/workflow-core": "0.6.79", + "@ballerine/workflow-node-sdk": "0.6.79", "@faker-js/faker": "^7.6.0", "@nestjs/axios": "^2.0.0", "@nestjs/common": "^9.3.12", @@ -112,8 +112,8 @@ "zod": "^3.23.4" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config": "^1.1.28", "@cspell/cspell-types": "^6.31.1", "@nestjs/cli": "9.3.0", "@nestjs/swagger": "7.4.0", diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 186cc70140..8f212fd18b 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 186cc70140c064c184f116ba388e31464670b0a8 +Subproject commit 8f212fd18bd2f806330a7ea6062d2044754dc337 diff --git a/services/workflows-service/prisma/migrations/20241219094046_workflow_runtime_soft_delete/migration.sql b/services/workflows-service/prisma/migrations/20241219094046_workflow_runtime_soft_delete/migration.sql new file mode 100644 index 0000000000..71ea9516b2 --- /dev/null +++ b/services/workflows-service/prisma/migrations/20241219094046_workflow_runtime_soft_delete/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "WorkflowRuntimeData" ADD COLUMN "deletedAt" TIMESTAMP(3), +ADD COLUMN "deletedBy" TEXT; + +-- CreateIndex +CREATE INDEX "WorkflowRuntimeData_deletedAt_idx" ON "WorkflowRuntimeData"("deletedAt"); diff --git a/services/workflows-service/prisma/schema.prisma b/services/workflows-service/prisma/schema.prisma index ad9d40b8a0..43b6f1baaf 100644 --- a/services/workflows-service/prisma/schema.prisma +++ b/services/workflows-service/prisma/schema.prisma @@ -276,6 +276,9 @@ model WorkflowRuntimeData { projectId String project Project @relation(fields: [projectId], references: [id]) + deletedAt DateTime? + deletedBy String? + @@index([assigneeId, status]) @@index([endUserId, status]) @@index([businessId, status]) @@ -285,6 +288,7 @@ model WorkflowRuntimeData { @@index([projectId]) @@index([uiDefinitionId]) @@index([tags(ops: JsonbPathOps)], type: Gin) + @@index([deletedAt]) } model File { 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/case-management/case-management.module.ts b/services/workflows-service/src/case-management/case-management.module.ts index dfd8ea71f8..731cff330f 100644 --- a/services/workflows-service/src/case-management/case-management.module.ts +++ b/services/workflows-service/src/case-management/case-management.module.ts @@ -6,6 +6,7 @@ import { WorkflowModule } from '@/workflow/workflow.module'; import { Module } from '@nestjs/common'; import { AlertModule } from '@/alert/alert.module'; import { EndUserModule } from '@/end-user/end-user.module'; +import { UiDefinitionModule } from '@/ui-definition/ui-definition.module'; @Module({ imports: [ @@ -14,6 +15,7 @@ import { EndUserModule } from '@/end-user/end-user.module'; TransactionModule, EndUserModule, AlertModule, + UiDefinitionModule, ], providers: [CaseManagementService], controllers: [CaseManagementController], diff --git a/services/workflows-service/src/case-management/case-management.service.ts b/services/workflows-service/src/case-management/case-management.service.ts index 2a17b073ba..d78e49e54d 100644 --- a/services/workflows-service/src/case-management/case-management.service.ts +++ b/services/workflows-service/src/case-management/case-management.service.ts @@ -4,14 +4,22 @@ import { WorkflowDefinitionService } from '@/workflow-defintion/workflow-definit import { WorkflowRunDto } from '@/workflow/dtos/workflow-run'; import { ajv } from '@/common/ajv/ajv.validator'; import { WorkflowService } from '@/workflow/workflow.service'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { TWorkflowDefinitionWithTransitionSchema } from '@/workflow-defintion/types'; +import { PrismaService } from '@/prisma/prisma.service'; +import { EndUserService } from '@/end-user/end-user.service'; +import { randomUUID } from 'crypto'; +import { BusinessPosition } from '@prisma/client'; +import { BUILT_IN_EVENT } from '@ballerine/workflow-core'; +import { UboToEntityAdapter } from './types'; @Injectable() export class CaseManagementService { constructor( protected readonly workflowDefinitionService: WorkflowDefinitionService, protected readonly workflowService: WorkflowService, + protected readonly prismaService: PrismaService, + protected readonly endUserService: EndUserService, ) {} async create( @@ -69,4 +77,157 @@ export class CaseManagementService { throw ValidationError.fromAjvError(validate.errors!); } } + + async createUbo({ + workflowId, + ubo, + projectId, + }: { + workflowId: string; + ubo: Record; + projectId: TProjectId; + }) { + await this.prismaService.$transaction(async transaction => { + const workflowRuntimeData = + await this.workflowService.getWorkflowRuntimeDataByIdAndLockUnscoped({ + id: workflowId, + transaction, + }); + + if (!workflowRuntimeData.businessId) { + throw new BadRequestException( + `Attempted to create a UBO to a parent workflow without a business`, + ); + } + + const uboToEntityAdapter = (ubo => { + return { + id: randomUUID(), + type: 'individual', + variant: 'ubo', + data: { + firstName: ubo.firstName, + lastName: ubo.lastName, + email: ubo.email, + nationalId: ubo.nationalId, + percentageOfOwnership: ubo.ownershipPercentage ?? ubo.percentageOfOwnership, + additionalInfo: { + fullAddress: ubo.fullAddress, + nationality: ubo.nationality, + companyName: workflowRuntimeData.context.entity.data.companyName, + customerCompany: + workflowRuntimeData.context.collectionFlow.additionalInformation.customerCompany, + }, + }, + }; + }) satisfies UboToEntityAdapter; + + const [{ ballerineEntityId }] = await this.workflowService.createOrUpdateWorkflowRuntime( + { + workflowDefinitionId: 'kyc_email_session_example', + parentWorkflowId: workflowId, + currentProjectId: projectId, + projectIds: [projectId], + context: { + entity: uboToEntityAdapter(ubo), + documents: [], + }, + config: {}, + }, + transaction, + ); + + await transaction.endUsersOnBusinesses.create({ + data: { + endUserId: ballerineEntityId, + businessId: workflowRuntimeData.businessId, + position: BusinessPosition.ubo, + }, + }); + }); + } + + async deleteUbosByIds({ + workflowId, + ids, + projectId, + deletedBy, + }: { + workflowId: string; + ids: string[]; + projectId: TProjectId; + deletedBy: string; + }) { + await this.prismaService.$transaction(async transaction => { + const workflowRuntimeData = + await this.workflowService.getWorkflowRuntimeDataByIdAndLockUnscoped({ + id: workflowId, + transaction, + }); + const workflowRuntimeDataByEndUserIds = await transaction.workflowRuntimeData.findMany({ + where: { + endUserId: { in: ids }, + parentRuntimeDataId: workflowId, + }, + select: { + id: true, + workflowDefinitionId: true, + }, + }); + + const workflowRuntimeDataToDelete = workflowRuntimeDataByEndUserIds.map(data => data.id); + const workflowDefinitionIdsToDelete = workflowRuntimeDataByEndUserIds.map( + data => data.workflowDefinitionId, + ); + + const childWorkflows = Object.entries( + workflowRuntimeData.context.childWorkflows ?? {}, + ).reduce((acc, [key, value]) => { + // First key is the workflow definition id - keep unrelated workflows unchanged + if (!workflowDefinitionIdsToDelete.includes(key)) { + acc[key] = value as Record; + } + + // Second key is the child workflow runtime data id - remove the ones we are deleting by not assigning them to the accumulator + acc[key] = Object.entries(value as Record).reduce((acc, [key, value]) => { + if (workflowRuntimeDataToDelete.includes(key)) { + return acc; + } + + acc[key] = value; + + return acc; + }, {} as Record); + + return acc; + }, {} as Record>); + + await this.workflowService.event( + { + id: workflowId, + name: BUILT_IN_EVENT.UPDATE_CONTEXT, + payload: { + context: { + ...workflowRuntimeData.context, + childWorkflows, + }, + }, + }, + [projectId], + projectId, + transaction, + ); + + await transaction.workflowRuntimeData.updateMany({ + where: { + endUserId: { in: ids }, + projectId, + }, + data: { + deletedAt: new Date(), + deletedBy, + }, + }); + }); + } } diff --git a/services/workflows-service/src/case-management/controllers/case-management.controller.ts b/services/workflows-service/src/case-management/controllers/case-management.controller.ts index 4aadb3c0a0..be5761fb60 100644 --- a/services/workflows-service/src/case-management/controllers/case-management.controller.ts +++ b/services/workflows-service/src/case-management/controllers/case-management.controller.ts @@ -18,16 +18,18 @@ import { Param, Post, Query, + UnauthorizedException, } from '@nestjs/common'; import { ApiExcludeController, ApiForbiddenResponse, ApiOkResponse } from '@nestjs/swagger'; import { EndUserService } from '@/end-user/end-user.service'; -import { StateTag, TStateTag } from '@ballerine/common'; +import { StateTag, TStateTag, EndUserAmlHitsSchema } from '@ballerine/common'; import { AlertService } from '@/alert/alert.service'; import { ZodValidationPipe } from '@/common/pipes/zod.pipe'; import { ListIndividualsProfilesSchema } from '@/case-management/dtos/list-individuals-profiles.dto'; import { z } from 'zod'; -import { EndUserAmlHitsSchema } from '@ballerine/common'; -import { Business, EndUsersOnBusinesses } from '@prisma/client'; +import type { Business, EndUsersOnBusinesses, UiDefinition } from '@prisma/client'; +import { TranslationService } from '@/providers/translation/translation.service'; +import { UiDefinitionService } from '@/ui-definition/ui-definition.service'; @Controller('case-management') @ApiExcludeController() @@ -40,6 +42,7 @@ export class CaseManagementController { protected readonly transactionService: TransactionService, protected readonly endUserService: EndUserService, protected readonly alertsService: AlertService, + protected readonly uiDefinitionService: UiDefinitionService, ) {} @Get('workflow-definition/:workflowDefinitionId') @@ -207,4 +210,70 @@ export class CaseManagementController { }; }); } + + @common.Post('/ui-definition/:id/translate/:language') + async translateUiDefinition( + @common.Param('id') id: string, + @common.Param('language') language: string, + @common.Body() + body: { + partialUiDefinition: Partial; + }, + @CurrentProject() projectId: TProjectId, + ) { + const uiDefinition = await this.uiDefinitionService.getById(id, {}, [projectId]); + const translationService = new TranslationService( + this.uiDefinitionService.getTranslationServiceResources(uiDefinition), + ); + + await translationService.init(); + + const elements = this.uiDefinitionService.traverseUiSchema( + // @ts-expect-error - error from Prisma types fix + body.partialUiDefinition.elements, + {}, + language, + translationService, + ); + + return { + ...body.partialUiDefinition, + elements, + }; + } + + @common.Post('/workflows/:workflowId/ubos') + async createUbo( + @common.Param('workflowId') workflowId: string, + @common.Body() body: Record, + @CurrentProject() projectId: TProjectId, + ) { + await this.caseManagementService.createUbo({ + workflowId, + ubo: body, + projectId, + }); + } + + @common.Delete('/workflows/:workflowId/ubos') + async deleteUbosByIds( + @common.Param('workflowId') workflowId: string, + @common.Body() + body: { + ids: string[]; + }, + @CurrentProject() projectId: TProjectId, + @UserData() authenticatedEntity: AuthenticatedEntity, + ) { + if (!authenticatedEntity?.user?.id) { + throw new UnauthorizedException(); + } + + await this.caseManagementService.deleteUbosByIds({ + workflowId, + ids: body.ids, + projectId, + deletedBy: authenticatedEntity?.user?.id, + }); + } } diff --git a/services/workflows-service/src/case-management/types.ts b/services/workflows-service/src/case-management/types.ts new file mode 100644 index 0000000000..d66f2800c9 --- /dev/null +++ b/services/workflows-service/src/case-management/types.ts @@ -0,0 +1,18 @@ +export type UboToEntityAdapter = (ubo: Record) => { + id: string; + type: 'individual'; + variant: 'ubo'; + data: { + firstName: string; + lastName: string; + email: string; + nationalId: string; + percentageOfOwnership: number; + additionalInfo: { + fullAddress: string; + nationality: string; + companyName: string; + customerCompany: string; + }; + }; +}; diff --git a/services/workflows-service/src/collection-flow/collection-flow.service.ts b/services/workflows-service/src/collection-flow/collection-flow.service.ts index e492633e36..dd23e4b3ab 100644 --- a/services/workflows-service/src/collection-flow/collection-flow.service.ts +++ b/services/workflows-service/src/collection-flow/collection-flow.service.ts @@ -9,10 +9,7 @@ import { TCustomerWithFeatures } from '@/customer/types'; import { EndUserService } from '@/end-user/end-user.service'; import { NotFoundException } from '@/errors'; import { FileService } from '@/providers/file/file.service'; -import { - ITranslationServiceResource, - TranslationService, -} from '@/providers/translation/translation.service'; +import { TranslationService } from '@/providers/translation/translation.service'; import type { TProjectId, TProjectIds } from '@/types'; import { UiDefinitionService } from '@/ui-definition/ui-definition.service'; import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository'; @@ -20,7 +17,7 @@ import { WorkflowService } from '@/workflow/workflow.service'; import { AnyRecord, DefaultContextSchema, TCollectionFlowConfig } from '@ballerine/common'; import { BUILT_IN_EVENT } from '@ballerine/workflow-core'; import { Injectable } from '@nestjs/common'; -import { EndUser, Prisma, UiDefinition, WorkflowRuntimeData } from '@prisma/client'; +import { EndUser, Prisma, WorkflowRuntimeData } from '@prisma/client'; import { randomUUID } from 'crypto'; import get from 'lodash/get'; @@ -45,33 +42,6 @@ export class CollectionFlowService { return await this.endUserService.getById(endUserId, {}, [projectId]); } - traverseUiSchema( - uiSchema: Record, - context: WorkflowRuntimeData['context'], - language: string, - _translationService: TranslationService, - ) { - for (const key in uiSchema) { - if (typeof uiSchema[key] === 'object' && uiSchema[key] !== null) { - // If the property is an object (including arrays), recursively traverse it - // @ts-expect-error - error from Prisma types fix - this.traverseUiSchema(uiSchema[key], context, language, _translationService); - } else if (typeof uiSchema[key] === 'string') { - const options: AnyRecord = {}; - - if (uiSchema.labelVariables) { - Object.entries(uiSchema.labelVariables).forEach(([key, value]) => { - options[key] = get(context, value); - }); - } - - uiSchema[key] = _translationService.translate(uiSchema[key] as string, language, options); - } - } - - return uiSchema; - } - async getFlowConfiguration( workflowDefinitionId: string, context: WorkflowRuntimeData['context'], @@ -93,7 +63,7 @@ export class CollectionFlowService { ); const translationService = new TranslationService( - this.getTranslationServiceResources(uiDefinition), + this.uiDefinitionService.getTranslationServiceResources(uiDefinition), ); await translationService.init(); @@ -104,7 +74,7 @@ export class CollectionFlowService { uiOptions: uiDefinition.uiOptions, uiSchema: { // @ts-expect-error - error from Prisma types fix - elements: this.traverseUiSchema( + elements: this.uiDefinitionService.traverseUiSchema( // @ts-expect-error - error from Prisma types fix uiDefinition.uiSchema.elements, context, @@ -119,17 +89,6 @@ export class CollectionFlowService { }; } - private getTranslationServiceResources( - uiDefinition: UiDefinition & { locales?: unknown }, - ): ITranslationServiceResource[] | undefined { - if (!uiDefinition.locales) return undefined; - - return Object.entries(uiDefinition.locales).map(([language, resource]) => ({ - language, - resource, - })); - } - // async updateFlowConfiguration( // configurationId: string, // steps: UiSchemaStep[], diff --git a/services/workflows-service/src/collection-flow/collection-flow.service.unit.test.ts b/services/workflows-service/src/collection-flow/collection-flow.service.unit.test.ts deleted file mode 100644 index 1c52ff8338..0000000000 --- a/services/workflows-service/src/collection-flow/collection-flow.service.unit.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { noop } from 'lodash'; - -import { BusinessService } from '@/business/business.service'; -import { CollectionFlowService } from '@/collection-flow/collection-flow.service'; -import { AppLoggerService } from '@/common/app-logger/app-logger.service'; -import { CustomerService } from '@/customer/customer.service'; -import { EndUserService } from '@/end-user/end-user.service'; -import { FileService } from '@/providers/file/file.service'; -import { TranslationService } from '@/providers/translation/translation.service'; -import { UiDefinitionService } from '@/ui-definition/ui-definition.service'; -import { WorkflowDefinitionRepository } from '@/workflow-defintion/workflow-definition.repository'; -import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository'; -import { WorkflowService } from '@/workflow/workflow.service'; - -describe('CollectionFlowService', () => { - let uiSchema: Record; - let context: Record; - - let collectionFlowService: CollectionFlowService; - const translationService: TranslationService = { - // @ts-expect-error - bad type, implemented used methods only - __i18next: { - init: jest.fn(), - addResourceBundle: jest.fn(), - t: jest.fn(), - }, - translate: jest.fn(), - }; - - beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ - { - provide: AppLoggerService, - useValue: noop, - }, - { - provide: EndUserService, - useValue: noop, - }, - { - provide: WorkflowRuntimeDataRepository, - useValue: noop, - }, - { - provide: WorkflowDefinitionRepository, - useValue: noop, - }, - { - provide: WorkflowService, - useValue: noop, - }, - { - provide: BusinessService, - useValue: noop, - }, - { - provide: UiDefinitionService, - useValue: noop, - }, - { - provide: CustomerService, - useValue: noop, - }, - { - provide: FileService, - useValue: noop, - }, - { - provide: UiDefinitionService, - useValue: noop, - }, - CollectionFlowService, - ], - }).compile(); - - collectionFlowService = module.get(CollectionFlowService); - }); - - beforeEach(() => { - uiSchema = { - title: 'Title', - description: 'Description', - nested: { - label: 'Label', - inner: { - text: 'Inner Text', - }, - }, - array: ['Item 1', 'Item 2'], - }; - - context = {}; - }); - - it('should translate leaf nodes of the uiSchema', () => { - const language = 'fr'; - const expectedUiSchema = { - title: 'Translated Title', - description: 'Translated Description', - nested: { - label: 'Translated Label', - inner: { - text: 'Translated Inner Text', - }, - }, - array: ['Translated Item 1', 'Translated Item 2'], - }; - - const translationService = new TranslationService(); - - translationService.translate = jest.fn((text, lang) => - lang === 'fr' ? `Translated ${text}` : text, - ); - - const result = collectionFlowService.traverseUiSchema( - uiSchema, - context, - language, - translationService, - ); - expect(result).toEqual(expectedUiSchema); - }); -}); diff --git a/services/workflows-service/src/end-user/end-user.service.ts b/services/workflows-service/src/end-user/end-user.service.ts index 74481a086b..3975238a80 100644 --- a/services/workflows-service/src/end-user/end-user.service.ts +++ b/services/workflows-service/src/end-user/end-user.service.ts @@ -94,23 +94,23 @@ export class EndUserService { ); } - async updateById(id: string, endUser: Omit) { + async updateById(id: string, args: Parameters[1]) { let activeMonitorings; - if (endUser.data.activeMonitorings !== undefined) { - activeMonitorings = EndUserActiveMonitoringsSchema.parse(endUser.data.activeMonitorings); + if (args.data.activeMonitorings !== undefined) { + activeMonitorings = EndUserActiveMonitoringsSchema.parse(args.data.activeMonitorings); } let amlHits; - if (endUser.data.amlHits !== undefined) { - amlHits = EndUserAmlHitsSchema.parse(endUser.data.amlHits); + if (args.data.amlHits !== undefined) { + amlHits = EndUserAmlHitsSchema.parse(args.data.amlHits); } return await this.repository.updateById(id, { - ...endUser, + ...args, data: { - ...endUser.data, + ...args.data, activeMonitorings, amlHits, }, 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..781fd7621b 100644 --- a/services/workflows-service/src/ui-definition/ui-definition.service.ts +++ b/services/workflows-service/src/ui-definition/ui-definition.service.ts @@ -1,9 +1,14 @@ -import type { TProjectId, TProjectIds } from '@/types'; +import { + TranslationService, + ITranslationServiceResource, +} from '@/providers/translation/translation.service'; +import type { AnyRecord, TProjectId, TProjectIds } from '@/types'; import { UiDefinitionRepository } from '@/ui-definition/ui-definition.repository'; import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository'; import { replaceNullsWithUndefined } from '@ballerine/common'; import { Injectable } from '@nestjs/common'; -import { Prisma, UiDefinitionContext } from '@prisma/client'; +import { Prisma, UiDefinition, UiDefinitionContext, WorkflowRuntimeData } from '@prisma/client'; +import { get } from 'lodash'; @Injectable() export class UiDefinitionService { @@ -65,13 +70,6 @@ export class UiDefinitionService { args: Omit, projectIds: TProjectIds, ) { - console.log({ - ...args, - where: { - id, - }, - }); - return await this.repository.update( { ...args, @@ -102,4 +100,42 @@ export class UiDefinitionService { return uiDefinitionCopy; } + + traverseUiSchema( + uiSchema: Record, + context: WorkflowRuntimeData['context'], + language: string, + _translationService: TranslationService, + ) { + for (const key in uiSchema) { + if (typeof uiSchema[key] === 'object' && uiSchema[key] !== null) { + // If the property is an object (including arrays), recursively traverse it + // @ts-expect-error - error from Prisma types fix + this.traverseUiSchema(uiSchema[key], context, language, _translationService); + } else if (typeof uiSchema[key] === 'string') { + const options: AnyRecord = {}; + + if (uiSchema.labelVariables) { + Object.entries(uiSchema.labelVariables).forEach(([key, value]) => { + options[key] = get(context, value); + }); + } + + uiSchema[key] = _translationService.translate(uiSchema[key] as string, language, options); + } + } + + return uiSchema; + } + + getTranslationServiceResources( + uiDefinition: UiDefinition & { locales?: unknown }, + ): ITranslationServiceResource[] | undefined { + if (!uiDefinition.locales) return undefined; + + return Object.entries(uiDefinition.locales).map(([language, resource]) => ({ + language, + resource, + })); + } } diff --git a/services/workflows-service/src/ui-definition/ui-definition.service.unit.test.ts b/services/workflows-service/src/ui-definition/ui-definition.service.unit.test.ts new file mode 100644 index 0000000000..dfbe875ffd --- /dev/null +++ b/services/workflows-service/src/ui-definition/ui-definition.service.unit.test.ts @@ -0,0 +1,78 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { noop } from 'lodash'; + +import { TranslationService } from '@/providers/translation/translation.service'; +import { UiDefinitionService } from '@/ui-definition/ui-definition.service'; + +import { WorkflowRuntimeDataRepository } from '@/workflow/workflow-runtime-data.repository'; +import { UiDefinitionRepository } from './ui-definition.repository'; + +describe('UiDefinitionService', () => { + let uiSchema: Record; + let context: Record; + + let uiDefinitionService: UiDefinitionService; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: UiDefinitionRepository, + useValue: noop, + }, + { + provide: WorkflowRuntimeDataRepository, + useValue: noop, + }, + UiDefinitionService, + ], + }).compile(); + + uiDefinitionService = module.get(UiDefinitionService); + }); + + beforeEach(() => { + uiSchema = { + title: 'Title', + description: 'Description', + nested: { + label: 'Label', + inner: { + text: 'Inner Text', + }, + }, + array: ['Item 1', 'Item 2'], + }; + + context = {}; + }); + + it('should translate leaf nodes of the uiSchema', () => { + const language = 'fr'; + const expectedUiSchema = { + title: 'Translated Title', + description: 'Translated Description', + nested: { + label: 'Translated Label', + inner: { + text: 'Translated Inner Text', + }, + }, + array: ['Translated Item 1', 'Translated Item 2'], + }; + + const translationService = new TranslationService(); + + translationService.translate = jest.fn((text, lang) => + lang === 'fr' ? `Translated ${text}` : text, + ); + + const result = uiDefinitionService.traverseUiSchema( + uiSchema, + context, + language, + translationService, + ); + expect(result).toEqual(expectedUiSchema); + }); +}); diff --git a/services/workflows-service/src/workflow/schemas/zod-schemas.ts b/services/workflows-service/src/workflow/schemas/zod-schemas.ts index a981b6203e..a19d763037 100644 --- a/services/workflows-service/src/workflow/schemas/zod-schemas.ts +++ b/services/workflows-service/src/workflow/schemas/zod-schemas.ts @@ -81,6 +81,15 @@ export const ConfigSchema = z .optional(), }) .optional(), + ubos: z + .object({ + create: z + .object({ + enabled: z.boolean().optional(), + }) + .optional(), + }) + .optional(), }) .strict() .optional(); diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index 26c41a81fa..8c34c7ea20 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -255,6 +255,10 @@ export class WorkflowService { const childWorkflowSelectArgs = { select: { ...args?.select, ...allEntities }, include: args?.include, + where: { + // @ts-expect-error - dynamically typed for all queries + deletedAt: args?.where?.deletedAt ?? null, + }, }; const workflow = (await this.workflowRuntimeDataRepository.findById( id, diff --git a/websites/docs/package.json b/websites/docs/package.json index 2234d7aa81..ac118e30a9 100644 --- a/websites/docs/package.json +++ b/websites/docs/package.json @@ -17,14 +17,14 @@ "dependencies": { "@astrojs/starlight": "0.11.1", "@astrojs/tailwind": "^4.0.0", - "@ballerine/common": "^0.9.58", + "@ballerine/common": "^0.9.60", "astro": "3.3.3", "sharp": "^0.32.4", "shiki": "^0.14.3" }, "devDependencies": { - "@ballerine/config": "^1.1.27", - "@ballerine/eslint-config": "^1.1.27", + "@ballerine/config": "^1.1.28", + "@ballerine/eslint-config": "^1.1.28", "eslint": "^8.46.0", "eslint-config-prettier": "^9.0.0", "eslint-config-standard-with-typescript": "^37.0.0",