diff --git a/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx b/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx index 5ce9cb8..3547673 100644 --- a/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx +++ b/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx @@ -14,10 +14,10 @@ const Grid = styled(Flex)` const Validation = observer(() => { const store = useStore(); - const messages = store.$tables.fingap.validation.getMessages(); + const errors = store.$tables.fingap.validation.getErrors(); - if (messages?.length) { - return ; + if (errors?.length) { + return ; } return null; diff --git a/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx b/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx index 6cc6fde..24ef8af 100644 --- a/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx +++ b/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx @@ -19,10 +19,10 @@ const TableWrapper = styled.div` const Validation = observer(() => { const store = useStore(); - const messages = store.$tables.insurance.validation.getMessages(); + const errors = store.$tables.insurance.validation.getErrors(); - if (messages?.length) { - return ; + if (errors?.length) { + return ; } return null; diff --git a/apps/web/config/process/default.ts b/apps/web/config/process/default.ts index f81c6b9..4cb4fe8 100644 --- a/apps/web/config/process/default.ts +++ b/apps/web/config/process/default.ts @@ -3,7 +3,7 @@ import * as bonuses from '@/process/bonuses'; // import * as calculate from '@/process/calculate'; import * as configurator from '@/process/configurator'; // import * as createKP from '@/process/create-kp'; -// import * as fingap from '@/process/fingap'; +import * as fingap from '@/process/fingap'; // import * as gibdd from '@/process/gibdd'; import { useProcess } from '@/process/hooks'; // import * as insurance from '@/process/insurance'; @@ -11,7 +11,7 @@ import { useProcess } from '@/process/hooks'; // import * as leasingObject from '@/process/leasing-object'; // import * as leasingWithoutKasko from '@/process/leasing-without-kasko'; // import * as loadKP from '@/process/load-kp'; -// import * as payments from '@/process/payments'; +import * as payments from '@/process/payments'; // import * as price from '@/process/price'; // import * as subsidy from '@/process/subsidy'; // import * as subsidyImportProgram from '@/process/subsidy-import-program'; @@ -24,7 +24,7 @@ export default function useReactions() { // useProcess(calculate); // useProcess(supplierAgent); // useProcess(price); - // useProcess(fingap); + useProcess(fingap); // useProcess(leasingWithoutKasko); // useProcess(subsidy); // useProcess(leasingObject); @@ -33,7 +33,7 @@ export default function useReactions() { useProcess(bonuses); // useProcess(usedPl); // useProcess(subsidyImportProgram); - // useProcess(payments); + useProcess(payments); // useProcess(gibdd); // useProcess(addProduct); // useProcess(insurance); diff --git a/apps/web/process/fingap/reactions/validation.ts b/apps/web/process/fingap/reactions/validation.ts index 3f8df5f..580208e 100644 --- a/apps/web/process/fingap/reactions/validation.ts +++ b/apps/web/process/fingap/reactions/validation.ts @@ -1,9 +1,14 @@ import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; +import { uid } from 'radash'; + +const key = uid(7); export default function reactions({ store }: ProcessContext) { const { $tables } = store; + const helper = new ValidationHelper(); reaction( () => { const hasPaymentsErrors = $tables.payments.validation.hasErrors; @@ -15,10 +20,15 @@ export default function reactions({ store }: ProcessContext) { }; }, ({ hasPaymentsErrors, finGAPInsuranceCompany }) => { - $tables.fingap.validate({ - invalid: finGAPInsuranceCompany !== null && hasPaymentsErrors, - message: 'Неверно заполнены платежи', - }); + if (finGAPInsuranceCompany !== null && hasPaymentsErrors) { + const removeError = $tables.fingap.setError({ + key, + message: 'Неверно заполнены платежи', + }); + helper.add(removeError); + } else { + helper.removeErrors(); + } if (hasPaymentsErrors) { $tables.fingap.clear(); diff --git a/apps/web/process/payments/lib/validation.ts b/apps/web/process/payments/lib/validation.ts deleted file mode 100644 index ae38795..0000000 --- a/apps/web/process/payments/lib/validation.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { SEASONS_PERIOD_NUMBER, SEASONS_PERIODS } from './seasons-constants'; -import { MIN_PAYMENT } from '@/constants/values'; -import type RootStore from '@/stores/root'; -import { counting, max, min, shift, sort } from 'radash'; -import { areEqual, isSorted } from 'tools/array'; - -// eslint-disable-next-line sonarjs/cognitive-complexity -export default function validatePaymentsTable({ $calculation, $tables }: RootStore) { - /** - * в таблице платежей в столбце Соотношение платежей - * для строк с 2 до "Срок лизинга-1" минимальное значение должно быть равно 3 - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1); - - if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) { - return `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`; - } - } - - switch ($calculation.element('radioGraphType').getValue()) { - // Дегрессия - case 100_000_001: { - if (!$calculation.element('selectSeasonType').getValue()) { - return 'Не выбран тип дегрессии'; - } - - /** - * при Дегрессии все значения не должны быть равны друг другу - * + что при Легком старте 2,3 и 4 платежи не должны быть равны 100 - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1); - - if (new Set(targetPayments).size === 1) { - return 'Платежи не должны быть равны друг другу'; - } - } - - /** - * Проверка на возрастание - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - 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) || 0) - (min(targetPayments) || 0) > 10) { - return 'Указана очень жесткая дегрессия. На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%'; - } - } - - /** - * Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны - * (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза), - * то осуществлять расчет, - * иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой", - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - 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.element('tbxLeasingPeriod').getValue(); - if (leasingPeriod < 14) { - return 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев'; - } - - const seasonType = $calculation.element('selectSeasonType').getValue(); - if (!seasonType) { - return 'Не выбран тип сезонности'; - } - - const highSeasonStartOption = $calculation.element('selectHighSeasonStart').getOption(); - 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; -} diff --git a/apps/web/process/payments/reactions/validation.ts b/apps/web/process/payments/reactions/validation.ts index 7d6eefb..9a3fdc8 100644 --- a/apps/web/process/payments/reactions/validation.ts +++ b/apps/web/process/payments/reactions/validation.ts @@ -1,40 +1,50 @@ -import validatePaymentsTable from '../lib/validation'; -import { MIN_LASTPAYMENT_NSIB } from '@/constants/values'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; import ValidationHelper from '@/stores/validation/helper'; import { comparer, reaction, toJS } from 'mobx'; +import { uid } from 'radash'; -export default function reactions({ store }: ProcessContext) { - const { $calculation, $tables } = store; +const key = uid(7); - const validationHelper = new ValidationHelper(); +export default function reactions(context: ProcessContext) { + const { $calculation, $tables } = context.store; + + const validationSchema = createValidationSchema(); + const helper = new ValidationHelper(); reaction( () => { const payments = toJS($tables.payments.values); - const graphType = $calculation.element('radioGraphType').getValue(); - const seasonType = $calculation.element('selectSeasonType').getValue(); - const highSeasonStart = $calculation.element('selectHighSeasonStart').getValue(); - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); + const values = $calculation.$values.getValues([ + 'graphType', + 'highSeasonStart', + 'leasingPeriod', + 'seasonType', + 'insNSIB', + 'lastPaymentRub', + ]); return { - graphType, - highSeasonStart, - leasingPeriod, - payments, - seasonType, + payments: { values: payments }, + ...values, }; }, - () => { - validationHelper.removeErrors(); + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - const errorText = validatePaymentsTable(store); - - if (errorText) { - $tables.payments.validate({ - helper: validationHelper, - invalid: errorText !== null, - message: errorText, + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Array).forEach((elementName) => { + if (elementName === 'payments') { + const removeError = $tables.payments.setError({ key, message }); + if (removeError) helper.add(removeError); + } else { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + } + }); }); } }, @@ -43,22 +53,4 @@ export default function reactions({ store }: ProcessContext) { equals: comparer.structural, } ); - - reaction( - () => { - const lastPaymentRub = $calculation.element('tbxLastPaymentRub').getValue(); - const insNSIB = $calculation.element('selectInsNSIB').getValue(); - - return { - insNSIB, - lastPaymentRub, - }; - }, - ({ lastPaymentRub, insNSIB }) => { - $calculation.element('tbxLastPaymentRub').validate({ - invalid: Boolean(insNSIB) && lastPaymentRub < MIN_LASTPAYMENT_NSIB, - message: `Последний платеж меньше ${MIN_LASTPAYMENT_NSIB} руб. не может быть при наличии НСИБ, укажите большее значение`, - }); - } - ); } diff --git a/apps/web/process/payments/validation.ts b/apps/web/process/payments/validation.ts new file mode 100644 index 0000000..5a5fc1f --- /dev/null +++ b/apps/web/process/payments/validation.ts @@ -0,0 +1,215 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable complexity */ +import { SEASONS_PERIOD_NUMBER, SEASONS_PERIODS } from './lib/seasons-constants'; +import { selectHighSeasonStart } from '@/config/default-options'; +import PaymentsSchema from '@/config/schema/payments'; +import ValuesSchema from '@/config/schema/values'; +import { MIN_LASTPAYMENT_NSIB, MIN_PAYMENT } from '@/constants/values'; +import { counting, max, min, shift, sort } from 'radash'; +import { areEqual, isSorted } from 'tools/array'; +import { z } from 'zod'; + +export function createValidationSchema() { + return ValuesSchema.pick({ + graphType: true, + highSeasonStart: true, + insNSIB: true, + lastPaymentRub: true, + leasingPeriod: true, + seasonType: true, + }) + .extend({ + payments: PaymentsSchema, + }) + .superRefine( + async ( + { + graphType, + highSeasonStart, + leasingPeriod, + payments, + seasonType, + insNSIB, + lastPaymentRub, + }, + ctx + ) => { + if (Boolean(insNSIB) && lastPaymentRub < MIN_LASTPAYMENT_NSIB) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Последний платеж не может быть меньше ${MIN_LASTPAYMENT_NSIB} руб. при наличии НСИБ, укажите большее значение`, + path: ['tbxLastPaymentRub'], + }); + } + + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + + if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`, + path: ['payments'], + }); + } + } + + switch (graphType) { + // Дегрессия + case 100_000_001: { + if (!seasonType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбран тип дегрессии', + path: ['payments', 'selectSeasonType'], + }); + break; + } + + /** + * при Дегрессии все значения не должны быть равны друг другу + * + что при Легком старте 2,3 и 4 платежи не должны быть равны 100 + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + if (new Set(targetPayments).size === 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Платежи не должны быть равны друг другу', + path: ['payments'], + }); + } + } + + /** + * Проверка на возрастание + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + for (let i = 2; i < targetPayments.length - 1; i += 1) { + if (targetPayments[i] > targetPayments[i - 1]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Платежи должны убывать', + path: ['payments'], + }); + } + } + } + + /** + * Если вид графика = Дегрессия И значения в "Соотношении платежей" у 2, 3 и 4 платежа отличаются друг от друга не более чем на 10 + * (т.е. берем значения в этих полях, определяем максимальное и минимальное значение и смотрим на их разницу) + * то не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. + * На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%", + * иначе осуществлять расчет + */ + { + const targetPayments = payments.values.slice(1, 4); + + if ((max(targetPayments) || 0) - (min(targetPayments) || 0) > 10) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Указана очень жесткая дегрессия. На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%', + path: ['payments'], + }); + } + } + + /** + * Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны + * (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза), + * то осуществлять расчет, + * иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой", + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + const counts = counting(targetPayments, (v) => v); + if (Object.values(counts).filter((count) => count > 1).length < 2) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Указана очень жесткая дегрессия. Не менее чем у 4х платежей соотношение должно не отличаться между собой', + path: ['payments'], + }); + } + } + + break; + } + + case 100_000_003: { + if (leasingPeriod < 14) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев', + path: ['payments', 'tbxLeasingPeriod'], + }); + } + + if (!seasonType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбран тип сезонности', + path: ['payments', 'selectSeasonType'], + }); + break; + } + + if (!highSeasonStart) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбрано смещение сезонности', + path: ['payments', 'selectHighSeasonStart'], + }); + break; + } + + const seasons = payments.values.slice(1, SEASONS_PERIOD_NUMBER + 1); + const highSeasonStartOption = selectHighSeasonStart.find( + (x) => x.value === highSeasonStart + ); + + if (highSeasonStartOption) { + 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)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Сезонные платежи должны убывать', + path: ['payments'], + }); + } + } + + break; + } + + // Легкий старт + case 100_000_004: { + const targetPayments = payments.values.slice(1, 4); + const sortedPayments = sort(targetPayments, (x) => x); + const areEqualPayments = new Set(targetPayments).size === 1; + + if (!areEqual(targetPayments, sortedPayments) || areEqualPayments) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '2, 3, 4 платежи должны возрастать', + path: ['payments'], + }); + } + + break; + } + default: { + break; + } + } + } + ); +} diff --git a/apps/web/stores/tables/fingap/index.ts b/apps/web/stores/tables/fingap/index.ts index 9df4da9..82d5f6d 100644 --- a/apps/web/stores/tables/fingap/index.ts +++ b/apps/web/stores/tables/fingap/index.ts @@ -1,5 +1,5 @@ import Validation from '../../validation'; -import type { RemoveError, ValidationParams } from '../../validation/types'; +import type { ValidationParams } from '../../validation/types'; import type * as FinGAP from '@/Components/Calculation/Form/Insurance/FinGAPTable/types'; import type RootStore from '@/stores/root'; import type { IObservableArray } from 'mobx'; @@ -38,17 +38,10 @@ export default class FinGAPTable { .reduce((sum, risk) => sum + risk.premium, 0); } - public validate = ({ invalid, message, helper }: ValidationParams) => { - let removeError: RemoveError | undefined; + public setError = (params: ValidationParams) => this.validation.setError(params); - if (invalid) { - removeError = this.validation?.addError(message); - if (helper && removeError) helper.add(removeError); - } else { - this.validation?.removeError(message); - } - - return removeError; + public removeError = (params: Pick) => { + this.validation.removeError(params); }; public clear = () => { diff --git a/apps/web/stores/tables/payments/index.ts b/apps/web/stores/tables/payments/index.ts index cbe63eb..ce9b135 100644 --- a/apps/web/stores/tables/payments/index.ts +++ b/apps/web/stores/tables/payments/index.ts @@ -1,5 +1,5 @@ import Validation from '../../validation'; -import type { RemoveError, ValidationParams } from '../../validation/types'; +import type { ValidationParams } from '../../validation/types'; import type { Row } from './types'; import type RootStore from '@/stores/root'; import type { IObservableArray } from 'mobx'; @@ -72,17 +72,10 @@ export default class PaymentsTable { this.setStatuses(statuses); }; - public validate = ({ invalid, message, helper }: ValidationParams) => { - let removeError: RemoveError | undefined; + public setError = (params: ValidationParams) => this.validation.setError(params); - if (invalid) { - removeError = this.validation?.addError(message); - if (helper && removeError) helper.add(removeError); - } else { - this.validation?.removeError(message); - } - - return removeError; + public removeError = (params: Pick) => { + this.validation.removeError(params); }; public reset = () => {