Skip to content

Commit

Permalink
WIP - Alerts decision (#2076)
Browse files Browse the repository at this point in the history
* feat(*): wip - alerts decision changes

* feat(*): added the functionality of making a decision

* feat(workflows-service): now assigning alerts on decision

* fix(backoffice-v2): coming soon decisions are now disabled

* refactor(backoffice-v2): revert decision now moves an alert to new/triggered

* refactor(backoffice-v2): renamed toast function

* style(schema.prisma): fixed format

* refactor(backoffice-v2): removed false positive mapping

* fix(backoffice-v2): fixed revert decision toast

* refactor(backoffice-v2): addressed PR comments

* refactor(backoffice-v2): addressed PR comments for toasts string literals
  • Loading branch information
Omri-Levy authored Feb 18, 2024
1 parent d44a495 commit 07a0d88
Show file tree
Hide file tree
Showing 21 changed files with 442 additions and 93 deletions.
12 changes: 12 additions & 0 deletions apps/backoffice-v2/public/locales/en/toast.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,17 @@
"unassign_alerts": {
"success": "The alerts have been unassigned successfully.",
"error": "Error occurred while unassigning the alerts."
},
"reject_alerts": {
"success": "The alerts have been rejected successfully.",
"error": "Error occurred while rejecting the alerts."
},
"not_suspicious_alerts": {
"success": "The alerts have been marked as not suspicious successfully.",
"error": "Error occurred while marking the alerts as not suspicious."
},
"revert_decision_alerts": {
"success": "The alerts decision have been reverted successfully.",
"error": "Error occurred while reverting the alerts decision."
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FunctionComponent, ReactNode, useCallback, useState } from 'react';
import { ReactNode, useCallback, useState } from 'react';
import {
Badge,
Button,
Expand All @@ -17,17 +17,37 @@ import {
import { CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons';
import { Separator } from '@/common/components/atoms/Separator/Separator';

export const MultiSelect: FunctionComponent<{
interface IMultiSelectProps<
TOption extends {
label: string;
value: unknown;
icon?: ReactNode;
},
> {
title: string;
selectedValues: string[];
onSelect: (value: string) => void;
selectedValues: Array<TOption['value']>;
onSelect: (value: Array<TOption['value']>) => void;
onClearSelect: () => void;
options: Array<{ label: string; value: string; icon?: ReactNode }>;
}> = ({ title, selectedValues, onSelect, onClearSelect, options }) => {
options: TOption[];
}

export const MultiSelect = <
TOption extends {
label: string;
value: unknown;
icon?: ReactNode;
},
>({
title,
selectedValues,
onSelect,
onClearSelect,
options,
}: IMultiSelectProps<TOption>) => {
const [selected, setSelected] = useState(selectedValues);

const onSelectChange = useCallback(
(value: string) => {
(value: TOption['value']) => {
const isSelected = selected.some(selectedValue => selectedValue === value);
const nextSelected = isSelected
? selected.filter(selectedValue => selectedValue !== value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const useFilter = () => {

const onFilter = useCallback(
(accessor: string) => {
return (values: string[]) => {
return (values: Array<string | null>) => {
setSearchParams({
filter: {
...filter,
Expand Down
14 changes: 13 additions & 1 deletion apps/backoffice-v2/src/common/types.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ComponentProps, FunctionComponent, PropsWithChildren } from 'react';
import { Action, Method, Resource, State } from './enums';
import translations from '../../public/locales/en/toast.json';

export type WithRequired<TObject, TKey extends keyof TObject> = TObject & {
[TProperty in TKey]-?: TObject[TProperty];
};

export type AnyArray = Array<any>;
export type AnyArray = any[];

export type AnyRecord = Record<PropertyKey, any>;

Expand Down Expand Up @@ -45,3 +46,14 @@ export type TypesafeOmit<TObj extends Record<PropertyKey, unknown>, TProps exten
} & {};

export type UnknownRecord = Record<PropertyKey, unknown>;

export type NoInfer<T> = [T][T extends any ? 0 : never];

export type TToastKeyWithSuccessAndError = {
[TKey in keyof typeof translations]: (typeof translations)[TKey] extends {
success: string;
error: string;
}
? TKey
: never;
}[keyof typeof translations];
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { isString } from '@/common/utils/is-string/is-string';
import { snakeCase, toUpperCase } from 'string-ts';

export const toScreamingSnakeCase = <TString extends string>(string_: TString) => {
if (!isString(string_)) return string_;

return toUpperCase(snakeCase(string_));
};
68 changes: 67 additions & 1 deletion apps/backoffice-v2/src/domains/alerts/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,52 @@ export const AlertTypes = [
AlertType.UNUSUAL_PATTERN,
] as const satisfies ReadonlyArray<TObjectValues<typeof AlertType>>;

export const AlertState = {
TRIGGERED: 'Triggered',
UNDER_REVIEW: 'UnderReview',
ESCALATED: 'Escalated',
RESOLVED: 'Resolved',
ACKNOWLEDGED: 'Acknowledged',
DISMISSED: 'Dismissed',
REJECTED: 'Rejected',
NOT_SUSPICIOUS: 'NotSuspicious',
} as const;

export const AlertStates = [
AlertState.TRIGGERED,
AlertState.UNDER_REVIEW,
AlertState.ESCALATED,
AlertState.RESOLVED,
AlertState.ACKNOWLEDGED,
AlertState.DISMISSED,
AlertState.REJECTED,
AlertState.NOT_SUSPICIOUS,
] as const satisfies ReadonlyArray<TObjectValues<typeof AlertState>>;

export const alertStateToDecision = {
REJECTED: 'Reject',
NOT_SUSPICIOUS: 'Not Suspicious',
REVERT_DECISION: 'Revert Decision',
} as const satisfies Partial<Record<keyof typeof AlertState | (string & {}), string>>;

export const alertDecisionToState = {
REJECT: 'Rejected',
NOT_SUSPICIOUS: 'NotSuspicious',
REVERT_DECISION: 'Triggered',
} as const satisfies Partial<Record<keyof typeof AlertState | (string & {}), string>>;

export type TAlertSeverity = (typeof AlertSeverities)[number];

export type TAlertSeverities = typeof AlertSeverities;

export type TAlertType = (typeof AlertTypes)[number];

export type TAlertTypes = typeof AlertTypes;

export type TAlertState = (typeof AlertStates)[number];

export type TAlertStates = typeof AlertStates;

export const AlertsListSchema = z.array(
ObjectWithIdSchema.extend({
dataTimestamp: z.string().datetime(),
Expand All @@ -59,7 +105,7 @@ export const AlertsListSchema = z.array(
.nullable()
.default(null),
status: z.enum(AlertStatuses),
// decision: z.string(),
decision: z.enum(AlertStates).nullable().default(null),
}),
);

Expand Down Expand Up @@ -103,3 +149,23 @@ export const assignAlertsByIds = async ({

return handleZodError(error, alerts);
};

export const updateAlertsDecisionByIds = async ({
decision,
alertIds,
}: {
decision: TAlertState;
alertIds: string[];
}) => {
const [alerts, error] = await apiClient({
url: `${getOriginUrl(env.VITE_API_URL)}/api/v1/external/alerts/decision`,
method: Method.PATCH,
body: {
decision,
alertIds,
},
schema: z.any(),
});

return handleZodError(error, alerts);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { t } from 'i18next';
import { AlertState, TAlertState, updateAlertsDecisionByIds } from '@/domains/alerts/fetchers';
import { TToastKeyWithSuccessAndError } from '@/common/types';

const getToastAction = (decision: TAlertState): TToastKeyWithSuccessAndError => {
if (decision === AlertState.REJECTED) {
return 'reject_alerts' as const;
}

if (decision === AlertState.NOT_SUSPICIOUS) {
return 'not_suspicious_alerts' as const;
}

if (decision === AlertState.TRIGGERED) {
return 'revert_decision_alerts' as const;
}

throw new Error(`Invalid decision: "${decision}"`);
};

export const useAlertsDecisionByIdsMutation = ({
onSuccess,
}: {
onSuccess?: <TData>(data: TData, { decision }: { decision: TAlertState }) => void;
} = {}) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: ({ decision, alertIds }: { decision: TAlertState; alertIds: string[] }) =>
updateAlertsDecisionByIds({
decision,
alertIds,
}),
onSuccess: (data, { decision }) => {
void queryClient.invalidateQueries();

const action = getToastAction(decision);

toast.success(t(`toast:${action}.success`));
onSuccess?.(data, { decision });
},
onError: (error, { decision }) => {
const action = getToastAction(decision);

toast.error(t(`toast:${action}.error`));
},
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useUsersQuery } from '@/domains/users/hooks/queries/useUsersQuery/useUs
import toast from 'react-hot-toast';
import { t } from 'i18next';
import { assignAlertsByIds } from '@/domains/alerts/fetchers';
import { TToastKeyWithSuccessAndError } from '@/common/types';

const getToastActionAndContext = ({
assigneeId,
Expand All @@ -12,7 +13,10 @@ const getToastActionAndContext = ({
assigneeId: string | null;
assigneeName: string | null;
isAssignedToMe: boolean;
}) => {
}): {
action: TToastKeyWithSuccessAndError;
context: Record<string, string>;
} => {
const action = assigneeId ? 'assign_alerts' : 'unassign_alerts';
const context = assigneeId
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import toast from 'react-hot-toast';
import { t } from 'i18next';
import { updateWorkflowSetAssignById } from '../../../fetchers';
import { useUsersQuery } from '../../../../users/hooks/queries/useUsersQuery/useUsersQuery';
import { TToastKeyWithSuccessAndError } from '@/common/types';

const getToastActionAndContext = ({
assigneeId,
Expand All @@ -12,7 +13,10 @@ const getToastActionAndContext = ({
assigneeId: string | null;
assigneeName: string | null;
isAssignedToMe: boolean;
}) => {
}): {
action: TToastKeyWithSuccessAndError;
context: Record<string, string>;
} => {
const action = assigneeId ? 'assign_case' : 'unassign_case';
const context = assigneeId
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ const columnHelper = createColumnHelper<AlertAnalysisItem>();
export const columns = [
columnHelper.accessor('date', {
cell: info => {
const date = dayjs(info.getValue()).format('MMM DD, YYYY');
const time = dayjs(info.getValue()).format('hh:mm');
const dateValue = info.getValue();
const date = dayjs(dateValue).format('MMM DD, YYYY');
const time = dayjs(dateValue).format('hh:mm');

return (
<div className={`flex flex-col space-y-0.5`}>
Expand All @@ -33,65 +34,77 @@ export const columns = [
}),
columnHelper.accessor('transactionId', {
cell: info => {
const value = info.getValue();
const transactionId = info.getValue();

return <TextWithNAFallback className="text-sm">{value}</TextWithNAFallback>;
return <TextWithNAFallback className="text-sm">{transactionId}</TextWithNAFallback>;
},
header: 'Transaction',
}),
columnHelper.accessor('direction', {
cell: info => {
const value = info.getValue();
const direction = info.getValue();

return <TextWithNAFallback className="text-sm">{value}</TextWithNAFallback>;
return <TextWithNAFallback className="text-sm">{direction}</TextWithNAFallback>;
},
header: 'Direction',
}),
columnHelper.accessor('amount', {
cell: info => {
const value = info.getValue();
const amount = info.getValue();

return <TextWithNAFallback className="text-sm">{value}</TextWithNAFallback>;
return <TextWithNAFallback className="text-sm">{amount}</TextWithNAFallback>;
},
header: 'Amount',
}),
columnHelper.accessor('business', {
cell: info => {
const value = info.getValue();
const business = info.getValue();

return <TextWithNAFallback className="text-sm font-semibold">{value}</TextWithNAFallback>;
return <TextWithNAFallback className="text-sm font-semibold">{business}</TextWithNAFallback>;
},
header: 'Business',
}),
columnHelper.accessor('businessId', {
cell: info => {
const value = info.getValue();
const businessId = info.getValue();

return <TextWithNAFallback className="text-sm font-semibold">{value}</TextWithNAFallback>;
return (
<TextWithNAFallback className="text-sm font-semibold">{businessId}</TextWithNAFallback>
);
},
header: 'Business ID',
}),
columnHelper.accessor('counterPartyName', {
cell: info => {
const value = info.getValue();
const counterPartyName = info.getValue();

return <TextWithNAFallback className="text-sm font-semibold">{value}</TextWithNAFallback>;
return (
<TextWithNAFallback className="text-sm font-semibold">
{counterPartyName}
</TextWithNAFallback>
);
},
header: 'Counterparty Name',
}),
columnHelper.accessor('counterPartyId', {
cell: info => {
const value = info.getValue();
const counterPartyId = info.getValue();

return <TextWithNAFallback className="text-sm font-semibold">{value}</TextWithNAFallback>;
return (
<TextWithNAFallback className="text-sm font-semibold">{counterPartyId}</TextWithNAFallback>
);
},
header: 'Counterparty ID',
}),
columnHelper.accessor('counterPartyName', {
cell: info => {
const value = info.getValue();
const counterPartyName = info.getValue();

return <TextWithNAFallback className="text-sm font-semibold">{value}</TextWithNAFallback>;
return (
<TextWithNAFallback className="text-sm font-semibold">
{counterPartyName}
</TextWithNAFallback>
);
},
header: 'Counterparty Institution',
}),
Expand Down
Loading

0 comments on commit 07a0d88

Please sign in to comment.