Проверка платежных данных

Платёжные данные представляют собой один из наиболее чувствительных типов пользовательской информации. Ошибки в их валидации приводят не только к отказам транзакций, но и к потенциальным уязвимостям, связанным с безопасностью и мошенничеством. Библиотека Joi позволяет строить строгие и выразительные схемы проверки данных, обеспечивая предсказуемость и контроль на уровне входных структур.

В контексте платёжных форм Joi применяется для проверки номеров банковских карт, сроков действия, CVV-кодов, платёжных адресов, а также комплексных объектов транзакций.


Структура платёжного объекта

Типичная структура платёжных данных может включать:

  • номер карты;
  • срок действия;
  • CVV/CVC код;
  • имя держателя;
  • биллинговый адрес;
  • сумма и валюта;
  • идентификатор метода оплаты.

Joi позволяет описать эту структуру декларативно, превращая набор правил в единый объект схемы.

import Joi from 'joi';

const paymentSchema = Joi.object({
  cardNumber: Joi.string().required(),
  expiryMonth: Joi.number().min(1).max(12).required(),
  expiryYear: Joi.number().min(2024).required(),
  cvv: Joi.string().length(3).required(),
  holderName: Joi.string().min(3).max(100).required(),
  amount: Joi.number().positive().precision(2).required(),
  currency: Joi.string().length(3).required()
});

Такой подход обеспечивает централизованную проверку и исключает дублирование логики.


Проверка номера банковской карты

Номер карты требует не только проверки формата, но и структурной корректности. На практике применяется алгоритм Луна, однако Joi не реализует его встроенно, поэтому используется кастомная проверка.

const luhnCheck = (value) => {
  let sum = 0;
  let shouldDouble = false;

  for (let i = value.length - 1; i >= 0; i--) {
    let digit = parseInt(value[i], 10);

    if (shouldDouble) {
      digit *= 2;
      if (digit > 9) digit -= 9;
    }

    sum += digit;
    shouldDouble = !shouldDouble;
  }

  return sum % 10 === 0;
};

const cardNumberSchema = Joi.string()
  .pattern(/^\d{13,19}$/)
  .custom((value, helpers) => {
    if (!luhnCheck(value)) {
      return helpers.error('any.invalid');
    }
    return value;
  });

Такой уровень проверки позволяет отсечь случайные ошибки ввода и часть недействительных номеров.


Проверка срока действия карты

Срок действия карты состоит из месяца и года, которые должны быть не только валидными по диапазону, но и не находиться в прошлом.

const expirySchema = Joi.object({
  month: Joi.number().min(1).max(12).required(),
  year: Joi.number().min(2024).required()
}).custom((value, helpers) => {
  const now = new Date();
  const currentYear = now.getFullYear();
  const currentMonth = now.getMonth() + 1;

  if (value.year === currentYear && value.month < currentMonth) {
    return helpers.error('date.expired');
  }

  if (value.year < currentYear) {
    return helpers.error('date.expired');
  }

  return value;
});

Подобная проверка предотвращает использование просроченных платёжных инструментов ещё до обращения к платёжному шлюзу.


Валидация CVV/CVC

CVV-код зависит от платёжной системы и обычно имеет длину 3 или 4 цифры.

const cvvSchema = Joi.string()
  .pattern(/^\d{3,4}$/)
  .required();

Дополнительно может применяться привязка к типу карты, однако это уже относится к бизнес-логике.


Имя держателя карты

Имя держателя карты часто используется в антифрод-системах. Joi позволяет ограничить набор допустимых символов:

const holderNameSchema = Joi.string()
  .min(3)
  .max(100)
  .pattern(/^[a-zA-Z\s'-]+$/)
  .required();

Такое ограничение исключает числовые значения и специальные символы, не характерные для имён.


Комплексная схема платёжной формы

Объединение всех компонентов формирует целостную схему:

const paymentSchema = Joi.object({
  cardNumber: cardNumberSchema.required(),
  expiry: expirySchema.required(),
  cvv: cvvSchema.required(),
  holderName: holderNameSchema.required(),
  amount: Joi.number().positive().precision(2).required(),
  currency: Joi.string().length(3).uppercase().required()
});

Валидация выполняется единым вызовом:

const result = paymentSchema.validate(data);

if (result.error) {
  console.log(result.error.details);
}

Нормализация входных данных

Перед проверкой часто требуется очистка данных: удаление пробелов, дефисов и других символов, вводимых пользователем.

const normalizeCardNumber = (value) =>
  value.replace(/[\s-]/g, '');

const cardSchema = Joi.string()
  .custom((value, helpers) => normalizeCardNumber(value))
  .pattern(/^\d{13,19}$/);

Такой подход снижает количество ошибок, связанных с форматом ввода.


Условная валидация платёжных методов

Платёжные системы часто поддерживают несколько методов оплаты: карта, PayPal, банковский перевод. Joi позволяет строить условные схемы.

const paymentMethodSchema = Joi.object({
  method: Joi.string().valid('card', 'paypal', 'bank'),
  card: Joi.when('method', {
    is: 'card',
    then: Joi.object({
      number: cardNumberSchema.required(),
      cvv: cvvSchema.required()
    }).required(),
    otherwise: Joi.forbidden()
  })
});

Это исключает передачу лишних данных для неподходящего метода.


Проверка валюты и суммы

Финансовые данные требуют строгой типизации и ограничений.

const amountSchema = Joi.number()
  .positive()
  .precision(2)
  .min(0.01)
  .max(100000);

Валюта обычно фиксируется по стандарту ISO 4217:

const currencySchema = Joi.string()
  .valid('USD', 'EUR', 'KZT', 'GBP')
  .required();

Кастомные сообщения об ошибках

Joi позволяет задавать человекочитаемые сообщения, что важно для финансовых интерфейсов:

const schema = Joi.string()
  .pattern(/^\d{16}$/)
  .messages({
    'string.pattern.base': 'Номер карты должен содержать 16 цифр'
  });

Защита от избыточных полей

В платёжных данных критично запрещать неожиданные поля:

const strictPaymentSchema = Joi.object({
  cardNumber: Joi.string().required(),
  cvv: Joi.string().required()
}).unknown(false);

Это предотвращает передачу потенциально опасных или лишних данных.


Сложные вложенные структуры транзакций

В реальных системах платёжный объект включает метаданные:

const transactionSchema = Joi.object({
  payment: paymentSchema.required(),
  customer: Joi.object({
    id: Joi.string().required(),
    email: Joi.string().email().required()
  }),
  metadata: Joi.object().pattern(Joi.string(), Joi.any())
});

Такой подход позволяет масштабировать структуру без потери контроля над критичными полями.


Асинхронная проверка данных

Некоторые проверки требуют обращения к внешним сервисам, например, проверка карты в платёжном шлюзе.

const asyncSchema = Joi.string().external(async (value) => {
  const exists = await checkCardInGateway(value);
  if (!exists) {
    throw new Error('Card not supported');
  }
});

Это расширяет возможности Joi за пределы синхронной валидации.


Контроль качества входных данных

При проектировании платёжных систем Joi используется как первый барьер защиты. Его роль заключается в:

  • фильтрации некорректных данных до бизнес-логики;
  • снижении нагрузки на внешние сервисы;
  • унификации структуры ошибок;
  • предотвращении инъекций через поля формы.

Комбинация строгих схем, кастомных валидаторов и нормализации данных формирует устойчивую основу для безопасной обработки финансовых операций.