Йо-йо! Очень часто приходиться валидировать разные формы. Вот и мне недавно пришлось создать форму регистрации поставщика. У такой формы множество полей и у каждого поля есть свои правила валидации. В этой статье я расскажу как «победил» такую форму, какие сложности я встретил и поделюсь тонкостями валидации данных юридических лиц.
Появилась библиотека валидации данных юр. лиц. Переходите по ссылке
// Устанавливай npm i @utkonos/entrepreneur // Читай документацию https://github.com/utkonos-ru/utk-entrepreneur
У этой статьи появились «братья») Смотрите как валидировать данные в видеоформате: валидация формы регистрации, валидация файлов, валидация данных юридических лиц
Самая первая сложность — это валидация множества полей. Часто можно просто проверить пару полей и выдать ошибку или дать возможность отправить форму. Но в случае с формой данных юр. лица нужно проверить множество полей и если не иметь системы, то код становиться огромным и нечитаемым.
Вторая сложность — большая вероятность ошибки при вводе данных. Есть такие поля как ИНН, КПП, расчётный счёт и т.д. Они представляют из себя длинную последовательность цифр. Существует большая вероятность ошибки.
Третья сложность — зависимость данных друг от друга. Такая сложность встретилась при проверке расчётного и корреспондентского счёта. Их правильность зависит от БИК.
Вероятность ошибки при вводе данных юр. лица исключается с помощью вычисления контрольных сумм. Дело в том, что есть специальные формулы, которые нужны для создания правильного ИНН, например. И есть обратные формулы вычисления контрольных сумм, в которых у каждого числа есть вес и сумма весов, которая после преобразования должна быть равна какому-то значению.
Когда я писал свой код, нашёл несколько полезных ссылок. Перейдя на них, вы сможете найти некоторые функции вычисления контрольных сумм на разных языках программирования:
На всех этих ссылках я не нашёл полного списка функций, которые мне были нужны (или некоторые мне нужно было разделить). Я написал свои версии. Мои версии на TypeScript, но их легко переделать в JavaScript, убрав типы.
export const isOGRNIP = (value: number) : boolean => { const valueToString = value ? value.toString() : '' if (valueToString.length === 15) { const num14 = Math.floor((value / 10) % 13); var dgt15 = num14 % 10; return (parseInt(value.toString()[14]) === dgt15) } return false }
export const isOGTN = (value: number) : boolean => { const valueToString = value ? value.toString() : '' if(valueToString.length === 13){ const num12 = Math.floor((value / 10) % 11) const dgt13 = (num12 === 10) ? 0 : num12 return (parseInt(valueToString[12]) === dgt13) } return false }
export const isKPP = (value: number) : boolean => { const valueToString = value ? value.toString() : '' if(valueToString.length !== 9) return false if(!valueToString.match(/\d{4}[\dA-Z][\dA-Z]\d{3}/)) return false return true }
export const isINNIndividual = (value: number) : boolean => { const valueToString = value ? value.toString() : '' const getN = (index: number) : number => (parseInt(valueToString[index])) if(valueToString.length === 12){ const dgt11 = (( 7 * getN(0) + 2 * getN(1) + 4 * getN(2) + 10 * getN(3) + 3 * getN(4) + 5 * getN(5) + 9 * getN(6) + 4 * getN(7) + 6 * getN(8) + 8 * getN(9)) % 11) % 10 const dgt12 = (( 3 * getN(0) + 7 * getN(1) + 2 * getN(2) + 4 * getN(3) + 10 * getN(4) + 3 * getN(5) + 5 * getN(6) + 9 * getN(7) + 4 * getN(8) + 6 * getN(9) + 8 * getN(10)) % 11) % 10 return (getN(10) === dgt11 && getN(11) === dgt12) } return false }
export const isINNLegalEntity = (value: number) : boolean => { const valueToString = value ? value.toString() : '' const getN = (index: number) : number => (parseInt(valueToString[index])) if(valueToString.length === 10){ const dgt10 = (( 2 * getN(0) + 4 * getN(1) + 10 * getN(2) + 3 * getN(3) + 5 * getN(4) + 9 * getN(5) + 4 * getN(6) + 6 * getN(7) + 8 * getN(8) ) % 11) % 10 return (getN(9) === dgt10) } return false }
export const checkBIK = (value: string) : boolean => { if(!/^\d{9}$/.test(value)) return false const thirdPart = value.slice(-3); if(+thirdPart === 0 || +thirdPart === 1 || +thirdPart === 2) return true return +thirdPart >= 50 && +thirdPart < 1000 }
export const checkPaymetAccount = (value: string, bik: string): boolean => { const valueToString = value ? value.toString() : ''; if (checkBIK(bik)) { if (!/[^0-9]/.test(valueToString) && valueToString.length === 20) { const bikRs = bik.toString().slice(-3) + valueToString; let checkSum = 0; const coefficients = [7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1]; for (var i in coefficients) { checkSum += coefficients[i] * (Number(bikRs[i]) % 10); } return checkSum % 10 === 0; } } return false; }; export const checkCorrespondentAccount = (value: string, bik: string) : boolean => { const valueToString = value ? value.toString() : '' if(checkBIK(bik)){ if(!/[^0-9]/.test(valueToString) && valueToString.length === 20){ const bikKs = '0' + bik.slice(4, 6) + valueToString; let checkSum = 0 const coefficients = [7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1, 3, 7, 1] for (var i in coefficients) { checkSum += coefficients[i] * (Number(bikKs[i]) % 10); } return (checkSum % 10 === 0) } } return false }
export const checkOKATO = (value: string) : boolean => { if(!value) return false const length = value.length if(length < 3) return false const getWeight = (index: number) : number => { if(index < 10) return index + 1 else return index % 10 + 1 } const getExpectedValue = () => { if(length < 4 ) return value.slice(-1) if(length >= 4 && length < 6) return value.substr(0, 3).slice(-1) if(length >= 6 && length < 9) return value.substr(0, 6).slice(-1) else return value.substr(0, 9).slice(-1) } const expectedValue = Number(getExpectedValue()) const getTestingString = () => { if(length < 3 ) return value if(length >= 3 && length < 5) return value.substr(0, 2) if(length >= 5 && length < 8) return value.substr(0, 5) else return value.substr(0, 8) } const valueStr = getTestingString() let summ = 0 for(const i in valueStr.split('')){ summ += Number(valueStr[i]) * getWeight(Number(i)) } const del11 = summ % 11 const check = (del11 === 10) ? 0 : del11 if(length > 9 && ( check === del11 )) return true if(check === expectedValue) return true return false }
export const checkOKPO = (value: string) : boolean => { if(!value) return false const expectedValue = Number(value.slice(-1)) const getWeight = (index: number) : number => { if(index < 10) return index + 1 else return index % 10 + 1 } const testingValue = value.slice(0, -1) let summ = 0 for(const i in testingValue.split('')){ summ += Number(testingValue[i]) * getWeight(Number(i)) } let del11 = summ % 11 if(del11 === 10){ summ = 0 for(const i in testingValue.split('')){ summ += Number(testingValue[i]) * ( getWeight(Number(i)) + 2) } del11 = (del11 === 10) ? 0 : del11 } return (del11 === expectedValue) }
Кроме самих функций мне нужен интерфейс, который делает следующие задачи: Принимает объект с данными, проверяет правильность ввода данных, выводит текст ошибки в зависимости от типа ошибки, возвращает валидны ли данные в объекте.
C такой задачей справляется библиотека yup. Yup умеет проверять и преобразовывать значения форм или переданных значений. В этой статье будет рассказано про проверку переданных значений.
Давайте рассмотрим пример валидации ИНН в составе объекта данных. У нас уже есть функция isINNIndividual (для валидации ИНН ИП). Давайте создадим схему валидации в Yup:
import * as yup from 'yup' export const validShape = yup.object().shape({ ... inn: yup .number() .test('innValid', 'Неверный ИНН', value => isINNIndividual(value)) .required(), .... })
Давайте разберём строку с операциями валидации ИНН.
inn: yup .number() // Проверяет как число, преобразует в число // Метод test принимает некое название теста, описание ошибки и функцию, которая возвращает true или false в зависимости от переданного значения .test('innValid', 'Неверный ИНН', value => isINNIndividual(value)) .required(), // Поле обязательное
При передаче в нашу схему объекта, нам будут возвращаться различные ошибки (если они будут). Но есть небольшая проблема — yup возвращает нам данные в не очень удобном виде. Хотелось бы иметь удобный интерфейс для работы с ошибками… И такой есть.
Что такое формик? Это либа, которая помогает нам работать с формами, валидацией данных, изменениями значений формы. И Formik отлично подходит для работы с Yup.
Допустим у нас в state есть объект:
this.state = { formValues: {inn: '', okpo: ''} }
и правила валидации:
export const validShape = yup.object().shape({ okpo: yup.string().test('checkOKPO', 'Неверный ОКПО', value => checkOKPO(value )).required() inn: yup .number() .test('innValid', 'Неверный ИНН', value => isINNIndividual(value)) .required(), })
Будьте внимательны, в функции isINNIndividual мы ожидаем на входе число и правило у нас первое yup.number(), а в checkOKPO мы ожидаем строку и правило там .string().
Далее переходим в render:
render(){ const { formValues } = this.state return( <Formik initialValues={formValues} validationSchema={validShape} onSubmit={values => handleSubmit(values)} // Функция на submit validateOnChange validateOnBlur > {({errors, touched, handleChange, values, isValid}) => ( <Form> { Object.keys(formValues).map((key) => { return <Input name={key} value={values[key]} error={error[name]} onChange={handleChange}/> }) } { isValid && <button onClick={()=>handleSubmit(values)}></button> } </Form> )} </Formik> ) }
Прошу заметить, что в моём примере Input — это компонент, который просто добавляет красивости на UI и выводит ошибки. Вы можете сделать такой самостоятельно.
В данном примере есть функция handleChange, которая приходит к нам из Formik. У неё есть особенности: на вход она принимает событие и необязательный параметр — детей формы, у изменяемого поля обязательно должен присутствовать атрибут name равный ключу объекта с данными переданными в initialValues.
Это был простой пример для типичного случая, но есть специфический случай — валидность одного поля зависит от другого как в случае с БИК, расчётным и корр. счетами. В Fromik, не будет преобразований, но вот в Yup…
В этом разделе будут рассмотрены сразу несколько моментов: валидация данных в зависимости от других полей, декларирование методов в Yup c TypeScript и использование таких методов.
И так.. начнём с самого начала. Представим, что у нас есть следующая схема валидации:
validationShapeEntrepreneur = yup.object().shape({ bik: yup .string() .test('bikwe', 'bikwe', value => checkBIK(value)) // Валидация БИК .required(), accountNumber: yup .string() .checkPaymentMethod(yup.ref('bik')) // Метод, который мы создадим и предадим данный из БИК .required(), correspAccountNum: yup .string() .checkCorrespondentMethod(yup.ref('bik')) // Метод, который мы создадим и предадим данный из БИК .required() })
Нам нужно создать метод, который будет проверять расчётный счёт и корреспондентский счёт. Давайте создадим их:
function checkPayment(this, ref, msg) { return this.test({ name: 'checkPaymentMethod', exclusive: false, message: msg || 'accountNumber', // Сообщение об ошибке params: { reference: ref.path}, test: function(value){ return checkPaymetAccount(value, this.resolve(ref)) // Функция валидации } }) } function checkCorrespondent(this, ref, msg) { return this.test({ name: 'correspAccountNum', exclusive: false, message: msg || 'correspAccountNum', // Сообщение об ошибке params: { reference: ref.path}, test: function(value){ return checkCorrespondentAccount(value, this.resolve(ref)) // Функция валидации } }) } yup.addMethod(yup.string, 'checkPaymentMethod', checkPayment) // Добавление метода к YUP yup.addMethod(yup.string, 'checkCorrespondentMethod', checkCorrespondent) // Добавление метода к YUP
В функции валидации передаётся 2 значения: значение поля (например, расчётного счёта) и значение другого поля.
В простом JavaScript будет работать и так, но не в TypeScript. Для него нужно сделать некоторые изменения:
declare module "yup" { interface StringSchema { checkPaymentMethod(ref: Ref): StringSchema; checkCorrespondentMethod(ref: Ref): StringSchema; } }
Т.к. у нас функции валидации ожидают строку, то для интерфейса StringShema мы добавляем наши методы. Если бы мы ожидали число или другой тип данных, то нам нужно было бы объявить методы для соответствующего интерфейса, например NumberSchema. Из названия были у нас в
yup.addMethod(yup.string, 'checkPaymentMethod', checkPayment) // второй параметр
Добавление метода мы тоже привяжем к типу:
yup.addMethod<yup.StringSchema>(yup.string, 'checkPaymentMethod', checkPayment); yup.addMethod<yup.StringSchema>(yup.string, 'checkCorrespondentMethod', checkCorrespondent);
Аналогично поступим с самими функциями методов, конечно же не забывая про соответствия схем.
function checkPayment(this: yup.StringSchema, ref: any, msg: string) { return this.test({ name: 'checkPaymentMethod', exclusive: false, message: msg || 'accountNumber', params: { reference: ref.path}, test: function(value: string){ return checkPaymetAccount(value, this.resolve(ref)) } }) } function checkCorrespondent(this: yup.StringSchema, ref: any, msg: string) { return this.test({ name: 'correspAccountNum', exclusive: false, message: msg || 'correspAccountNum', params: { reference: ref.path}, test: function(value: string){ return checkCorrespondentAccount(value, this.resolve(ref)) } }) }