feat(msb-treasury-deals): миграция от 18.11

This commit is contained in:
Ivan Nasekin
2025-11-18 19:21:54 +03:00
parent f5431d7d98
commit e1d6eda7a6
44 changed files with 13041 additions and 12285 deletions
+1
View File
@@ -6,3 +6,4 @@ lerna-debug.log
.idea
.vscode
/msb-*
.eslintcache
+1
View File
@@ -1,3 +1,4 @@
# Миграция ECO
eco-treasury-deals/
migration
.eslintcache
+2 -2
View File
@@ -8,9 +8,9 @@
"build:dev": "build-app build:dev",
"build": "build-app build",
"clone": "git clone ssh://git@bitbucket.gboteam.ru:7999/eco_fe/eco-treasury-deals.git && cd ./eco-treasury-deals && git checkout client_area_for_msb && cd ..",
"lint-fix": "eslint --fix --ext .js,.jsx,.ts,.tsx ./src",
"lint-fix": "eslint --cache --fix --ext .js,.jsx,.ts,.tsx ./src",
"check-types": "tsc",
"lint": "eslint --ext .js,.jsx,.ts,.tsx ./src --max-warnings=0",
"lint": "eslint --cache --ext .js,.jsx,.ts,.tsx ./src --max-warnings=0",
"stylelint": "stylelint --fix --ext .js,.jsx,.ts,.tsx ./src --max-warnings=0",
"lint:ci": "eslint --quiet --ext .js,.jsx,.ts,.tsx ./src",
"format": "prettier --write ./src",
@@ -103,13 +103,8 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
locale.deposit.modal.mnoHeader,
locale.conversion.modal.conversionHeader
);
const buttonText = returnDocTypeBinder(
dealType,
locale.common.dealType.deposit,
locale.common.dealType.mno,
locale.common.dealType.gsno,
locale.common.dealType.conversion
);
const buttonText = locale.common.dealType.text({ dealType });
const buttonDescription = locale.common.dealType.description({ dealType });
const handleClick = () => {
if (viewOrNone(info)) {
@@ -132,6 +127,7 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
return {
text: isAllowed ? buttonText : <Tooltip text={locale.deposit.button.hover}>{buttonText}</Tooltip>,
description: buttonDescription,
onClick: handleClick,
disabled: !isAllowed,
};
@@ -166,7 +162,7 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
<ContextMenuBase<HTMLButtonElement>
items={items as unknown as MenuItemProps[]}
size={'L'}
title={locale.common.dealType.createDeal}
title={locale.common.createDeal}
width={'100%'}
>
{({ ref, toggleOpen }) => (
@@ -235,14 +231,14 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
size="M"
onClick={() => ModalService.showStatus(noPrivilegesModal)}
>
{locale.common.dealType.createDeal}
{locale.common.createDeal}
</Button>
)}
{items?.length > 0 && (
<ContextMenuBase<HTMLButtonElement>
items={items as unknown as MenuItemProps[]}
size={'L'}
title={locale.common.dealType.createDeal}
title={locale.common.createDeal}
width={'100%'}
>
{({ ref, toggleOpen }) => (
@@ -254,7 +250,7 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
size={'M'}
onClick={e => (items?.length ? toggleOpen(e) : ModalService.showStatus(noPrivilegesModal))}
>
{locale.common.dealType.createDeal}
{locale.common.createDeal}
</Button>
)}
</ContextMenuBase>
@@ -277,7 +273,7 @@ export const SelectButtons: React.FC<ISelectButtons> = ({ userInfo, size = 'M' }
<ContextMenuBase<HTMLButtonElement>
items={items as unknown as MenuItemProps[]}
size="L"
title={locale.common.dealType.createDeal}
title={locale.common.createDeal}
width={'100%'}
>
{({ ref, toggleOpen }) => (
@@ -1,6 +1,6 @@
import React from 'react';
import { Label, WithInfoTooltip } from '@msb/platform-compat/ui';
import type { AUTO_COMPLETE } from '@msb/platform-compat/ui';
import { Label, WithInfoTooltip } from '@msb/platform-compat/ui';
import { isEqual, isFunction } from '@treasury-deals/common/utils/check-types';
import type { FieldValidator } from 'final-form';
import type { FieldInputProps, FieldMetaState } from 'react-final-form';
@@ -1,11 +1,12 @@
import React from 'react';
import type { IDstForSign } from '@msb/platform-compat/services/client';
import { appendToPortal, asModal, deleteFromPortal } from '@msb/platform-compat/ui';
import { appendToPortal, deleteFromPortal } from '@treasury-deals/common/components/portal-container';
import { STATISTIC_TYPE } from '@treasury-deals/common/enums/statistics';
import type { IDigestHolderDto } from '@treasury-deals/common/interfaces/common';
import { StatisticService } from '@treasury-deals/common/services';
import { getTextErrorMessage } from '@treasury-deals/common/utils/form';
import { locale } from '@treasury-deals/localization';
import { asModal } from '@platform/ui';
import { CloudSignDialog } from './cloud-sign-dialog';
// type ICloudSignItem = Omit<ICloudSignData, 'documentId'>; // Todo типов там больше нет
@@ -21,4 +21,8 @@ export enum STATISTIC_TYPE {
CRYPTO_ERROR = 'CRYPTO_ERROR',
/** Лог профиля автокотирование. */
AUTO_QUOTE_PROFILE = 'AUTO_QUOTE_PROFILE',
/** Логирование перехода по банеру лучшей ставки. */
DEAL_BEST_RATE_INIT = 'DEAL_BEST_RATE_INIT',
/** Подписание сделки с лучшей ставкой. */
DEAL_BEST_RATE_SIGN = 'DEAL_BEST_RATE_SIGN',
}
@@ -0,0 +1,53 @@
/* eslint-disable @eco/no-missing-localization */
/* eslint-disable jest/prefer-strict-equal */
import { ACCOUNT_TYPES, ACCOUNT_USAGE_TYPES } from '@treasury-deals/common/constants/accounts';
import { formatOther, formatAccountNumber, formatAccountType, formatAccountUsage, formatUserInfoOperations } from '../accounts';
describe('accounts', () => {
test('formatOther', () => {
expect(formatOther('test')).toBe('test');
expect(formatOther('')).toBe('-');
expect(formatOther({})).toBe('-');
expect(formatOther(undefined)).toBe('-');
expect(formatOther(null)).toBe('-');
});
test('formatAccountNumber', () => {
expect(formatAccountNumber('40701840200007670001')).toBe('40701.840.2.00007670001');
expect(formatAccountNumber('40201840201107672222')).toBe('40201.840.2.01107672222');
expect(formatAccountNumber('hello')).not.toBe('40201.840.2.01107672222');
expect(formatAccountNumber('')).toBe('-');
});
test('formatAccountType', () => {
expect(formatAccountType('INTERNAL')).toEqual(ACCOUNT_TYPES[0].title);
expect(formatAccountType('EXTERNAL')).toEqual(ACCOUNT_TYPES[1].title);
expect(formatAccountType('')).toBeUndefined();
});
test('formatAccountUsage', () => {
expect(formatAccountUsage([])).toBeUndefined();
expect(formatAccountUsage(['MNO'])).toEqual(ACCOUNT_USAGE_TYPES[0].title);
expect(formatAccountUsage(['DEPOSIT_VALUE'])).toEqual(ACCOUNT_USAGE_TYPES[1].title);
expect(formatAccountUsage(['GSNO'])).toEqual(ACCOUNT_USAGE_TYPES[4].title);
expect(formatAccountUsage(['DEPOSIT_VALUE', 'MNO'])).toBe(`${ACCOUNT_USAGE_TYPES[0].title}, ${ACCOUNT_USAGE_TYPES[1].title}`);
expect(formatAccountUsage(['DEPOSIT_VALUE', 'DEPOSIT_MATURITY'])).toBe(
`${ACCOUNT_USAGE_TYPES[2].title}, ${ACCOUNT_USAGE_TYPES[1].title}`
);
expect(formatAccountUsage(['DEPOSIT_VALUE', 'MNO', 'INTEREST'])).toBe(
`${ACCOUNT_USAGE_TYPES[3].title}, ${ACCOUNT_USAGE_TYPES[0].title}, ${ACCOUNT_USAGE_TYPES[1].title}`
);
expect(formatAccountUsage(['DEPOSIT_VALUE', 'MNO', 'INTEREST', 'DEPOSIT_MATURITY'])).toBe(
`${ACCOUNT_USAGE_TYPES[2].title}, ${ACCOUNT_USAGE_TYPES[3].title}, ${ACCOUNT_USAGE_TYPES[0].title}, ${ACCOUNT_USAGE_TYPES[1].title}`
);
expect(formatAccountUsage(['DEPOSIT_VALUE', 'GSNO', 'MNO'])).toBe(
`${ACCOUNT_USAGE_TYPES[4].title}, ${ACCOUNT_USAGE_TYPES[0].title}, ${ACCOUNT_USAGE_TYPES[1].title}`
);
});
test('formatUserInfoOperations', () => {
expect(formatUserInfoOperations(['DEPOSIT', 'MNO'])).toBe('');
expect(formatUserInfoOperations(['MNO'])).toBe('депозитные сделки');
expect(formatUserInfoOperations(['DEPOSIT'])).toBe('сделки МНО');
expect(formatUserInfoOperations([])).toBe('сделки МНО, депозитные сделки');
});
});
@@ -0,0 +1,13 @@
/* eslint-disable jest/prefer-strict-equal */
import { AGREEMENTS_STATUS_TYPES } from '@treasury-deals/common/constants';
import { AGREEMENT_STATUSES } from '@treasury-deals/common/enums';
import { formatAgreementStatus } from '../agreements';
describe('agreements', () => {
test('formatAgreementStatus', () => {
expect(formatAgreementStatus(AGREEMENT_STATUSES.ACTIVE)).toEqual(AGREEMENTS_STATUS_TYPES[0].title);
expect(formatAgreementStatus(AGREEMENT_STATUSES.BLOCKED)).toEqual(AGREEMENTS_STATUS_TYPES[1].title);
expect(formatAgreementStatus(AGREEMENT_STATUSES.DELETED)).toEqual(AGREEMENTS_STATUS_TYPES[2].title);
expect(formatAgreementStatus('')).toBeUndefined();
});
});
@@ -0,0 +1,46 @@
import { ERROR_MESSAGE_DEVIATION } from '@treasury-deals/common/constants/autoquotation';
import { checkRate } from '@treasury-deals/common/utils/autoquotation';
describe('checkRate', () => {
it('returns ERROR_MESSAGE_DEVIATION when currentRate is greater than the maxRateDeviation', () => {
const result = checkRate(100, 0.1, 121);
expect(result).toStrictEqual(ERROR_MESSAGE_DEVIATION);
});
it('returns ERROR_MESSAGE_DEVIATION when currentRate is less than the minRateDeviation', () => {
const result = checkRate(100, 0.1, 79);
expect(result).toStrictEqual(ERROR_MESSAGE_DEVIATION);
});
it('returns nothing when currentRate is between the minRateDeviation and maxRateDeviation', () => {
const result = checkRate(100, 0.1, 110);
expect(result).toBeUndefined();
});
it('correctly calculates the maxRateDeviation when given a positive rateDeviation', () => {
const result = checkRate(100, 0.1, 115);
expect(result).toStrictEqual(ERROR_MESSAGE_DEVIATION);
});
it('correctly calculates the minRateDeviation when given a positive rateDeviation', () => {
const result = checkRate(100, 0.1, 105);
expect(result).toBeUndefined();
});
it('returns nothing when given a zero rateDeviation', () => {
const result = checkRate(100, 0, 100);
expect(result).toBeUndefined();
});
it('correctly handles a zero originalRate', () => {
const result = checkRate(undefined, 0.1, 5);
expect(result).toBeUndefined();
});
});
@@ -0,0 +1,28 @@
/* eslint-disable jest/prefer-strict-equal */
import { formatDate, formatTime, differenceInDays, addDaysToDate } from '../date';
describe('date', () => {
test('formatDate', () => {
expect(formatDate('2020-10-10')).toBe('10.10.2020');
expect(formatDate('12.20.2020')).toBe('20.12.2020');
expect(formatDate('2020/10/10')).toBe('10.10.2020');
});
test('formatTime', () => {
expect(formatTime(1)).toBe('00:00');
expect(formatTime(20_000_000_000)).toBe('33:20');
expect(formatTime(100_000)).toBe('01:40');
});
test('differenceInDays', () => {
expect(differenceInDays('2019-01-25', '2018-06-05')).toBe(234);
expect(differenceInDays('2019-02-25', '2019-01-05')).toBe(51);
expect(differenceInDays('2020-01-01', '2019-01-01')).toBe(365);
});
test('addDaysToDate', () => {
expect(addDaysToDate('2019-01-25', 1)).toBe('2019-01-26');
expect(addDaysToDate('2019-02-25', 5)).toBe('2019-03-02');
expect(addDaysToDate('2020-01-01', 10)).toBe('2020-01-11');
});
});
@@ -0,0 +1,14 @@
/* eslint-disable jest/prefer-strict-equal */
import { formatServerValidation, formatServerError } from '../form';
import { ERROR, ERROR_OBJECT, DEFAULT_ERROR_MESSAGE } from '../test-constants';
describe('form utils', () => {
test('formatServerValidation', () => {
expect(formatServerValidation({})).toEqual({});
expect(formatServerValidation(ERROR)).toEqual({ Error: 'Error message example' });
});
test('formatServerError', () => {
expect(formatServerError({})).toEqual(DEFAULT_ERROR_MESSAGE);
expect(formatServerError(ERROR_OBJECT)).toEqual(ERROR_OBJECT.message);
});
});
@@ -0,0 +1,20 @@
import { formattedRate } from '@treasury-deals/common/utils/autoquotation';
describe('formattedRate', () => {
it('simple tests', () => {
// prettier-ignore
expect(formattedRate(75.55, 4)).toBe('75.5500');
// prettier-ignore
expect(formattedRate(75.134, 4)).toBe('75.1340');
expect(formattedRate(75.556_666, 4)).toBe('75.5567');
// prettier-ignore
expect(formattedRate(75.4, 6)).toBe('75.400000');
expect(formattedRate(75, 4)).toBe('75.0000');
});
it('returns a string with the correct number of characters when the number of chars is zero', () => {
const result = formattedRate(3.14, 0);
expect(result).toBe('3.14');
});
});
@@ -0,0 +1,121 @@
/* eslint-disable jest/prefer-strict-equal */
import { ACCOUNT_USAGE_TYPES } from '@treasury-deals/common/constants/accounts';
import * as user from '@treasury-deals/common/constants/user';
import {
formatDeclination,
formatDeclinationDays,
formatUsageItem,
formatAdminFilter,
formatParamsToServer,
formatTableSettingsColumns,
formatTableExportColumns,
getFilter,
formatNumberWithSpace,
formatNumberWithSpaceAndComma,
DECL_DAYS,
formatWithSpace,
} from '../formatters';
import {
DECL_ITEMS,
ADMIN_FILTERS,
ADMIN_FILTERS_RESULT,
TABLE_SETTINGS_COLUMNS,
TABLE_COLUMNS,
DATA_FOR_FORMATTING,
DATA_WITHOUT_LABEL_VALUE,
DATA_WITHOUT_LABEL,
} from '../test-constants';
jest.mock('common/constants/user', () => ({
__esModule: true,
isAdmin: true,
}));
const mockConfig = user as { isAdmin: boolean };
describe('formatters', () => {
test('formatDeclination', () => {
expect(formatDeclination(21, DECL_ITEMS)).toEqual(DECL_ITEMS[0]);
expect(formatDeclination(34, DECL_ITEMS)).toEqual(DECL_ITEMS[1]);
expect(formatDeclination(55, DECL_ITEMS)).toEqual(DECL_ITEMS[2]);
expect(formatDeclination(24, [])).toBeUndefined();
});
test('formatDeclinationDays', () => {
expect(formatDeclinationDays(31)).toBe(`31 ${DECL_DAYS[0]}`);
expect(formatDeclinationDays(24)).toBe(`24 ${DECL_DAYS[1]}`);
expect(formatDeclinationDays(45)).toBe(`45 ${DECL_DAYS[2]}`);
});
test('formatUsageItem', () => {
expect(formatUsageItem(ACCOUNT_USAGE_TYPES, 'DEPOSIT_VALUE')).toEqual(ACCOUNT_USAGE_TYPES[1].title);
expect(formatUsageItem(ACCOUNT_USAGE_TYPES, 'MNO')).toEqual(ACCOUNT_USAGE_TYPES[0].title);
});
test('formatAdminFilter', () => {
expect(formatAdminFilter(ADMIN_FILTERS.correct)).toMatchObject(ADMIN_FILTERS_RESULT.correct);
expect(formatAdminFilter(ADMIN_FILTERS.incorrect)).toMatchObject(ADMIN_FILTERS_RESULT.incorrect);
});
test('formatParamsToServer', () => {
expect(formatParamsToServer({ filter: ADMIN_FILTERS.correct })).toMatchObject({
filter: { ...ADMIN_FILTERS_RESULT.correct },
});
mockConfig.isAdmin = false;
expect(formatParamsToServer({ filter: ADMIN_FILTERS.correct })).toMatchObject({
filter: {},
});
});
test('formatTableSettingsColumns', () => {
mockConfig.isAdmin = true;
expect(formatTableSettingsColumns(TABLE_SETTINGS_COLUMNS.basic, TABLE_SETTINGS_COLUMNS.admin)).toEqual([
TABLE_SETTINGS_COLUMNS.admin[0],
TABLE_SETTINGS_COLUMNS.basic[0],
]);
mockConfig.isAdmin = false;
expect(formatTableSettingsColumns(TABLE_SETTINGS_COLUMNS.basic, TABLE_SETTINGS_COLUMNS.admin)).toEqual(TABLE_SETTINGS_COLUMNS.basic);
});
test('formatTableExportColumns', () => {
const { showedColumns, exportColumnsArr } = TABLE_COLUMNS;
expect(formatTableExportColumns(showedColumns.slice(0, 2), exportColumnsArr)).toEqual([exportColumnsArr[0], exportColumnsArr[2]]);
expect(formatTableExportColumns(showedColumns, exportColumnsArr)).toEqual([
exportColumnsArr[0],
exportColumnsArr[2],
exportColumnsArr[1],
exportColumnsArr[3],
]);
});
test('getFilter', () => {
expect(getFilter('label')(DATA_FOR_FORMATTING)).toEqual(DATA_WITHOUT_LABEL);
expect(getFilter('label', 'value')(DATA_FOR_FORMATTING)).toEqual(DATA_WITHOUT_LABEL_VALUE);
expect(getFilter('name', 'code')(DATA_FOR_FORMATTING)).toEqual(DATA_FOR_FORMATTING);
expect(getFilter('label', 'value')({})).toEqual({});
expect(getFilter('')(DATA_FOR_FORMATTING)).toEqual(DATA_FOR_FORMATTING);
});
test('formatNumberWithSpace', () => {
expect(formatNumberWithSpace(222_222_222_222.0)).toBe('222 222 222 222.00');
expect(formatNumberWithSpace('222222222222.00')).toBe('222 222 222 222.00');
expect(formatNumberWithSpace('')).toBe('-');
expect(formatNumberWithSpace(0)).toBe('-');
expect(formatNumberWithSpace('')).toBe('-');
});
test('formatNumberWithSpaceAndComma', () => {
expect(formatNumberWithSpaceAndComma(222_222_222_222.0)).toBe('222 222 222 222,00');
expect(formatNumberWithSpaceAndComma('222222222222.00')).toBe('222 222 222 222,00');
expect(formatNumberWithSpaceAndComma('')).toBe('-');
expect(formatNumberWithSpaceAndComma(0)).toBe('-');
expect(formatNumberWithSpaceAndComma('')).toBe('-');
expect(formatNumberWithSpaceAndComma(50_000, true, true)).toBe('50 000');
expect(formatNumberWithSpaceAndComma(50_000, true, false)).toBe('50 000,00');
});
test('formatWithSpace', () => {
expect(formatWithSpace('322222222222.00')).toBe('322 222 222 222.00');
expect(formatWithSpace('422222222222.00')).toBe('422 222 222 222.00');
expect(formatWithSpace('522222222222.00')).toBe('522 222 222 222.00');
});
});
@@ -0,0 +1,58 @@
/* eslint-disable @eco/no-missing-localization */
import React from 'react';
import { render } from '@testing-library/react';
import { renderErrorMessage } from '@treasury-deals/common/utils/autoquotation';
describe('renderErrorMessage', () => {
it('should render an empty string when there are no errors', () => {
const errors = {};
const result = renderErrorMessage(errors);
expect(result).toBe('');
const { asFragment } = render(<div>{result}</div>);
expect(asFragment()).toMatchSnapshot();
});
it('should render an error message for each error field', () => {
const errors = {
amountBuy: 'Error in amountBuy field',
accountSell: 'Error in accountSell field',
rate: 'Error in rate field',
};
const result = renderErrorMessage(errors);
expect(result).toContain('Ошибка в поле покупка');
expect(result).toContain('Ошибка в поле счет списания');
expect(result).toContain('Ошибка в поле курс');
expect(result).not.toContain('Ошибка в поле продажа');
expect(result).not.toContain('Ошибка в поле счет зачисления');
expect(result).not.toContain('Ошибка в поле генеральное соглашение');
const { asFragment } = render(<div>{result}</div>);
expect(asFragment()).toMatchSnapshot();
});
it('should render an error message for each error field even if some fields have no errors', () => {
const errors = {
amountBuy: 'Error in amountBuy field',
accountBuy: 'Error in accountBuy field',
};
const result = renderErrorMessage(errors);
expect(result).toContain('Ошибка в поле покупка');
expect(result).not.toContain('Ошибка в поле продажа');
expect(result).toContain('Ошибка в поле счет зачисления');
expect(result).not.toContain('Ошибка в поле счет списания');
expect(result).not.toContain('Ошибка в поле курса');
expect(result).not.toContain('Ошибка в поле генерального соглашения');
const { asFragment } = render(<div>{result}</div>);
expect(asFragment()).toMatchSnapshot();
});
});
@@ -0,0 +1,162 @@
/* eslint-disable @eco/no-missing-localization */
import { render, screen } from '@testing-library/react';
import type { ITradingInterval } from '@treasury-deals/common/interfaces/autoquotations/autoquote-v2';
import { renderHoverText } from '@treasury-deals/common/utils/autoquotation';
import { filterElementsById } from '@treasury-deals/common/utils/shared';
import '@testing-library/jest-dom';
describe('filterElementsById', () => {
it('должен правильно фильтровать элементы', () => {
const idArray = ['id1', 'id2'];
const elementsArray = [
{
id: 'id1',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
{
id: 'id2',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: false }],
},
{
id: 'id3',
userActions: [],
},
];
const filteredElements = filterElementsById(idArray, elementsArray);
expect(filteredElements).toStrictEqual([
{
id: 'id1',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
]);
});
it('должен возвращать пустой массив при отсутствии совпадений', () => {
const idArray = ['id4', 'id5'];
const elementsArray = [
{
id: 'id1',
userActions: [],
},
{
id: 'id2',
userActions: [],
},
];
const filteredElements = filterElementsById(idArray, elementsArray);
expect(filteredElements).toStrictEqual([]);
});
it('должен возвращать пустой массив при отсутствии элементов с type "DELETE"', () => {
const idArray = ['id1', 'id2'];
const elementsArray = [
{
id: 'id1',
userActions: [],
},
{
id: 'id2',
userActions: [{ type: 'UPDATE', allowed: true, allowedForUser: true }],
},
];
const filteredElements = filterElementsById(idArray, elementsArray);
expect(filteredElements).toStrictEqual([]);
});
it('должен возвращать пустой массив при deleteAction.allowed: false', () => {
const idArray = ['id1', 'id2'];
const elementsArray = [
{
id: 'id1',
userActions: [{ type: 'DELETE', allowed: false, allowedForUser: true }],
},
{
id: 'id2',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
];
const filteredElements = filterElementsById(idArray, elementsArray);
expect(filteredElements).toStrictEqual([
{
id: 'id2',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
]);
});
it('должен возвращать пустой массив при deleteAction.allowedForUser: false', () => {
const idArray = ['id1', 'id2'];
const elementsArray = [
{
id: 'id1',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: false }],
},
{
id: 'id2',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
];
const filteredElements = filterElementsById(idArray, elementsArray);
expect(filteredElements).toStrictEqual([
{
id: 'id2',
userActions: [{ type: 'DELETE', allowed: true, allowedForUser: true }],
},
]);
});
});
describe('Функция renderHoverText', () => {
const tradingIntervals: Record<string, ITradingInterval[]> = {
buy: [
{ timeFrom: '18:00:00', timeTo: '20:00:00' },
{ timeFrom: '09:00:00', timeTo: '12:00:00' },
{ timeFrom: '14:00:00', timeTo: '18:00:00' },
],
sell: [{ timeFrom: '10:00:00', timeTo: '13:00:00' }],
};
const tradingIntervalsThree: Record<string, ITradingInterval[]> = {
buy: [
{ timeFrom: '15:00:00', timeTo: '16:00:00' },
{ timeFrom: '02:00:00', timeTo: '13:50:00' },
{ timeFrom: '18:00:00', timeTo: '23:00:00' },
],
sell: [{ timeFrom: '02:00:00', timeTo: '23:50:00' }],
};
const tradingIntervalsFour: Record<string, ITradingInterval[]> = {
buy: [{ timeFrom: '02:00:00', timeTo: '13:50:00' }],
sell: [{ timeFrom: '02:00:00', timeTo: '13:50:00' }],
};
it('должен отображать правильный текст, когда доступны оба направления', () => {
render(renderHoverText(tradingIntervals, false));
expect(screen.getByText('Доступно')).toBeInTheDocument();
expect(screen.getByText('Покупка с 09:00 до 12:00')).toBeInTheDocument();
expect(screen.getByText('Покупка с 14:00 до 18:00')).toBeInTheDocument();
});
it('вывод нескольких доступных интервалов', () => {
render(renderHoverText(tradingIntervalsThree, false));
expect(screen.getByText('Покупка с 02:00 до 13:50')).toBeInTheDocument();
expect(screen.getByText('Покупка с 15:00 до 16:00')).toBeInTheDocument();
expect(screen.getByText('Покупка с 18:00 до 23:00')).toBeInTheDocument();
expect(screen.getByText('Продажа с 02:00 до 23:50')).toBeInTheDocument();
});
it('вывод одинаковых интервалов', () => {
render(renderHoverText(tradingIntervalsFour, false));
expect(screen.getByText('Покупка и Продажа с 02:00 до 13:50')).toBeInTheDocument();
});
});
@@ -0,0 +1,72 @@
/* eslint-disable jest/prefer-strict-equal */
import * as user from '@treasury-deals/common/constants/user';
import { selectRow, getTableColumns, getShowedColumnsIds, getTableSettingsColumns } from '../table';
import { ROW_1, ROW_2, TABLE_COLUMNS } from '../test-constants';
// https://mikeborozdin.com/post/changing-jest-mocks-between-tests/
jest.mock('common/constants/user', () => ({
__esModule: true,
isAdmin: true,
}));
const mockConfig = user as { isAdmin: boolean };
describe('table utils', () => {
test('selectRow', () => {
expect(selectRow([], ROW_1)).toContain(ROW_1);
expect(selectRow([ROW_1, ROW_2], ROW_1)).toEqual([ROW_2]);
expect(selectRow([], [ROW_1, ROW_2])).toEqual([ROW_1, ROW_2]);
expect(selectRow([ROW_1, ROW_2], [])).toEqual([]);
expect(selectRow([], [])).toEqual([]);
});
test('getTableColumns', () => {
const { showedColumns, tableColumnsArr, columnSizes } = TABLE_COLUMNS;
expect(getTableColumns(showedColumns.slice(0, 2), columnSizes, tableColumnsArr)).toEqual([
{
...tableColumnsArr[0],
width: columnSizes.accountNameExtended,
},
{
...tableColumnsArr[2],
width: columnSizes.currency,
},
]);
expect(getTableColumns(showedColumns.slice(0, 1), columnSizes, tableColumnsArr)).toEqual([
{
...tableColumnsArr[0],
width: columnSizes.accountNameExtended,
},
]);
expect(getTableColumns(showedColumns, columnSizes, tableColumnsArr)).toEqual([
{
...tableColumnsArr[0],
width: columnSizes.accountNameExtended,
},
{
...tableColumnsArr[2],
width: columnSizes.currency,
},
{
...tableColumnsArr[1],
},
]);
});
test('getShowedColumnsIds', () => {
const { tableSettings, showedColumns } = TABLE_COLUMNS;
expect(getShowedColumnsIds(tableSettings)).toEqual(showedColumns);
});
test('getTableSettingsColumns', () => {
const { tableSettings, adminTableSettings } = TABLE_COLUMNS;
expect(getTableSettingsColumns(tableSettings, adminTableSettings)).toEqual([...adminTableSettings, ...tableSettings]);
mockConfig.isAdmin = false;
expect(getTableSettingsColumns(tableSettings, adminTableSettings)).toEqual(tableSettings);
});
});
@@ -1,7 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react';
import type { ModalProps } from '@fractal-ui/overlays';
import { CryptoInstallerModal, ERROR } from '@msb/platform-compat/services/client';
import { appendToPortal, deleteFromPortal } from '@msb/platform-compat/ui';
import { appendToPortal, deleteFromPortal } from '@treasury-deals/common/components/portal-container';
import { serverErrorMessage, signErrorMessage } from '@treasury-deals/common/constants/error-messages';
import type { Func } from '@treasury-deals/common/interfaces/common';
import { formatServerError, getSignError } from '@treasury-deals/common/utils/form';
@@ -2910,20 +2910,28 @@
"common.successPage.layout.header": {
"ru": "Подтверждение отправлено"
},
"common.dealType.createDeal": {
"common.createDeal": {
"ru": "Заключить сделку"
},
"common.dealType.deposit": {
"ru": "Заявка на депозит"
"common.dealType.text": {
"@dealType": "string",
"ru": {
"dealType === 'DEPOSIT'": "Заявка на депозит",
"dealType === 'MNO'": "Заявка на МНО",
"dealType === 'GSNO'": "Заявка на ГСНО",
"dealType === 'FX'": "Заявка на конверсию",
"true": ""
}
},
"common.dealType.mno": {
"ru": "Заявка на МНО"
},
"common.dealType.gsno": {
"ru": "Заявка на ГСНО"
},
"common.dealType.conversion": {
"ru": "Заявка на конверсию"
"common.dealType.description": {
"@dealType": "string",
"ru": {
"dealType === 'DEPOSIT'": "",
"dealType === 'MNO'": "Неснижаемый остаток по счёту",
"dealType === 'GSNO'": "Неснижаемый остаток группы счетов",
"dealType === 'FX'": "",
"true": ""
}
},
"common.rateTimer.caption.end": {
"ru": "Срок действия предложенной ставки истек. Пожалуйста, нажмите на кнопку «Запросить ставку», либо поменяйте параметры сделки, чтобы получить новое предложение."
File diff suppressed because it is too large Load Diff
@@ -238,7 +238,6 @@ export const StyledRequestBody = styled.div`
`;
export const StyledCounterText = styled.div`
@media (width <= 744px) {
margin-top: 16px;
margin-bottom: 16px;
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import { Spinner } from '@fractal-ui/core';
import { Fields } from '@fractal-ui/form';
import { Wrapper, Text } from '@fractal-ui/styling';
@@ -6,20 +6,35 @@ import { getInternalAccountsAppend } from '@treasury-deals/common/sources/accoun
import { formatAccountNumber } from '@treasury-deals/common/utils/accounts';
import { isEmpty } from '@treasury-deals/common/utils/check-types';
import { locale } from '@treasury-deals/localization';
import { useAccountsContext } from '@treasury-deals/pages/accounts/create-account/context';
import { COMMON_FIELDS } from '@treasury-deals/pages/accounts/create-account/enums';
import { useAllInternal } from '@treasury-deals/pages/accounts/create-account/hooks/use-all-internal-form';
import { useSelectOrganization } from '@treasury-deals/pages/accounts/create-account/hooks/use-select-organization';
import { formatAccountInitialValues } from '@treasury-deals/pages/accounts/create-account/utils/form';
import { gap } from '@treasury-deals/pages/dashboard/constants/styles';
import { Access } from '@treasury-deals/pages/reports-and-subscription/reports/components/access';
import { useForm, useFormState } from 'react-final-form';
import { SelectOrganization } from '../inputs/select-organization';
import { DrawerContainer } from '../styled';
export const ChooseAccount: React.FC = () => {
const { accounts, isLoading, onSelectAll, checkScroll, containerRef, unselectAllSelected, isFetchingNextPage } =
useAllInternal(getInternalAccountsAppend);
const { reset } = useForm();
const {
values: { organization },
} = useFormState({ subscription: { values: true } });
const { userInfo, formType } = useAccountsContext();
const { handleChange, options, organizations } = useSelectOrganization(null, false);
const handleReset = useCallback(() => {
const init = formatAccountInitialValues(formType);
reset(init);
}, [formType, reset]);
return (
<DrawerContainer ref={containerRef} rowGap={gap.m} onScroll={checkScroll}>
<Access action={handleReset} id={organization} privilege="accountsPrivilegeLevel" searchField="clientId" userInfo={userInfo} />
<SelectOrganization
handleChange={handleChange}
isEdit={false}
@@ -29,26 +44,27 @@ export const ChooseAccount: React.FC = () => {
organizations={organizations}
/>
{isLoading && <Spinner dataName="accountsLoading" size="L" />}
{Array.isArray(accounts) && isEmpty(accounts) ? (
<Text.P2>{locale.account.form.create.text.noAccounts}</Text.P2>
) : (
<Wrapper display="grid" gridRowGap={gap.m}>
{accounts && accounts.length > 0 && (
<Wrapper borderBottomColor="bg.tertiary" borderBottomStyle="solid" borderBottomWidth="1px" pb={gap.s}>
<Fields.Checkbox label={locale.account.fields.checkbox.add} name="allSelected" onChange={onSelectAll} />
</Wrapper>
)}
{accounts?.map(account => (
<Fields.Checkbox
key={`accounts.account-${account.accountNumber}`}
label={formatAccountNumber(account.accountNumber)}
name={`accounts.account-${account.accountNumber}`}
onChange={unselectAllSelected}
/>
))}
{isFetchingNextPage && <Spinner dataName="nexPageLoading" />}
</Wrapper>
)}
{organization &&
(Array.isArray(accounts) && isEmpty(accounts) ? (
<Text.P2>{locale.account.form.create.text.noAccounts}</Text.P2>
) : (
<Wrapper display="grid" gridRowGap={gap.m}>
{accounts && accounts.length > 0 && (
<Wrapper borderBottomColor="bg.tertiary" borderBottomStyle="solid" borderBottomWidth="1px" pb={gap.s}>
<Fields.Checkbox label={locale.account.fields.checkbox.add} name="allSelected" onChange={onSelectAll} />
</Wrapper>
)}
{accounts?.map(account => (
<Fields.Checkbox
key={`accounts.account-${account.accountNumber}`}
label={formatAccountNumber(account.accountNumber)}
name={`accounts.account-${account.accountNumber}`}
onChange={unselectAllSelected}
/>
))}
{isFetchingNextPage && <Spinner dataName="nexPageLoading" />}
</Wrapper>
))}
</DrawerContainer>
);
};
@@ -113,14 +113,15 @@ export const Scroller: React.FC<IScroller> = ({
const filtersWithoutClientId = Object.fromEntries(Object.entries(filters).filter(([key]) => key !== 'clientId'));
return (
<MultiArmAccessMatrix
generalAgreement={access.hasGeneralAgreement}
hasDeals={tableData.length > 0}
systemAgreement={access.hasSystemAgreement}
>
{isEmpty(filterObjectByNonEmptyArrays(filtersWithoutClientId)) && tableData.length === 0 && !table.isLoading ? (
<>
{isEmpty(filterObjectByNonEmptyArrays(filtersWithoutClientId)) && tableData.length === 0 && !table.isLoading && (
<EmptyAccountsPlaceholder isMobile={breakpoints.XS} onOpenForm={onOpenForm} />
) : (
)}
<MultiArmAccessMatrix
generalAgreement={access.hasGeneralAgreement}
hasDeals={tableData.length > 0}
systemAgreement={access.hasSystemAgreement}
>
<TableWrapper flexGrow="1">
{breakpoints.XS ? (
<CardScroller
@@ -175,7 +176,7 @@ export const Scroller: React.FC<IScroller> = ({
onSubmit={handlerOnSubmit}
/>
</TableWrapper>
)}
</MultiArmAccessMatrix>
</MultiArmAccessMatrix>
</>
);
};
@@ -53,3 +53,5 @@ export const STATUS_BADGE_TYPE: Record<string, BadgeType> = {
ENDED: 'system',
BLOCKED: 'system',
};
export const PAGE_CONTENT_ID = '#page-content';
@@ -5,16 +5,17 @@ import { SERVICE_NAME } from '@treasury-deals/common/constants';
import type { Step } from '@treasury-deals/common/interfaces/wizard';
import { POSITION } from '@treasury-deals/common/interfaces/wizard';
import { locale } from '@treasury-deals/localization';
import { PAGE_CONTENT_ID } from './table';
export const WIZARD_RULE_DEFAULT: Step[] = [
{
selector: '#page-content',
selector: PAGE_CONTENT_ID,
position: POSITION.CENTER,
description: locale.wizard.agreement.page({ service: SERVICE_NAME }),
icon: <WdIcons.Agreements scale={'X2L'} />,
},
{
selector: '[data-role="header"]',
selector: PAGE_CONTENT_ID,
position: POSITION.BOTTOM_CENTER,
description: locale.wizard.agreement.default,
icon: <img height="160" src={Images.AgreementDefault} width="400" />,
@@ -34,7 +35,7 @@ export const WIZARD_RULE_DEFAULT: Step[] = [
export const WIZARD_RULE_EXTENDED: Step[] = [
...WIZARD_RULE_DEFAULT.slice(0, 2),
{
selector: '#page-content',
selector: PAGE_CONTENT_ID,
position: POSITION.CENTER,
description: locale.wizard.agreement.gsno,
width: 1010,
@@ -47,7 +47,7 @@ const Header: React.FC = () => {
return (
<HeaderContainer px={{ XS: 'calendar.mobileHeaderPx', S: '0' }} onClick={handleContainerClick}>
<Wrapper display="flex" height="32px" pb="12px" pt="16px">
<Wrapper display="flex" pb="12px" pt="16px">
<StyledHeaderButton
data-action="monthselect"
data-name="calendar-header-month-button"
@@ -15,7 +15,7 @@ export const ReportsBannerTab: React.FC = () => {
<BannerTab onClick={toReports}>
<Wrapper display="flex" flexDirection="column" justifyContent="space-evenly" pb={2} pt={2}>
<Title.H5>Выписки по депозитным счетам</Title.H5>
<Button dataAction="to-reports" height="24px" shape="default" size="XS" width="125px" onClick={toReports}>
<Button dataAction="to-reports" height="24px" shape="default" size="XS" width="130px" onClick={toReports}>
Получить выписку
</Button>
</Wrapper>
@@ -8,6 +8,7 @@ import { Overlay } from '@msb/shared/ui/loader';
import { MultiArmAccessMatrix, Wizard } from '@treasury-deals/common/components/controls';
import { TableFilter } from '@treasury-deals/common/components/table/table-filter-fractal';
import { isAdmin } from '@treasury-deals/common/constants';
import { isMsb } from '@treasury-deals/common/utils/is-msb';
import { locale } from '@treasury-deals/localization';
import { AutoQuotationV2 } from '@treasury-deals/modules/autoquotation-v2';
import { RequestsBlock } from '@treasury-deals/modules/autoquotation-v2/components/requests-block';
@@ -93,7 +94,7 @@ export const TableView = ({
}) => (
<GroupedRefetchProvider isEnabled={filteredOrganizations.length > 1} isGrouped={isGrouped}>
<Wrapper data-id="tables-data" display="flex" flexDirection="column" height="100%">
{!isAdmin && hasAutoQuotationAccess && (
{!isMsb() && !isAdmin && hasAutoQuotationAccess && (
<>
{fakeAutoquoteWizard && <AutoQuotationWizard newbieSettings={newbieSettings} refreshNewbie={refreshNewbie} />}
{!fakeAutoquoteWizard && (
@@ -1,15 +1,19 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import React, { useMemo } from 'react';
import { ProgressDots } from '@fractal-ui/core';
import { Informer } from '@fractal-ui/extended';
import { Wrapper, useBreakpoints } from '@fractal-ui/styling';
import { SpinnerWrapped } from '@treasury-deals/common/components/controls';
import { STATISTIC_TYPE } from '@treasury-deals/common/enums/statistics';
import { useFeatureToggles } from '@treasury-deals/common/hooks';
import { StatisticService } from '@treasury-deals/common/services';
import { returnDocTypeBinder } from '@treasury-deals/common/utils/table';
import { locale } from '@treasury-deals/localization';
import { gap } from '@treasury-deals/pages/dashboard/constants/styles';
import { DEAL_EDIT_FIELDS } from '@treasury-deals/pages/deals/drawer-forms-old/constants/constants';
import { checkIsGeneralAgreementDU, checkIsSingleOption } from '@treasury-deals/pages/deals/drawer-forms-old/helpers';
import type { BestRate, CustomOption } from '@treasury-deals/pages/deals/drawer-forms-old/interfaces';
import { useForm } from 'react-final-form';
import { useDealForm } from '../../hooks/use-form';
import { ActionBar } from '../action-bars/deals-action-bar';
import { CustomSelect, SelectField } from '../common-inputs';
@@ -24,6 +28,7 @@ import { dealProgressBarWidth, dealProgressBarWidthXSRequest, FormContainer, Ani
export const Fields = ({ type, values, submitError, modified }) => {
const toggles = useFeatureToggles();
const { getState } = useForm();
const {
generalAgreements,
isGeneralAgreementDU,
@@ -53,6 +58,8 @@ export const Fields = ({ type, values, submitError, modified }) => {
const handleApplyRate = async (rate: BestRate) => {
await actions.applyBestRate(rate);
StatisticService.logRequest(STATISTIC_TYPE.DEAL_BEST_RATE_INIT, location.href, getState().values, {});
};
const { clientWebDealingId, generalAgreement, dealRate, managementFund } = values;
@@ -46,7 +46,7 @@ export const cryptoSignController =
const { signKind, certificateId } = certificate;
const { checkTimer, cloudExec, cloudResult, cloudResultError, timeout, cryptoServiceSignExec, signAndSendExec } = signParams;
const { dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source } = sendParams;
const { dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source, deal, isApplyBestRate } = sendParams;
const signature =
signKind === SIGN_KIND.CLOUD_SIGN
@@ -94,6 +94,10 @@ export const cryptoSignController =
signResult
);
if (isApplyBestRate) {
StatisticService.logRequest(STATISTIC_TYPE.DEAL_BEST_RATE_SIGN, location.href, { ...deal }, {});
}
if (!isConfirmation) onClose(false);
} catch (err) {
const { message, code } = formatError(err);
@@ -30,8 +30,20 @@ import { useDealStore } from '../store';
import { updateBestRatesSelector } from '../store/selectors';
export const useFormActions = (requestType: string, clientId: string, isConfirmed: boolean, isTimeEnd: boolean): IUseFormActionsReturn => {
const { dispatch, setOnSubmit, onClose, type, checkMatrixClientById, mode, dealsNotification, dirtyForm, deal, setSaved, goBack } =
useDealContext();
const {
dispatch,
setOnSubmit,
onClose,
type,
checkMatrixClientById,
mode,
dealsNotification,
dirtyForm,
deal,
setSaved,
goBack,
setBestRateApply,
} = useDealContext();
const { url, source, tab } = useGetPath();
const { getState, change, submit } = useForm();
const { values, submitError } = getState();
@@ -55,30 +67,35 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
setTimeout(submit, time);
}, 200);
const handleRateRequest = useCallback(() => {
updateBestRates(undefined);
setOnSubmit({
type: 'initialize',
submitFunc: async formData => {
const res = await rateExec(formData);
const handleRateRequest = useCallback(
() =>
new Promise<void>(resolve => {
updateBestRates(undefined);
setOnSubmit({
type: 'initialize',
submitFunc: async formData => {
const res = await rateExec(formData);
if (res?.dealRate) {
change(DEAL_EDIT_FIELDS.DEAL_RATE, res.dealRate);
}
if (res?.dealRate) {
change(DEAL_EDIT_FIELDS.DEAL_RATE, res.dealRate);
}
return res;
},
submitType: 'rateRequest',
});
return res;
},
submitType: 'rateRequest',
});
setTimeout(
() =>
submit()?.then(() => {
afterRateRequest(dispatch);
}),
0
);
}, [dispatch, rateExec, setOnSubmit, submit, getBestRatesExec, type]);
setTimeout(
() =>
submit()?.then(() => {
afterRateRequest(dispatch);
resolve();
}),
0
);
}),
[dispatch, rateExec, setOnSubmit, submit, getBestRatesExec, type]
);
const rateRequest = useDebouncedCallback(() => {
const { values: formValues } = getState();
@@ -91,7 +108,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
return;
}
handleRateRequest();
void handleRateRequest();
}, 700);
const sign = useCallback(async () => {
@@ -164,7 +181,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
okButtonText: locale.agreement.modal.check.error.decline,
preventSimpleClose: true,
cancelCallback: () => {
if (isAllowedRequest && !hasDealRate) handleRateRequest();
if (isAllowedRequest && !hasDealRate) void handleRateRequest();
},
okCallback: () => {
onClose();
@@ -238,7 +255,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
handleValidateForm();
};
const applyBestRate = (best: BestRate) => {
const applyBestRate = async (best: BestRate) => {
if (!best) return;
const amount = best.amount?.replace(/\s/g, '') ?? '';
@@ -257,7 +274,9 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
change(DEAL_EDIT_FIELDS.DEAL_RATE, undefined);
handleRateRequest();
setBestRateApply(best.rate);
await handleRateRequest();
};
const redirectToAccounts = useCallback(() => {
@@ -22,10 +22,11 @@ interface DstForSign extends IDstForSign {
}
export const useSign = () => {
const { dealsNotification, onClose, deal, type, clientId, dispatch, isConfirmation, setSaved, goBack } = useDealContext();
const { dealsNotification, onClose, deal, type, clientId, dispatch, isConfirmation, setSaved, goBack, bestRateApply } = useDealContext();
const { url, source } = useGetPath();
const isFx = type === DEPOSITS_OPERATION_TYPES_ENUM.CONVERSION;
const hasVisa = 'visaType' in deal;
const isApplyBestRate = 'dealRate' in deal && deal?.dealRate?.value === bestRateApply;
const requestType = type.toLowerCase();
const crypto = getCloudCrypto();
const [, cloudResult, cloudResultError] = useExecute(crypto.getSignResult);
@@ -91,7 +92,7 @@ export const useSign = () => {
const cryptoSign = cryptoSignController(
{ checkTimer, cloudExec, cloudResult, cloudResultError, timeout, cryptoServiceSignExec, signAndSendExec },
{ dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source }
{ dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source, deal, isApplyBestRate }
);
const handleCertAndSign = certAndSignController({
@@ -79,6 +79,7 @@ export const DrawerFormsOld: React.FC<IDrawerForm> = ({ clientIds, refresh }) =>
} = useDeal(dealType, dealId, dealMode, source, clientIds as string[]);
const [onSubmit, setOnSubmit] = useState<SubmitFuncStorage>({ submitFunc: noop, type: 'initialize' });
const resetStore = useDealStore(state => state.reset);
const [bestRateApply, setBestRateApply] = useState<string>();
const { userInfo } = useUserInfo({});
@@ -157,6 +158,8 @@ export const DrawerFormsOld: React.FC<IDrawerForm> = ({ clientIds, refresh }) =>
canCloseWithoutWarning,
goBack,
searchParams,
bestRateApply,
setBestRateApply,
}),
[
deal,
@@ -188,6 +191,8 @@ export const DrawerFormsOld: React.FC<IDrawerForm> = ({ clientIds, refresh }) =>
canCloseWithoutWarning,
goBack,
searchParams,
bestRateApply,
setBestRateApply,
]
);
@@ -126,6 +126,10 @@ export interface DealPageContext {
* Параметры url, подмешиваются в сделку.
*/
searchParams: Record<string, string> | null | undefined;
/** Лучшая ставка. */
bestRateApply?: string;
/** Изменить лучшую ставку. */
setBestRateApply: Dispatch<SetStateAction<string | undefined>>;
}
export interface SubmitFuncStorage {
@@ -32,6 +32,8 @@ export interface SendParams {
goBack(): void;
dispatch: Dispatch<DispatchAction>;
setSaved: Dispatch<SetStateAction<boolean>>;
deal: DealDto | IFxDealsDto;
isApplyBestRate: boolean;
}
export interface CertAndSign {
@@ -1,8 +1,11 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
import React from 'react';
import { Informer } from '@fractal-ui/extended';
import { SearchIcon } from '@fractal-ui/library';
import { Wrapper, useBreakpoints } from '@fractal-ui/styling';
import { STATISTIC_TYPE } from '@treasury-deals/common/enums/statistics';
import { useFeatureToggles } from '@treasury-deals/common/hooks';
import { StatisticService } from '@treasury-deals/common/services';
import { LABEL_POSITION } from '@treasury-deals/fractal/interfaces';
import ModalService from '@treasury-deals/fractal/services/modal-service';
import { locale } from '@treasury-deals/localization';
@@ -11,6 +14,7 @@ import { DEAL_EDIT_FIELDS, formSubmitErrors } from '@treasury-deals/pages/deals/
import { checkIsSingleOption } from '@treasury-deals/pages/deals/drawer-forms/helpers';
import type { BestRate } from '@treasury-deals/pages/deals/drawer-forms/interfaces';
import { useTimerStore } from '@treasury-deals/pages/deals/drawer-forms/store/timer';
import { useForm } from 'react-final-form';
import { pickHighestRate } from '../../helpers/drawer';
import { useDealForm } from '../../hooks/use-form';
import { useDealStore } from '../../store';
@@ -32,6 +36,7 @@ import { dealProgressBarWidth, AnimatedContainer, FormGridContainer } from './st
export const FormFields = ({ type, values, submitError, modified, userInfoLoading }) => {
const toggles = useFeatureToggles();
const { getState } = useForm();
const {
generalAgreements,
isGeneralAgreementDU,
@@ -80,6 +85,8 @@ export const FormFields = ({ type, values, submitError, modified, userInfoLoadin
const handleApplyRate = async (rate: BestRate) => {
await actions.applyBestRate(rate);
StatisticService.logRequest(STATISTIC_TYPE.DEAL_BEST_RATE_INIT, location.href, getState().values, {});
updateBestRates(undefined);
};
@@ -46,7 +46,7 @@ export const cryptoSignController =
const { signKind, certificateId } = certificate;
const { checkTimer, cloudExec, cloudResult, cloudResultError, timeout, cryptoServiceSignExec, signAndSendExec } = signParams;
const { dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source } = sendParams;
const { dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source, deal, isApplyBestRate } = sendParams;
const signature =
signKind === SIGN_KIND.CLOUD_SIGN
@@ -94,6 +94,10 @@ export const cryptoSignController =
signResult
);
if (isApplyBestRate) {
StatisticService.logRequest(STATISTIC_TYPE.DEAL_BEST_RATE_SIGN, location.href, { ...deal }, {});
}
if (!isConfirmation) onClose(false);
} catch (err) {
const { message, code } = formatError(err);
@@ -30,8 +30,19 @@ import { useDealStore } from '../store';
import { useTimerStore } from '../store/timer';
export const useFormActions = (requestType: string, clientId: string, isConfirmed: boolean): IUseFormActionsReturn => {
const { dispatch, setOnSubmit, onClose, type, checkMatrixClientById, mode, dealsNotification, dirtyForm, setSaved, goBack } =
useDealContext();
const {
dispatch,
setOnSubmit,
onClose,
type,
checkMatrixClientById,
mode,
dealsNotification,
dirtyForm,
setSaved,
goBack,
setBestRateApply,
} = useDealContext();
const { url, source, tab } = useGetPath();
const { getState, change, submit } = useForm();
const { values, submitError } = getState();
@@ -56,23 +67,28 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
setTimeout(submit, time);
}, 200);
const handleRateRequest = useCallback(() => {
updateBestRates(undefined);
change(DEAL_EDIT_FIELDS.DEAL_RATE);
setOnSubmit({
type: 'initialize',
submitFunc: rateExec,
submitType: 'rateRequest',
});
const handleRateRequest = useCallback(
() =>
new Promise<void>(resolve => {
updateBestRates(undefined);
change(DEAL_EDIT_FIELDS.DEAL_RATE);
setOnSubmit({
type: 'initialize',
submitFunc: rateExec,
submitType: 'rateRequest',
});
setTimeout(
() =>
submit()?.then(() => {
afterRateRequest(dispatch);
}),
0
);
}, [updateBestRates, change, setOnSubmit, rateExec, submit, dispatch]);
setTimeout(
() =>
submit()?.then(() => {
afterRateRequest(dispatch);
resolve();
}),
0
);
}),
[updateBestRates, change, setOnSubmit, rateExec, submit, dispatch]
);
const rateRequest = useDebouncedCallback(() => {
const { values: formValues } = getState();
@@ -87,7 +103,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
return;
}
handleRateRequest();
void handleRateRequest();
}, 700);
const sign = useCallback(async () => {
@@ -160,7 +176,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
okButtonText: locale.agreement.modal.check.error.decline,
preventSimpleClose: true,
cancelCallback: () => {
if (isAllowedRequest && !hasDealRate) handleRateRequest();
if (isAllowedRequest && !hasDealRate) void handleRateRequest();
},
okCallback: () => {
onClose();
@@ -232,7 +248,7 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
handleValidateForm();
};
const applyBestRate = (best: BestRate) => {
const applyBestRate = async (best: BestRate) => {
if (!best) return;
const amount = best.amount?.replace(/\s/g, '') ?? '';
@@ -251,7 +267,9 @@ export const useFormActions = (requestType: string, clientId: string, isConfirme
change(DEAL_EDIT_FIELDS.DEAL_RATE, undefined);
handleRateRequest();
setBestRateApply(best.rate);
await handleRateRequest();
};
const redirectToAccounts = useCallback(() => {
@@ -22,10 +22,11 @@ interface DstForSign extends IDstForSign {
}
export const useSign = () => {
const { dealsNotification, onClose, deal, type, clientId, dispatch, isConfirmation, setSaved, goBack } = useDealContext();
const { dealsNotification, onClose, deal, type, clientId, dispatch, isConfirmation, setSaved, goBack, bestRateApply } = useDealContext();
const { url, source } = useGetPath();
const isFx = type === DEPOSITS_OPERATION_TYPES_ENUM.CONVERSION;
const hasVisa = 'visaType' in deal;
const isApplyBestRate = 'dealRate' in deal && deal?.dealRate?.value === bestRateApply;
const requestType = type.toLowerCase();
const crypto = getCloudCrypto();
const [, cloudResult, cloudResultError] = useExecute(crypto.getSignResult);
@@ -90,7 +91,7 @@ export const useSign = () => {
const cryptoSign = cryptoSignController(
{ checkTimer, cloudExec, cloudResult, cloudResultError, timeout, cryptoServiceSignExec, signAndSendExec },
{ dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source }
{ dealsNotification, isConfirmation, onClose, dispatch, setSaved, goBack, url, source, deal, isApplyBestRate }
);
const handleCertAndSign = certAndSignController({
@@ -82,6 +82,7 @@ export const DrawerForms: React.FC<IDrawerForm> = ({ clientIds, refresh }) => {
const [onSubmit, setOnSubmit] = useState<SubmitFuncStorage>({ submitFunc: noop, type: 'initialize' });
const resetStore = useDealStore(state => state.reset);
const clearAll = useTimerStore(state => state.clearAll);
const [bestRateApply, setBestRateApply] = useState<string>();
const { setupUserInfoState, reset } = useUserInfoStore(state => ({ setupUserInfoState: state.setupUserInfoState, reset: state.reset }));
const { userInfoLoading, userInfo } = useUserInfo({
onSuccess: setupUserInfoState,
@@ -162,6 +163,8 @@ export const DrawerForms: React.FC<IDrawerForm> = ({ clientIds, refresh }) => {
canCloseWithoutWarning,
goBack,
searchParams,
bestRateApply,
setBestRateApply,
}),
[
deal,
@@ -193,6 +196,8 @@ export const DrawerForms: React.FC<IDrawerForm> = ({ clientIds, refresh }) => {
canCloseWithoutWarning,
goBack,
searchParams,
bestRateApply,
setBestRateApply,
]
);
@@ -27,6 +27,7 @@ export interface DealParameters {
setMNO: boolean | string;
signTimeEnded: boolean;
expired: boolean;
bestRateDeal: BestRate;
}
export interface FractalSelectOnChange extends IFractalOnChangeFieldArgs {
@@ -125,6 +126,10 @@ export interface DealPageContext {
* Параметры url, подмешиваются в сделку.
*/
searchParams: Record<string, string> | null | undefined;
/** Лучшая ставка. */
bestRateApply?: string;
/** Изменить лучшую ставку. */
setBestRateApply: Dispatch<SetStateAction<string | undefined>>;
}
export interface SubmitFuncStorage {
@@ -32,6 +32,8 @@ export interface SendParams {
goBack(): void;
dispatch: Dispatch<DispatchAction>;
setSaved: Dispatch<SetStateAction<boolean>>;
deal: DealDto | IFxDealsDto;
isApplyBestRate: boolean;
}
export interface CertAndSign {
@@ -233,7 +233,7 @@ export const DEPOSIT_TABLE_SETTINGS_COLUMNS = [
{
id: 'period',
title: locale.table.column.period,
show: !isMsb(),
show: true,
},
{
id: 'amount',
@@ -351,7 +351,7 @@ export const DEPOSIT_DEFAULT_COLUMN_SETTINGS = [
...((!isMsb() && [['dealDate']]) || []),
['dateBegin'],
...((!isMsb() && [['dateEnd']]) || []),
...((!isMsb() && [['period']]) || []),
['period'],
['amount'],
/* ['currency'], */
['dealRateValue'],
@@ -0,0 +1,64 @@
/* eslint-disable sonarjs/no-duplicate-string */
import { getUUID } from '../helper';
describe('getUUID function', () => {
it('should return "" when path is an empty string', () => {
const result = getUUID('');
expect(result).toBe('');
});
it('should return "" when path is undefined', () => {
const result = getUUID(undefined as unknown as string);
expect(result).toBe('');
});
it('should return "" when path does not contain a valid UUID', () => {
const result = getUUID('/some/random/path/withoutuuid');
expect(result).toBe('');
});
it('should return "" when UUID is not properly formatted', () => {
const result = getUUID('/some/path/with/invalid-uuid-format');
expect(result).toBe('');
});
it('should return the UUID when the path contains a valid UUID at the end', () => {
const result = getUUID('/users/123e4567-e89b-12d3-a456-426614174000');
expect(result).toBe('123e4567-e89b-12d3-a456-426614174000');
});
it('should return "" if the UUID is malformed (e.g., missing hyphens)', () => {
const result = getUUID('/path/without-hyphens/123e4567e89b12d3a456426614174000');
expect(result).toBe('');
});
it('should return "" if the UUID is in uppercase format but still valid', () => {
const result = getUUID('/path/with/uppercase/UUID/123E4567-E89B-12D3-A456-426614174000');
expect(result).toBe('123E4567-E89B-12D3-A456-426614174000');
});
it('should return "" for a path with an invalid UUID and extra parts', () => {
const result = getUUID('/some/path/with/invalid-uuid-format/extra/parts');
expect(result).toBe('');
});
it('should return "" for a path that includes slashes but no UUID', () => {
const result = getUUID('/some/random/path/');
expect(result).toBe('');
});
it('should return the UUID if the path is just a single UUID', () => {
const result = getUUID('123e4567-e89b-12d3-a456-426614174000');
expect(result).toBe('123e4567-e89b-12d3-a456-426614174000');
});
});