From 91899164c0bcec797f32048372499adee3d8e661 Mon Sep 17 00:00:00 2001 From: vchikalkin Date: Sun, 5 Mar 2023 13:47:39 +0300 Subject: [PATCH] validate saleBonus using zod --- apps/web/Components/Output/Validation.jsx | 18 +++-- apps/web/config/process/default.ts | 68 +++++++++---------- .../bonuses/{reactions => }/lib/helper.ts | 2 +- apps/web/process/bonuses/reactions/common.ts | 2 +- .../process/bonuses/reactions/validation.ts | 29 +++++--- apps/web/process/bonuses/validation.ts | 24 +++++++ .../process/calculate/reactions/validation.ts | 1 + apps/web/stores/calculation/index.ts | 27 +++----- .../stores/calculation/validation/hooks.js | 5 +- apps/web/stores/validation/index.ts | 34 ++++++---- apps/web/stores/validation/types.ts | 11 +-- 11 files changed, 126 insertions(+), 95 deletions(-) rename apps/web/process/bonuses/{reactions => }/lib/helper.ts (97%) create mode 100644 apps/web/process/bonuses/validation.ts diff --git a/apps/web/Components/Output/Validation.jsx b/apps/web/Components/Output/Validation.jsx index 6435e2b..c66cff2 100644 --- a/apps/web/Components/Output/Validation.jsx +++ b/apps/web/Components/Output/Validation.jsx @@ -25,12 +25,12 @@ const AlertWrapper = styled(Box)` function getElementsErrors($calculation) { return Object.values($calculation.$validation).map((validation) => { - const elementErrors = validation.getMessages(); + const elementErrors = validation.getErrors(); const elementTitle = validation.params.err_title; - return elementErrors.map((error) => ( + return elementErrors.map(({ key, message }) => ( - + )); }); @@ -38,18 +38,22 @@ function getElementsErrors($calculation) { function getPaymentsTableErrors($tables) { const { payments } = $tables; - const messages = payments.validation.getMessages(); + const errors = payments.validation.getErrors(); const title = payments.validation.params.err_title; - return messages.map((text) => ); + return errors.map(({ key, message }) => ( + + )); } function getInsuranceTableErrors($tables) { const { insurance } = $tables; - const messages = insurance.validation.getMessages(); + const errors = insurance.validation.getErrors(); const title = insurance.validation.params.err_title; - return messages.map((text) => ); + return errors.map(({ key, message }) => ( + + )); } const Errors = observer(() => { diff --git a/apps/web/config/process/default.ts b/apps/web/config/process/default.ts index 25c9010..37b6f8e 100644 --- a/apps/web/config/process/default.ts +++ b/apps/web/config/process/default.ts @@ -1,40 +1,40 @@ -import * as addProduct from '@/process/add-product'; +// import * as addProduct from '@/process/add-product'; 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 gibdd from '@/process/gibdd'; +// 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 gibdd from '@/process/gibdd'; import { useProcess } from '@/process/hooks'; -import * as insurance from '@/process/insurance'; -import * as leadOpportunity from '@/process/lead-opportunity'; -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 price from '@/process/price'; -import * as subsidy from '@/process/subsidy'; -import * as subsidyImportProgram from '@/process/subsidy-import-program'; -import * as supplierAgent from '@/process/supplier-agent'; -import * as usedPl from '@/process/used-pl'; +// import * as insurance from '@/process/insurance'; +// import * as leadOpportunity from '@/process/lead-opportunity'; +// 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 price from '@/process/price'; +// import * as subsidy from '@/process/subsidy'; +// import * as subsidyImportProgram from '@/process/subsidy-import-program'; +// import * as supplierAgent from '@/process/supplier-agent'; +// import * as usedPl from '@/process/used-pl'; export default function useReactions() { - useProcess(leadOpportunity); - useProcess(loadKP); - useProcess(calculate); - useProcess(supplierAgent); - useProcess(price); - useProcess(fingap); - useProcess(leasingWithoutKasko); - useProcess(subsidy); - useProcess(leasingObject); - useProcess(configurator); - useProcess(createKP); + // useProcess(leadOpportunity); + // useProcess(loadKP); + // useProcess(calculate); + // useProcess(supplierAgent); + // useProcess(price); + // useProcess(fingap); + // useProcess(leasingWithoutKasko); + // useProcess(subsidy); + // useProcess(leasingObject); + // useProcess(configurator); + // useProcess(createKP); useProcess(bonuses); - useProcess(usedPl); - useProcess(subsidyImportProgram); - useProcess(payments); - useProcess(gibdd); - useProcess(addProduct); - useProcess(insurance); + // useProcess(usedPl); + // useProcess(subsidyImportProgram); + // useProcess(payments); + // useProcess(gibdd); + // useProcess(addProduct); + // useProcess(insurance); } diff --git a/apps/web/process/bonuses/reactions/lib/helper.ts b/apps/web/process/bonuses/lib/helper.ts similarity index 97% rename from apps/web/process/bonuses/reactions/lib/helper.ts rename to apps/web/process/bonuses/lib/helper.ts index 4b4ae16..8599393 100644 --- a/apps/web/process/bonuses/reactions/lib/helper.ts +++ b/apps/web/process/bonuses/lib/helper.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { ProcessContext } from '../../../types'; +import type { ProcessContext } from '../../types'; import { getUser } from '@/api/user/query'; import type { ElementsTypes } from '@/Components/Calculation/config/map/values'; import { STALE_TIME } from '@/constants/request'; diff --git a/apps/web/process/bonuses/reactions/common.ts b/apps/web/process/bonuses/reactions/common.ts index 93a4fdc..550f205 100644 --- a/apps/web/process/bonuses/reactions/common.ts +++ b/apps/web/process/bonuses/reactions/common.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../../types'; -import helper from './lib/helper'; +import helper from '../lib/helper'; import { makeDisposable } from '@/../../packages/tools'; import * as CRMTypes from '@/graphql/crm.types'; import dayjs from 'dayjs'; diff --git a/apps/web/process/bonuses/reactions/validation.ts b/apps/web/process/bonuses/reactions/validation.ts index 961c169..b6dc370 100644 --- a/apps/web/process/bonuses/reactions/validation.ts +++ b/apps/web/process/bonuses/reactions/validation.ts @@ -1,23 +1,32 @@ -import helper from './lib/helper'; +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 { reaction } from 'mobx'; -import { round } from 'tools'; +import { uid } from 'radash'; + +const key = uid(7); export default function reactions(context: ProcessContext) { const { store } = context; const { $calculation } = store; - const { getCoefficient } = helper(context); + const validationSchema = createValidationSchema(context); + const helper = new ValidationHelper(); reaction( () => $calculation.$values.getValues(['product', 'saleBonus']), - async ({ product: productId, saleBonus }) => { - const coefficient = await getCoefficient(productId); - const maxBonus = (coefficient?.evo_sot_coefficient || 0) * 100; + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - $calculation.element('tbxSaleBonus').validate({ - invalid: round(saleBonus, 2) > round(maxBonus, 2), - message: 'Размер бонуса МПЛ не может быть выше установленного по СОТ', - }); + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); + }); + } } ); } diff --git a/apps/web/process/bonuses/validation.ts b/apps/web/process/bonuses/validation.ts new file mode 100644 index 0000000..826bae9 --- /dev/null +++ b/apps/web/process/bonuses/validation.ts @@ -0,0 +1,24 @@ +import type { ProcessContext } from '../types'; +import helper from './lib/helper'; +import ValuesSchema from '@/config/schema/values'; +import { round } from 'tools'; +import { z } from 'zod'; + +export function createValidationSchema(context: ProcessContext) { + const { getCoefficient } = helper(context); + + return ValuesSchema.pick({ product: true, saleBonus: true }).superRefine( + async ({ product, saleBonus }, ctx) => { + const coefficient = await getCoefficient(product); + const maxBonus = (coefficient?.evo_sot_coefficient || 0) * 100; + + if (round(saleBonus, 2) > round(maxBonus, 2)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Размер бонуса МПЛ не может быть выше установленного по СОТ', + path: ['tbxSaleBonus'], + }); + } + } + ); +} diff --git a/apps/web/process/calculate/reactions/validation.ts b/apps/web/process/calculate/reactions/validation.ts index 1ace7fb..6de15b4 100644 --- a/apps/web/process/calculate/reactions/validation.ts +++ b/apps/web/process/calculate/reactions/validation.ts @@ -21,6 +21,7 @@ export default function reactions({ store }: ProcessContext) { const hasElementsErrors = Object.values($calculation.$validation).some( (validation) => validation.hasErrors ); + const hasPaymentsErrors = $tables.payments.validation.hasErrors; const hasInsuranceErrors = $tables.insurance.validation.hasErrors; const hasFingapErrors = $tables.fingap.validation.hasErrors; diff --git a/apps/web/stores/calculation/index.ts b/apps/web/stores/calculation/index.ts index 42816cb..ab662c5 100644 --- a/apps/web/stores/calculation/index.ts +++ b/apps/web/stores/calculation/index.ts @@ -1,4 +1,4 @@ -import type { RemoveError, ValidationParams } from '../validation/types'; +import type { ValidationParams } from '../validation/types'; import OptionsStore from './options'; import StatusStore from './statuses'; import ValuesStore from './values'; @@ -56,6 +56,10 @@ export default class CalculationStore { return this.$values.getValue(valueName) as Values.ElementsTypes[E]; }, + removeError: (params: Pick) => { + this.$validation[elementName]?.removeError(params); + }, + reset: () => { const valueName = getValueName(elementName); this.$values.resetValue(valueName); @@ -80,6 +84,12 @@ export default class CalculationStore { return this.element(elementName); }, + setError: (params: ValidationParams) => { + if (!this.$validation[elementName]) this.createElementValidation(elementName); + + return this.$validation[elementName]?.setError(params); + }, + setOptions: (options: Array>) => { this.$options.setOptions(elementName, options); @@ -98,20 +108,5 @@ export default class CalculationStore { return this.element(elementName); }, - - validate: ({ invalid, message, silent, helper }: ValidationParams) => { - if (!this.$validation[elementName]) this.createElementValidation(elementName); - - let removeError: RemoveError | undefined; - - if (invalid) { - removeError = this.$validation[elementName]?.addError(message, silent); - if (helper && removeError) helper.add(removeError); - } else { - this.$validation[elementName]?.removeError(message); - } - - return removeError; - }, }); } diff --git a/apps/web/stores/calculation/validation/hooks.js b/apps/web/stores/calculation/validation/hooks.js index 9ae1faa..7a8a06e 100644 --- a/apps/web/stores/calculation/validation/hooks.js +++ b/apps/web/stores/calculation/validation/hooks.js @@ -2,9 +2,8 @@ import { useStore } from '@/stores/hooks'; export function useValidation(elementName) { const { $calculation } = useStore(); - const messages = $calculation.$validation[elementName]?.getMessages(); - - if (messages?.length) { + const hasErrors = $calculation.$validation?.[elementName]?.hasErrors; + if (hasErrors) { return { help: 'Некорректные данные', isValid: false, diff --git a/apps/web/stores/validation/index.ts b/apps/web/stores/validation/index.ts index e9c86b7..5b9ca5a 100644 --- a/apps/web/stores/validation/index.ts +++ b/apps/web/stores/validation/index.ts @@ -1,32 +1,38 @@ -import type { RemoveError, ValidationConfig } from './types'; +import type { RemoveError, ValidationConfig, ValidationError, ValidationParams } from './types'; import { makeAutoObservable } from 'mobx'; import notification from 'ui/elements/notification'; export default class Validation { private params: ValidationConfig; - private messages: Set; + private errors: Set; constructor(config: ValidationConfig) { this.params = config; - this.messages = new Set(); + this.errors = new Set(); makeAutoObservable(this); } public get hasErrors() { - return this.messages.size > 0; + return this.errors.size > 0; } - public getMessages() { - return [...this.messages]; + public getErrors() { + return [...this.errors]; } - public removeError = (message: string) => { - this.messages.delete(message); - if (this.messages.size === 0) notification.close(this.params.err_key); + public removeError = ({ key }: Pick) => { + const error = [...this.errors].find((x) => x.key === key); + if (error) this.errors.delete(error); + if (this.errors.size === 0) notification.close(this.params.err_key); }; - public addError = (message: string, silent?: boolean) => { - if (!silent && !this.messages.has(message)) { + public setError = ({ key, message, silent }: ValidationParams) => { + const error = [...this.errors].find((x) => x.key === key); + if (error) this.removeError({ key }); + + this.errors.add({ key, message }); + + if (!silent) { notification.error({ description: message, key: this.params.err_key, @@ -34,13 +40,11 @@ export default class Validation { }); } - this.messages.add(message); - - return (() => this.removeError(message)) as RemoveError; + return (() => this.removeError({ key })) as RemoveError; }; public clearErrors = () => { - this.messages.clear(); + this.errors.clear(); notification.close(this.params.err_key); }; } diff --git a/apps/web/stores/validation/types.ts b/apps/web/stores/validation/types.ts index 4c27464..888cd7e 100644 --- a/apps/web/stores/validation/types.ts +++ b/apps/web/stores/validation/types.ts @@ -1,15 +1,10 @@ -import type ValidationHelper from './helper'; - export type ValidationConfig = { err_key: string; err_title: string; }; -export type ValidationParams = { - helper?: ValidationHelper; - invalid: boolean; - message: string; - silent?: boolean; -}; +export type ValidationError = { key: string; message: string }; + +export type ValidationParams = ValidationError & { silent?: boolean }; export type RemoveError = () => void;