Валидация данных юридического лица. Formik, Yup, React, TypeScript

Йо-йо! Очень часто приходиться валидировать разные формы. Вот и мне недавно пришлось создать форму регистрации поставщика. У такой формы множество полей и у каждого поля есть свои правила валидации. В этой статье я расскажу как «победил» такую форму, какие сложности я встретил и поделюсь тонкостями валидации данных юридических лиц.

У этой статьи появились «братья») Смотрите как валидировать данные в видео-формате: валидация формы регистрации, валидация файлов, валидация данных юридических лиц

Сложности валидации данных юридических лиц

Самая первая сложность — это валидация множества полей. Часто можно просто проверить пару поля и выдать ошибку или дать возможность отправить форму. Но в случае с формой данных юр. лица нужно проверить множество полей и если не иметь системы то код становиться огромным и не читаемым.

Вторая сложность — большая вероятность ошибки при вводе данных. Есть такие поля как ИНН, КПП, расчётный счёт и т.д. Они представляют из себя длинную последовательность цифр. Есть большая вероятность ошибки.

Третья сложность зависимость данных друг от друга. Такая сложность встретилась при проверке расчётного и корреспондентского счёта. Их правильность зависит от БИК.

Вероятность ошибки при вводе данных юр. лица исключается с помощью вычисления контрольных сумм. Дело в том, что есть специальные формулы, которые нужны для создания правильного, например, ИНН. И есть обратные формулы вычисления контрольных сумм в которых у каждого числа есть вес и сумма весов после преобразования должна быть равна какому-то значению.

Контрольные суммы. Валидация

Когда я писал свой код я нашёл несколько полезных ссылок, перейдя на которые вы сможете найти некоторые функции вычисления контрольных сумм на разных языках программирования:

На всех этих ссылках я не нашёл полного списка функций, которые мне были нужны (или некоторые мне нужно было разделить) и я написал свои версии. Мои версии на 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)
}

Валидация объектов данных через Yup

Кроме самих функций мне нужен интерфейс, который делает следующие задачи: Принимает объект с данными, проверяет правильность ввода данных, выводит текст ошибки, в зависимости от типа ошибки, возвращает валидны ли данные в объекте.

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 c валидацией через Yup

Что такое формик? Это либа, которая помогает нам работать с формами, валидацией данных, изменения значений формы. И Formik отлично подходит для работы с 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, методы, декларирование типов для методов в 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 значения: значение поля (например, расчётного счёта) и значение другого поля.

Добавление методов к Yup в TypeScript

В простом 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))
    }
  })
}