Compare commits

...

27 Commits

Author SHA1 Message Date
Chika
cf7189123e add validation for seasons payments 2022-10-17 15:48:02 +03:00
Chika
e0de887c45 process/payments/validation: pass root store 2022-10-17 15:07:47 +03:00
Chika
9b9e27bd74 process/payments: trigger validation on leasingPeriod 2022-10-14 15:32:03 +03:00
Chika
4749c2167f process/payments/validation: При сезонном виде графика срок лизинга должен быть больше 14 месяцев 2022-10-14 15:10:56 +03:00
Chika
de26e39eff process/payments: extract function generateSeasonPaymentsRows 2022-10-14 14:00:03 +03:00
Chika
600bb1a730 fix ca8d060 2022-10-14 13:55:17 +03:00
Chika
073dd1ac01 process/payments/degression: remove reset selectSeasonType value when change options 2022-10-14 13:39:29 +03:00
Chika
310736f050 process/payments/seasons: remove reset selectHighSeasonStart value when change options 2022-10-14 13:38:52 +03:00
Chika
333fcbd8b4 fix prev commit 2022-10-14 13:29:32 +03:00
Chika
ca8d060851 process/payments/seasons: disable all except periods start 2022-10-14 13:24:59 +03:00
Chika
9cac288dcc process/payments: refactor /lib files structure 2022-10-14 13:15:48 +03:00
Chika
4acbcde1cc process/payments: add highSeasonStart filtering 2022-10-14 13:11:00 +03:00
Chika
5a25b342f6 process/payments: fix seasons payments 2022-10-14 12:59:16 +03:00
Chika
ab89ca3475 process/payments: fix payments shifting 2022-10-13 14:37:10 +03:00
Chika
3cb0a6a73c tables/payments: Сезонные 2022-10-13 14:04:23 +03:00
Chika
2d2d5d6cc3 process/payments: degression auto fill payments 2022-10-06 13:05:55 +03:00
Chika
adf186d8da process/payments: prepare for season graphtype 2022-10-05 17:01:11 +03:00
Chika
6107e0c15f process/payments: add validation 2022-10-05 16:41:21 +03:00
Chika
8f47cc44f8 process/payments(degression): set default seasonType value 2022-10-05 11:44:44 +03:00
Chika
c8afffe5b2 process/payments(degression): fix reaction error 2022-10-04 20:11:26 +03:00
Chika
2b6960de45 tables/payments: Дегрессия 2022-10-04 19:59:29 +03:00
Chika
326257cd7c Components/Payments: dynamic UI Elements 2022-10-04 17:45:52 +03:00
Chika
375e59fb6a add for 9bedfe8 2022-10-04 15:35:11 +03:00
Chika
9bedfe8fab tables/payments: Легкий старт 2022-10-03 19:51:23 +03:00
Chika
7c40e2cf4f fingap/validation: fix removeError is undefined 2022-10-03 16:24:38 +03:00
Chika
048da7947a tables/payments: Равноубывающий 2022-10-03 14:34:01 +03:00
Chika
eb7dc997e4 tables/payments: аннуитет 2022-10-03 14:03:19 +03:00
19 changed files with 977 additions and 92 deletions

View File

@ -0,0 +1,45 @@
import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
import { Flex } from 'UIKit/grid';
import elementsRender from '../../config/elements-render';
import { elements } from './config';
function PaymentsParams() {
const renderedElements = elements.map((elementName) => {
const render = elementsRender[elementName]?.render;
return render();
});
const [selectSeasonType, tbxParmentsDecreasePercent, selectHighSeasonStart] = renderedElements;
const { $calculation } = useStore();
const graphType = $calculation.getElementValue('radioGraphType');
switch (graphType) {
case 100_000_000:
return null;
case 100_000_001: {
return <Flex flexDirection="column">{selectSeasonType}</Flex>;
}
case 100_000_002: {
return <Flex flexDirection="column">{tbxParmentsDecreasePercent}</Flex>;
}
case 100_000_003: {
return (
<Flex flexDirection="column">
{selectSeasonType}
{selectHighSeasonStart}
</Flex>
);
}
case 100_000_004: {
return null;
}
default:
break;
}
}
export default observer(PaymentsParams);

