Как PDP подключили валидацию адресов через feature-флаг
Разбираем фрагмент интеграции PDP с внешним сервисом нормализации адресов: почему валидация опциональна, а ручной ввод остаётся штатным сценарием.

3 опциональные интеграции в PDP дают 8 состояний для проверки ещё до бизнес-сценариев: внешняя CRM включена или нет, 1С включена или нет, сервис валидации адресов включён или нет.
В этой задаче подключали внешний сервис, который валидирует и нормализует адрес при создании объекта недвижимости. На форме пользователь вводит адрес, система пробует привести его к нормальному виду через сервис, а дальше объект создаётся уже с нормализованными данными.
Но сервис не должен становиться обязательной частью контура. Если он выключен или недоступен, PDP продолжает работать: адрес вводится вручную, объект недвижимости создаётся, остальные интеграции живут по своим флагам.
Почему это не включили жёстко
В PDP уже есть несколько внешних точек: CRM, 1С и теперь валидация адресов. Все они опциональны. Это важно не только для разных установок системы, но и для отказоустойчивости: недоступность адресного сервиса не должна блокировать создание объекта.
У адреса в этой логике два нормальных пути:
- прошёл через внешний сервис и вернулся в нормализованном виде;
- введён вручную, потому что интеграция выключена или сервис не ответил.
Второй путь не считается ошибкой пользователя. Это штатный fallback. Поэтому флаг стоит проверять до вызова внешнего API, а исключение от сервиса не пробрасывать наверх как причину остановить создание объекта.
Упрощённо этот кусок выглядит так:
const integrations = {
externalCrm: env.bool('EXTERNAL_CRM_ENABLED', false),
oneC: env.bool('ONE_C_ENABLED', false),
addressValidation: env.bool('ADDRESS_VALIDATION_ENABLED', false),
}
async function resolveAddress(input: AddressInput): Promise<AddressResult> {
if (!integrations.addressValidation) {
return {
source: 'manual',
address: input,
}
}
try {
const normalized = await addressValidationService.normalize(input)
return {
source: 'validation-service',
address: normalized,
}
} catch (error) {
logger.warn({ error }, 'Address validation service unavailable')
return {
source: 'manual',
address: input,
}
}
}
Здесь важны не названия переменных, а границы ответственности.
Флаг ADDRESS_VALIDATION_ENABLED отвечает только за попытку сходить во внешний сервис. Он не меняет сам сценарий создания объекта недвижимости. Форма и доменная логика получают результат в одном формате: адрес есть всегда, меняется только источник.
source нужен не для красоты. Вызывающий код не должен угадывать, откуда взялся адрес: от сервиса или из ручного ввода. Это особенно полезно рядом с интеграциями, где одинаковое поле дальше может уйти во внешнюю CRM или в 1С.
Ошибка сервиса — не ошибка создания объекта
Самый опасный вариант — связать доступность валидации с возможностью сохранить объект. Тогда внешний сервис становится критической зависимостью, хотя задача у него вспомогательная: подсказать и нормализовать адрес.
Поэтому catch возвращает ручной режим. Да, адрес может остаться в том виде, в котором его ввели. Зато пользователь не теряет сценарий создания объекта из-за чужого API.
Тут легко ошибиться в тестах. Сначала хочется проверить два случая: флаг выключен и флаг включён. Но реальная матрица шире, потому что рядом живут ещё CRM и 1С. Получается минимум 8 комбинаций флагов:
- все интеграции выключены;
- включена только CRM;
- включена только 1С;
- включена только валидация адресов;
- включены пары интеграций;
- включены все три.
И это без проверки недоступности адресного сервиса. Живая неприятность как раз в таких сочетаниях: локально легко включить всё сразу и не заметить, что сценарий «валидация выключена, 1С включена» идёт по другой ветке.
Что проверяли отдельно
Для этого куска важнее не количество тестов, а покрытие состояний флагов и отказа внешнего API.
Минимальный набор, который имеет смысл держать рядом с такой интеграцией:
- при выключенном
ADDRESS_VALIDATION_ENABLEDвнешний сервис не вызывается; - при включённом флаге успешный ответ сервиса возвращает нормализованный адрес;
- при ошибке сервиса возвращается ручной адрес;
- остальные опциональные интеграции не меняют решение о том, можно ли создать объект.
Последний пункт скучный, но именно он защищает от комбинаторики. Валидация адреса не должна внезапно зависеть от того, включена ли отправка данных во внешнюю CRM или 1С. Следующий шаг для таких интеграций — держать матрицу флагов явно, а не собирать её из памяти перед релизом.