View File

@ -4,7 +4,6 @@ export const id = 'payments';
export const title = 'Платежи';
export const elements: ElementsRow['0'] = [
'radioGraphType',
'selectSeasonType',
'tbxParmentsDecreasePercent',
'selectHighSeasonStart',

View File

@ -1,18 +1,11 @@
import { Box, Flex } from 'UIKit/grid';
import elementsRender from '../../config/elements-render';
import { elements, id, title } from './config';
import { id, title } from './config';
import PaymentsParams from './PaymentsParams';
import PaymentsTable from './PaymentsTable';
function Payments() {
const renderedElements = elements.map((elementName) => {
const render = elementsRender[elementName]?.render;
return render();
});
// eslint-disable-next-line operator-linebreak
const [radioGraphType, selectSeasonType, tbxParmentsDecreasePercent, selectHighSeasonStart] =
renderedElements;
const radioGraphType = elementsRender.radioGraphType.render();
return (
<Flex flexDirection="column">
@ -24,11 +17,7 @@ function Payments() {
}}
>
{radioGraphType}
<Flex flexDirection="column">
{selectSeasonType}
{tbxParmentsDecreasePercent}
{selectHighSeasonStart}
</Flex>
<PaymentsParams />
</Box>
<PaymentsTable />
</Flex>

View File

@ -2,7 +2,9 @@
import { Container, Head } from 'Components/Layout/Element';
import Link from 'Elements/Link';
import Tooltip from 'Elements/Tooltip';
import { observer } from 'mobx-react-lite';
import type { ComponentProps } from 'react';
import { useStore } from 'stores/hooks';
import buildReadonly from '../../builders/build-readonly';
import builders from '../elements-builders';
import components from '../elements-components';
@ -162,6 +164,42 @@ const overrideRender: Partial<Record<keyof typeof map, RenderProps>> = {
);
},
},
selectSeasonType: {
render: () => {
const elementName = 'selectSeasonType';
const valueName = map.selectSeasonType;
const Component = components.selectSeasonType;
const props = elementsProps.selectSeasonType;
const builder = builders.selectSeasonType;
const Element = builder(Component, {
elementName,
valueName,
});
const Title = observer(() => {
const { $calculation } = useStore();
const graphType = $calculation.getElementValue('radioGraphType');
switch (graphType) {
case 100_000_001:
return <span>Тип дегрессии</span>;
case 100_000_003:
return <span>Тип сезонности</span>;
default:
return <span>{titles.selectSeasonType}</span>;
}
});
return (
<Container>
<Head htmlFor={elementName} title={<Title />} />
<Element {...props} id={elementName} />
</Container>
);
},
},
};
export default overrideRender;

View File

@ -23,7 +23,7 @@ export function Head({
addon,
htmlFor,
}: {
title: string;
title: ReactNode;
addon?: ReactNode;
htmlFor: string;
}) {

View File

@ -1,6 +1,51 @@
import { alphabetical } from 'radash/dist/array';
import type { CalculationOptions } from 'stores/calculation/options/types';
export const selectSeasonType = [
{
label: '6/6',
value: 100_000_000,
},
{
label: '8/4',
value: 100_000_001,
},
{
label: '4/4/4',
value: 100_000_002,
},
{
label: 'Гибкая дегрессия',
value: 100_000_007,
},
{
label: '100.50.25',
value: 100_000_003,
},
{
label: '100.30.10',
value: 100_000_004,
},
{
label: '100.70.40',
value: 100_000_005,
},
{
label: '100.7.3',
value: 100_000_006,
},
];
export const selectHighSeasonStart = Array.from(
{
length: 12,
},
(_, i) => ({
label: `${i + 2}`,
value: 100_000_000 + i,
})
);
const defaultOptions: CalculationOptions = {
radioLastPaymentRule: [
{
@ -51,50 +96,9 @@ const defaultOptions: CalculationOptions = {
},
],
selectSeasonType: [
{
label: '6/6',
value: 100_000_000,
},
{
label: '8/4',
value: 100_000_001,
},
{
label: '4/4/4',
value: 100_000_002,
},
{
label: 'Гибкая дегрессия',
value: 100_000_007,
},
{
label: '100.50.25',
value: 100_000_003,
},
{
label: '100.30.10',
value: 100_000_004,
},
{
label: '100.70.40',
value: 100_000_005,
},
{
label: '100.7.3',
value: 100_000_006,
},
],
selectSeasonType,
selectHighSeasonStart: Array.from(
{
length: 12,
},
(_, i) => ({
label: `${i + 2}`,
value: 100_000_000 + i,
})
),
selectHighSeasonStart,
radioDeliveryTime: [
{

View File

@ -23,8 +23,8 @@ const defaultValues: CalculationValues = {
balanceHolder: 100_000_001,
graphType: 100_000_000,
parmentsDecreasePercent: 94,
seasonType: 100_000_000,
highSeasonStart: 100_000_000,
seasonType: null,
highSeasonStart: null,
comissionPerc: 0,
comissionRub: 0,
saleBonus: 1.3,

View File

@ -26,9 +26,7 @@ export default function validationReactions(
({ hasPaymentsErrors, finGAPInsuranceCompany }) => {
if (finGAPInsuranceCompany && hasPaymentsErrors) {
removeError = $tables.fingap.validation.addError(errorText);
} else {
removeError();
}
} else if (removeError) removeError();
},
{
fireImmediately: true,

View File

@ -0,0 +1,25 @@
import { getPositionIndex } from '../seasons-tools';
describe('process/payments/lib/seasons-tools', () => {
describe('[function] getPositionIndex', () => {
test('should return 0', () => {
const result = getPositionIndex(100_000_001, 0);
expect(result).toEqual(0);
});
test('should return 0', () => {
const result = getPositionIndex(100_000_001, 7);
expect(result).toEqual(0);
});
test('should return 1', () => {
const result = getPositionIndex(100_000_001, 8);
expect(result).toEqual(1);
});
test('should return 1', () => {
const result = getPositionIndex(100_000_001, 11);
expect(result).toEqual(1);
});
});
});

View File

@ -0,0 +1,15 @@
export const SEASONS_PERIODS: Record<number, Array<number>> = {
100_000_000: [0, 6],
100_000_001: [0, 8],
100_000_002: [0, 4, 8],
};
export const FORBIDDEN_HIGH_SEASON_START: Record<number, Array<string>> = {
100_000_000: ['9', '10'],
100_000_001: ['7', '8', '9'],
100_000_002: ['11'],
};
export const SEASONS_PERIOD_NUMBER = 12;
export const DEFAULT_SEASONS_VALUES = [100, 75, 50];

View File

@ -0,0 +1,55 @@
/* eslint-disable implicit-arrow-linebreak */
import type { CalculationValues } from 'stores/calculation/values/types';
import { SEASONS_PERIODS, SEASONS_PERIOD_NUMBER } from './seasons-constants';
type SeasonType = NonNullable<CalculationValues['seasonType']>;
type LeasingPeriod = CalculationValues['leasingPeriod'];
export function getPositionIndex(seasonType: SeasonType, seasonsIndex: number) {
let positionIndex = 0;
SEASONS_PERIODS[seasonType].forEach((position, i) => {
if (seasonsIndex >= position) {
positionIndex = i;
}
});
return positionIndex;
}
export function getSeasonsValues(seasonType: SeasonType, seasons: Array<number>) {
return SEASONS_PERIODS[seasonType].map((index) => seasons[index]);
}
export function generateSeasons(seasonType: SeasonType, values: Array<number>) {
const positions = SEASONS_PERIODS[seasonType];
const seasons: Array<number> = [];
[...positions, SEASONS_PERIOD_NUMBER].forEach((position, i, arr) => {
if (position < SEASONS_PERIOD_NUMBER) {
const start = arr[i];
const end = arr[i + 1];
const value = values[i];
// eslint-disable-next-line unicorn/new-for-builtins
seasons.push(...Array(end - start).fill(value));
}
});
return seasons;
}
export function generateSeasonsPayments(leasingPeriod: LeasingPeriod, seasons: Array<number>) {
return (
Array.from(
{
length: Math.floor((leasingPeriod - 2) / SEASONS_PERIOD_NUMBER),
},
() => seasons
)
.flat()
// eslint-disable-next-line unicorn/prefer-spread
.concat(seasons.slice(0, (leasingPeriod - 2) % SEASONS_PERIOD_NUMBER))
);
}

View File

@ -1,25 +1,78 @@
/* eslint-disable function-paren-newline */
/* eslint-disable implicit-arrow-linebreak */
import type { ApolloClient } from '@apollo/client';
import { reaction, toJS } from 'mobx';
import type { QueryClient } from '@tanstack/react-query';
import { selectHighSeasonStart, selectSeasonType } from 'config/default-options';
import { comparer, reaction, toJS } from 'mobx';
import type { CalculationOptions } from 'stores/calculation/options/types';
import type RootStore from 'stores/root';
import type { Row } from 'stores/tables/payments/types';
import { difference, shift } from 'tools/array';
import * as seasonsConstants from './lib/seasons-constants';
import * as seasonsTools from './lib/seasons-tools';
import validatePaymentsTable from './validation';
export default function paymentsReactions(store: RootStore, apolloClient: ApolloClient<object>) {
export default function paymentsReactions(
store: RootStore,
apolloClient: ApolloClient<object>,
queryClient: QueryClient
) {
const { $calculation, $tables } = store;
reaction(
() => $calculation.getElementValue('tbxFirstPaymentPerc'),
(firstPaymentPerc) => {
$tables.payments.setValue(0, firstPaymentPerc);
}
);
reaction(
() => $calculation.getElementValue('tbxLastPaymentPerc'),
(lastPaymentPerc) => {
const paymentsLength = $tables.payments.values.length;
$tables.payments.setValue(paymentsLength - 1, lastPaymentPerc);
}
);
/**
* При изменении срока лизинга регулируем длину таблицы платежей
* Аннуитет
*/
reaction(
() => $calculation.$values.getValue('leasingPeriod'),
(leasingPeriod) => {
if (leasingPeriod) {
$tables.payments.setValues(
Array.from(
{
length: leasingPeriod,
},
() => 0
)
() => {
const graphType = $calculation.getElementValue('radioGraphType');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
return {
graphType,
leasingPeriod,
};
},
({ graphType, leasingPeriod }) => {
if (graphType === 100_000_000) {
const middlePayments: Row[] = Array.from(
{
length: leasingPeriod - 2,
},
() => ({
value: 100,
status: 'Disabled',
})
);
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...middlePayments,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
}
},
{
@ -28,22 +81,479 @@ export default function paymentsReactions(store: RootStore, apolloClient: Apollo
);
/**
* Проверяем платежи на 0
* Равноубывающий
*/
reaction(
() => {
const graphType = $calculation.getElementValue('radioGraphType');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const parmentsDecreasePercent = $calculation.getElementValue('tbxParmentsDecreasePercent');
return {
graphType,
leasingPeriod,
parmentsDecreasePercent,
};
},
({ graphType, leasingPeriod, parmentsDecreasePercent }) => {
if (graphType === 100_000_002) {
const middlePayments: Row[] = Array.from(
{
length: leasingPeriod - 2,
},
(_, k) => {
const payment = 100 * (parmentsDecreasePercent / 100) ** k;
return {
value: Number(payment.toFixed(2)),
status: 'Disabled',
};
}
);
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...middlePayments,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
}
}
);
/**
* Легкий старт
*/
reaction(
() => {
const graphType = $calculation.getElementValue('radioGraphType');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
return {
graphType,
leasingPeriod,
};
},
({ graphType, leasingPeriod }) => {
if (graphType === 100_000_004) {
const editablePayments: Row[] = [
{
value: 25,
status: 'Default',
},
{
value: 50,
status: 'Default',
},
{
value: 75,
status: 'Default',
},
];
const payments: Row[] = Array.from(
{
length: leasingPeriod - 5,
},
() => ({
value: 100,
status: 'Disabled',
})
);
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...editablePayments,
...payments,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
}
}
);
/**
* Дегрессия
*/
// TODO: const exeption = tarif?.evo_seasons_type_exception
const degressionSeasonTypes = new Set([
100_000_003, 100_000_004, 100_000_005, 100_000_006, 100_000_007,
]);
const seasonSeasonTypes = new Set([100_000_000, 100_000_001, 100_000_002]);
reaction(
() => {
const graphType = $calculation.getElementValue('radioGraphType');
return graphType;
},
(graphType) => {
let selectSeasonTypeOptions: CalculationOptions['selectSeasonType'] = [];
if (graphType === 100_000_001) {
selectSeasonTypeOptions = selectSeasonType.filter((option) =>
degressionSeasonTypes.has(option.value)
);
} else if (graphType === 100_000_003) {
selectSeasonTypeOptions = selectSeasonType.filter((option) =>
seasonSeasonTypes.has(option.value)
);
}
$calculation.setElementOptions('selectSeasonType', selectSeasonTypeOptions);
}
);
const degressionSteps: { [key: number]: Array<number> } = {
100_000_003: [100, 50, 25],
100_000_004: [100, 30, 10],
100_000_005: [100, 70, 40],
100_000_006: [100, 7, 3],
};
reaction(
() => {
const degressionType = $calculation.getElementValue('selectSeasonType');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const graphType = $calculation.getElementValue('radioGraphType');
return {
degressionType,
leasingPeriod,
graphType,
};
},
({ degressionType, leasingPeriod, graphType }) => {
if (graphType === 100_000_001) {
let payments: Row[] = [];
switch (degressionType) {
case 100_000_007: {
const editablePayments: Row[] = Array.from(
{
length: leasingPeriod - 3,
},
() => ({
value: 100,
status: 'Default',
})
);
payments = [
{
value: 100,
status: 'Disabled',
},
...editablePayments,
];
break;
}
case 100_000_003:
case 100_000_004:
case 100_000_005:
case 100_000_006: {
const [step1, step2, step3] = degressionSteps[degressionType];
const paymentsInStep = Math.ceil((leasingPeriod - 2) / 3);
payments = Array.from(
{
length: leasingPeriod - 2,
},
(_v, i) => {
let value = step3;
if (i <= paymentsInStep * 2 - 1) {
value = step2;
}
if (i <= paymentsInStep - 1) {
value = step1;
}
return {
value,
status: 'Disabled',
};
}
);
break;
}
default: {
break;
}
}
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...payments,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
}
}
);
reaction(
() => {
const graphType = $calculation.getElementValue('radioGraphType');
const payments = toJS($tables.payments.values);
const degressionType = $calculation.getElementValue('selectSeasonType');
return {
graphType,
payments,
degressionType,
};
},
(nextParams, prevParams) => {
if (nextParams.graphType === 100_000_001 && nextParams.degressionType === 100_000_007) {
const changes = difference(nextParams.payments, prevParams.payments);
if (changes === null || changes.length > 1) return;
const [changeIndex] = changes;
const value = nextParams.payments[changeIndex];
const payments = nextParams.payments.slice(1, -1).map((payment, i) => {
if (i <= changeIndex - 2) return payment;
return value;
});
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setValues([firstPaymentPerc, ...payments, lastPaymentPerc]);
}
},
{
delay: 50,
equals: comparer.structural,
}
);
/**
* Сезонный
*/
reaction(
() => {
const graphType = $calculation.getElementValue('radioGraphType');
return {
graphType,
};
},
({ graphType }) => {
if (graphType !== 100_000_003) return;
const seasonType = $calculation.getElementValue('selectSeasonType');
const highSeasonStart = $calculation.getElementValue('selectHighSeasonStart');
if (!seasonType || !highSeasonStart) {
$tables.payments.setValues([]);
}
}
);
reaction(
() => {
const seasonType = $calculation.getElementValue('selectSeasonType');
return seasonType;
},
(seasonType) => {
const graphType = $calculation.getElementValue('radioGraphType');
if (graphType !== 100_000_003) return;
if (!seasonType) {
$calculation.resetElement('selectHighSeasonStart');
return;
}
const highSeasonStartOptions = selectHighSeasonStart.filter(
(option) => !seasonsConstants.FORBIDDEN_HIGH_SEASON_START[seasonType].includes(option.label)
);
$calculation.setElementOptions('selectHighSeasonStart', highSeasonStartOptions);
}
);
function generateSeasonPaymentsRows(seasonType: number, shiftNumber: number, payments: number[]) {
const shiftedPeriods = new Set(
seasonsConstants.SEASONS_PERIODS[seasonType].map(
(position) => (position + shiftNumber) % seasonsConstants.SEASONS_PERIOD_NUMBER
)
);
const rows: Row[] = payments.map((value, i) => ({
value,
status: shiftedPeriods.has(i) ? 'Default' : 'Disabled',
}));
return rows;
}
reaction(
() => {
const seasonType = $calculation.getElementValue('selectSeasonType');
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
return {
seasonType,
highSeasonStartOption,
leasingPeriod,
};
},
({ seasonType, highSeasonStartOption, leasingPeriod }) => {
const graphType = $calculation.getElementValue('radioGraphType');
if (graphType !== 100_000_003) return;
if (!seasonType || !highSeasonStartOption) {
$tables.payments.setValues([]);
return;
}
const seasons = seasonsTools.generateSeasons(
seasonType,
seasonsConstants.DEFAULT_SEASONS_VALUES
);
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
const payments = seasonsTools.generateSeasonsPayments(
leasingPeriod,
shift(seasons, shiftNumber)
);
const rows: Row[] = generateSeasonPaymentsRows(seasonType, shiftNumber, payments);
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...rows,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
}
);
reaction(
() => {
const payments = toJS($tables.payments.values);
const seasons = payments.slice(1, seasonsConstants.SEASONS_PERIOD_NUMBER + 1);
return seasons;
},
(nextSeasons, prevSeasons) => {
const graphType = $calculation.getElementValue('radioGraphType');
if (graphType !== 100_000_003) return;
const seasonType = $calculation.getElementValue('selectSeasonType');
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
if (!seasonType || !highSeasonStartOption) return;
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
const unshiftedNextSeasons = shift(nextSeasons, -shiftNumber);
const unshiftedPrevSeasons = shift(prevSeasons, -shiftNumber);
const changes = difference(unshiftedNextSeasons, unshiftedPrevSeasons);
if (changes === null || changes.length > 1) return;
const [changeIndex] = changes;
const positionIndex = seasonsTools.getPositionIndex(seasonType, changeIndex);
const values = seasonsTools.getSeasonsValues(seasonType, unshiftedNextSeasons);
values[positionIndex] = unshiftedNextSeasons[changeIndex];
const seasons = seasonsTools.generateSeasons(seasonType, values);
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const payments = seasonsTools.generateSeasonsPayments(
leasingPeriod,
shift(seasons, shiftNumber)
);
const rows: Row[] = generateSeasonPaymentsRows(seasonType, shiftNumber, payments);
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
$tables.payments.setRows([
{
value: firstPaymentPerc,
status: 'Disabled',
},
...rows,
{
value: lastPaymentPerc,
status: 'Disabled',
},
]);
},
{
delay: 50,
equals: comparer.structural,
}
);
/**
* Валидация
*/
const errorText = 'Значения должны быть больше 0';
let removeError: () => void;
reaction(
() => toJS($tables.payments.values),
(values) => {
if (values.includes(0)) {
() => {
const payments = toJS($tables.payments.values);
const graphType = $calculation.getElementValue('radioGraphType');
const seasonType = $calculation.getElementValue('selectSeasonType');
const highSeasonStart = $calculation.getElementValue('selectHighSeasonStart');
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
return {
payments,
graphType,
seasonType,
highSeasonStart,
leasingPeriod,
};
},
() => {
if (removeError) removeError();
const errorText = validatePaymentsTable(store);
if (errorText) {
removeError = $tables.payments.validation.addError(errorText);
} else {
removeError();
}
},
{
fireImmediately: true,
delay: 50,
equals: comparer.structural,
}
);
}

View File

@ -0,0 +1,134 @@
/* eslint-disable max-len */
/* eslint-disable no-case-declarations */
import { counting, max, min, sort } from 'radash/dist/array';
import type RootStore from 'stores/root';
import { areEqual, isSorted, shift } from 'tools/array';
import { SEASONS_PERIODS, SEASONS_PERIOD_NUMBER } from './lib/seasons-constants';
export default function validatePaymentsTable({ $calculation, $tables }: RootStore) {
switch ($calculation.getElementValue('radioGraphType')) {
// Дегрессия
case 100_000_001:
if (!$calculation.getElementValue('selectSeasonType')) {
return 'Не выбран тип дегрессии';
}
/**
* в таблице платежей в столбце Соотношение платежей для строк с 2 до "Срок лизинга-1" минимальное значение должно быть равно 3
*/
{
const MIN_PAYMENT = 3;
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) {
return `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`;
}
}
/**
* при Дегрессии все значения не должны быть равны друг другу + что при Легком старте 2,3 и 4 платежи не должны быть равны 100
*/
{
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
if (new Set(targetPayments).size === 1) {
return 'Платежи не должны быть равны друг другу';
}
}
/**
* Проверка на возрастание
*/
{
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
for (let i = 2; i < targetPayments.length - 1; i += 1) {
if (targetPayments[i] > targetPayments[i - 1]) {
return 'Платежи должны убывать';
}
}
}
/**
* Если вид графика = Дегрессия И значения в "Соотношении платежей" у 2, 3 и 4 платежа отличаются друг от друга не более чем на 10
* (т.е. берем значения в этих полях, определяем максимальное и минимальное значение и смотрим на их разницу)
* то не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия.
* На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%",
* иначе осуществлять расчет
*/
{
const targetPayments = $tables.payments.values.slice(1, 4);
if (max(targetPayments) - min(targetPayments) > 10) {
return 'Указана очень жесткая дегрессия';
}
}
/**
* Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны
* (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза),
* то осуществлять расчет,
* иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой",
*/
{
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
const counts = counting(targetPayments, (v) => v);
if (Object.values(counts).filter((count) => count > 1).length < 2) {
return 'Указана очень жесткая дегрессия. Не менее чем у 4х платежей соотношение должно не отличаться между собой';
}
}
break;
case 100_000_003: {
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
if (leasingPeriod < 14) {
return 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев';
}
const seasonType = $calculation.getElementValue('selectSeasonType');
if (!seasonType) {
return 'Не выбран тип сезонности';
}
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
if (!highSeasonStartOption) {
return 'Не выбрано смещение сезонности';
}
{
const seasons = $tables.payments.values.slice(1, SEASONS_PERIOD_NUMBER + 1);
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
const unshiftedSeasons = shift(seasons, -shiftNumber);
const positions = SEASONS_PERIODS[seasonType];
const seasonsValues = positions.map((position) => unshiftedSeasons[position]);
if (isSorted(seasonsValues)) {
return 'Сезонные платежи должны убывать';
}
}
break;
}
// Легкий старт
case 100_000_004: {
const targetPayments = $tables.payments.values.slice(1, 4);
const sortedPayments = sort(targetPayments, (x) => x);
const areEqualPayments = new Set(targetPayments).size === 1;
if (!areEqual(targetPayments, sortedPayments) || areEqualPayments) {
return '2, 3, 4 платежи должны возрастать';
}
break;
}
default:
return null;
}
return null;
}

View File

@ -34,7 +34,7 @@ export default class OptionsStore {
if (
// eslint-disable-next-line operator-linebreak
!this.options[elementName]?.length ||
this.options[elementName].some((x) => x.value === value)
!this.options[elementName].some((x) => x.value === value)
) {
this.root.$calculation.resetElementValue(elementName);
}

View File

@ -4,11 +4,7 @@ import { makeAutoObservable, observable, reaction } from 'mobx';
import type RootStore from 'stores/root';
import Validation from '../validation';
type Row = {
value: number;
status: Status;
};
import type { Row } from './types';
export default class PaymentsTable {
root: RootStore;

View File

@ -0,0 +1,6 @@
import type { Status } from 'Elements/types';
export type Row = {
value: number;
status: Status;
};

View File

@ -29,6 +29,7 @@ export default class Validation {
const removeError = () => {
this.messages.delete(message);
notification.close(this.params.err_key);
};
notification.error({

View File

@ -0,0 +1,39 @@
import { shift } from 'tools/array';
describe('tools/array', () => {
describe('[function] shift', () => {
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
test('should shift array right 3 positions', () => {
const result = shift(arr, 3);
expect(result).toEqual([7, 8, 9, 1, 2, 3, 4, 5, 6]);
});
test('should shift array left 3 positions', () => {
const result = shift(arr, -3);
expect(result).toEqual([4, 5, 6, 7, 8, 9, 1, 2, 3]);
});
test('should shift array right 6 positions', () => {
const result = shift(arr, 15);
expect(result).toEqual([4, 5, 6, 7, 8, 9, 1, 2, 3]);
});
test('should shift array left 6 positions', () => {
const result = shift(arr, -15);
expect(result).toEqual([7, 8, 9, 1, 2, 3, 4, 5, 6]);
});
test('should keep array as is', () => {
const result = shift(arr, 0);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
test('should keep array as is', () => {
const result = shift(arr, 9);
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
});
test('should return empty array', () => {
const result = shift([], 0);
expect(result).toEqual([]);
});
test('should return empty array', () => {
const result = shift([], 0);
expect(result).toEqual([]);
});
});
});

31
tools/array.ts Normal file
View File

@ -0,0 +1,31 @@
export function areEqual<T>(arr1: ReadonlyArray<T>, arr2: ReadonlyArray<T>) {
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
}
export function difference<T>(arr1: ReadonlyArray<T>, arr2: ReadonlyArray<T>) {
if (arr1.length !== arr2.length) return null;
const changes = [];
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < arr1.length; i += 1) {
if (arr1[i] !== arr2[i]) {
changes.push(i);
}
}
return changes;
}
export function shift<T>(arr: Array<T>, n: number) {
if (arr.length === 0) return arr;
const shiftNumber = n % arr.length;
if (shiftNumber === 0) return arr;
return [...arr.slice(-shiftNumber, arr.length), ...arr.slice(0, -shiftNumber)];
}
export function isSorted(arr: Array<number>) {
return arr.every((value, index, array) => !index || array[index - 1] <= value);
}