ИНЖЕНЕРНЫЕ МЕТОДЫP0L3

Эволюция схемы финансового события

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

Справочный материал. Контракты данных, архитектурные решения и ограничения применения. Источники приведены в материалах, где используются внешние основания.

Задача и границы#

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

Здесь предлагается инженерный контракт для интеграции событий, а не бухгалтерское нормативное требование или описание опыта автора. Алгоритм сверки ключей рассматривается отдельно в сверке наборов событий. Примеры синтетические; исполняемого SQL или Python нет. Статья не подтверждает поведение какой-либо СУБД или установленной библиотеки.

Зерно и независимые версии#

Входное зерно - одна версия исходной строки финансового события. Ключ логической строки в этом примере: (source_system, legal_entity, event_id, line_id). event_version обозначает исправление её содержания по контракту источника. schema_version определяет формат и согласованное значение полей. Переименование поля не является новой финансовой операцией; новая редакция суммы не становится сменой схемы.

business_date задаёт учётную дату, observed_at - первое получение данной версии события EDWH. Время повторной обработки сохраняют отдельно: оно не заменяет первоначальную известность. Если меняется определение самой известности, это изменение контракта времени, которое надо явно отразить в новых выпусках.

Сумма представлена знаковым целым коэффициентом amount_units, валюта обязательна, масштаб s задаётся контрактом: значение в основных единицах равно amount_units / 10^s. В примере дебет положителен, кредит отрицателен. Это соглашение источника, а не универсальная форма проводки. Масштаб хранения не следует автоматически из кода валюты; подробности - в денежной точности.

Идентификатор Что закрепляет
schema_version Неизменяемое описание формата и семантики входа
event_version Редакцию конкретного финансового события
adapter_version Правила преобразования в каноническую модель EDWH
release_id Входной снимок и зависимости конкретного выпуска

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

Совместимость имеет направление#

В этой статье обратная совместимость означает, что новый получатель читает старый вход; прямая - что старый получатель читает новый вход. Двусторонняя требует обоих направлений. Следует проверять все сохраняемые версии, а не только соседние: переходы V1 к V2 и V2 к V3 сами по себе не доказывают чтение V1 получателем V3.

По спецификации Avro 1.12.0 отсутствующее в записи отправителя поле получателя читается с его default; без default возникает ошибка разрешения схем. Default не делает поле необязательным при записи. Лишнее поле отправителя может быть проигнорировано получателем. Эти правила формата не доказывают корректность финансовой интерпретации. Для логического типа decimal при разрешении схем должны совпадать precision и scale.

В JSON Schema Draft 2020-12 required проверяет наличие свойства, но само по себе не запрещает null: нужны отдельные ограничения типа. default является аннотацией, а не предписанием заполнять пропуски. Добавление обязательного свойства делает старый объект без него невалидным по новой схеме. Политика неизвестных полей тоже влияет на совместимость и должна быть закреплена.

Это требования соответствующих спецификаций к формату и валидации. Направления совместимости и правила допуска партии здесь заданы проектным контрактом; бухгалтерский стандарт их не устанавливает. Проверка JSON или успешное чтение Avro не заменяет предметные проверки валюты, знака и назначения суммы.

Три изменения с разными последствиями#

Изменение Риск Предлагаемое решение
Обязательный cost_center в V2 В V1 признак отсутствовал Отдельный адаптер V1, подтверждённое историческое обогащение или карантин
Масштаб RUB с 2 в V1 до 3 в V2 То же целое означает другую сумму Преобразование по закреплённой версии, проверка диапазона и остатка
amount из валовой суммы превращён в чистую Тип и имя прежние, смысл другой Новое семантическое поле или явно несовместимая версия

Нельзя подставлять произвольный центр затрат только ради прохождения required. Если допустима категория «Неизвестно», она должна быть разрешена владельцем данных и явно исключена из расчётов, где нужен реальный центр. Историческое обогащение закрепляет версию справочника и основание выбора на нужную дату; текущее соответствие не доказывает прошлое.

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

Синтетическая арифметическая иллюстрация: V1 передаёт 12345 при s = 2, то есть 123.45 RUB; V2 для той же суммы передаёт 123450 при s = 3. При каноническом масштабе 2 первое значение сохраняется, второе делится на 10 без остатка. Но 123451 в V2 означает 123.451 RUB и не представимо точно при масштабе 2. Контракт должен выбрать более точную модель, разрешённое округление с протоколом потери либо отказ. Молчаливое целочисленное деление недопустимо. Для отрицательных значений действуют те же требования сохранения значения и явной политики округления.

Процесс перехода и повторной обработки#

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

Следующая последовательность - псевдокод процесса, не проверенная реализация:

Закрепить исходные байты, манифест и версию схемы каждой записи.
Выбрать явно поддерживаемую пару схемы и адаптера.
Проверить структуру по исходному контракту.
Преобразовать единицы, знаки и атрибуты без скрытых потерь.
Сохранить происхождение обогащения и причины отказов.
Проверить канонический контракт и финансовые инварианты.
Зафиксировать новый выпуск с входами и версиями зависимостей.
Переключить потребителей только после сравнения результатов.

До перехода фиксируют матрицу отправителей, получателей и архивных версий, критерии допуска и порядок отката. Сначала получатели учатся принимать новую версию, затем источник начинает её отправлять, если такой порядок подтверждён матрицей. Неизвестная версия блокируется, а не интерпретируется «как последняя».

Старая партия читается по своей исходной схеме. Для точного повторения прежнего выпуска нужны прежние адаптер, справочники и правила; для исправленного расчёта выбирают новые зависимости и создают отдельный выпуск. Сохраняют содержимое схем, а не только URL или идентификатор реестра, а также исходный снимок, версии исполняемых артефактов и параметры. Результат не дописывают как новые события в тот же финансовый набор: повторная обработка не должна удваивать проводки. Правила идентичности и фиксации рассмотрены в инкрементальных загрузках.

Инварианты и граничные случаи#

После преобразования сравнивают сумму в одной согласованной единице отдельно по документу и валюте, а также положительные и отрицательные движения. Нулевой общий итог не доказывает правильность масштаба. Сохраняют ключ и версию события, исходное значение, результат и версию адаптера; для разрешённого округления отдельно отражают разницу. Если зерно выхода меняется, требуется явное правило разложения и контроль его суммы, а не сравнение числа строк один к одному.

Перед допуском нужны сценарии отсутствующего поля, явного null, неизвестного значения перечисления, смешанных версий партии, поздней V1 после запуска V2, отрицательной суммы, переполнения при увеличении масштаба и ненулевого остатка при его уменьшении. Проверяют также повтор старой партии с прежним адаптером и сравнение выпусков после его исправления. Это рекомендуемые проверки будущей реализации, а не утверждение о выполненных тестах примеров.

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

Карантин и состояния исключения#

Карантин в предлагаемом контракте - сохранённое исключение из допуска, а не удаление плохой строки. Зерно журнала исключений - одна причина отказа для исходной версии строки при конкретной попытке проверки. exception_id связывает неизменяемые исходные байты, batch_id, позицию в поставке, ключ и версию события, если их удалось прочитать, версии схемы, адаптера и проверки. Для нечитаемой строки достаточно идентификатора поставки и позиции: выдумывать финансовый ключ нельзя. Несколько причин могут относиться к одной версии; число исключений не равно числу отклонённых строк.

Журнал хранит код причины и диагностические сведения, ответственного за решение, время обнаружения и переходов, основание решения, ссылки на повторную попытку и исправляющую версию. Если валюта или масштаб не определены, сумму помечают как неизвестную, а не как ноль. Контроль ведут отдельно по юридическому лицу, партии, периоду и валюте; неопределённый период блокирует все потенциально затронутые выпуски до уточнения периметра. Исходные данные остаются в защищённом контуре с согласованными правами и сроками хранения; публичная статья не содержит их.

Состояние Условие перехода Влияние на допуск
Открыто Проверка отклонила исходную версию Строка не участвует в окончательном результате
Ожидает исправления Назначен владелец, сохранены причина и требуемое действие Исключение остаётся блокирующим
Повторная проверка Получена исправляющая версия либо утверждён исправленный адаптер Это ещё не допуск; новый отказ сохраняет историю
Разрешено допуском Все причины устранены, новая попытка прошла предметные контроли Зафиксированы принятая версия, партия допуска и зависимые периоды
Разрешено исключением из периметра Владелец утвердил доказанное отсутствие строки в согласованном периметре Сохранены основание и новая версия периметра; строка не считается принятой

Переходы добавляют историю, не переписывают первоначальную причину. Закрывать исключение по таймауту или числу повторов нельзя. Окончательный отказ без доказанного решения оставляет выпуск заблокированным. Если одна строка отклонена по двум причинам, устранение одной не разрешает вторую.

Повтор доставки, исправление и повторный допуск#

Технический повтор приносит те же исходные байты и версию события. Он добавляет сведения о доставке, но не новую экономическую операцию и не доказательство исправления. Изменённое содержание под прежней идентичностью версии считается конфликтом по контракту источника. Исправление источником сохраняют как новую версию с явной связью с отклонённой. Исправление адаптера оставляет вход прежним, но закрепляет новую версию преобразования и причину повторной обработки. Первоначальный observed_at не заменяют временем повтора; известность исправляющей версии фиксируют отдельно.

В документации Apache Kafka 4.1 доставка at-least-once допускает повторную обработку; запись во внешнюю систему требует согласования позиции потребителя с сохранённым результатом. Это свойство конкретной технологии, а не гарантия финансового допуска или требование использовать Kafka. Приведённые состояния карантина - проектное решение, не требование Avro, JSON Schema или бухгалтерского стандарта. Поведение транзакций какой-либо СУБД здесь не проверяется.

Синтетический сценарий: партия B1 содержит версию V1 строки RUB с отсутствующим обязательным центром затрат. Повтор B1 оставляет её отклонённой. Источник присылает V2 в B2 с подтверждённым центром затрат; после проверок V2 связывают с исключением V1 и партией B2. V1 не включают дополнительно, поэтому повторный допуск не удваивает движение. Если источнику нужны отдельные сторнирующие события, применяется его контракт коррекции, а не автоматическая замена. Для разных валют разрешение исключений проверяют раздельно.

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

Исключения и готовность выпуска#

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

Для доказанного исключения из периметра требуется явное утверждение нового периметра и его версии; нельзя закрыть ошибку лишь потому, что итоговый баланс равен нулю. Контракт поставки определяет завершённость входа, сверка событий проверяет охват, а закрытие периода определяет выпуск результата. Здесь добавлен только контракт разрешения отклонений.

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

Вопросы для подготовки#

  1. Почему default позволяет прочитать старое событие, но не подтверждает его финансовый смысл?
  2. Что произойдёт с суммой, если масштаб выбрать по дате доставки вместо версии исходного контракта?
  3. Какие зависимости нужны для повторения старого выпуска и какие меняются при исправленном пересчёте?

Источники и документация

  1. Apache Kafka 4.1 - Message Delivery Semantics ↗
  2. Apache Avro 1.12.0 - Specification ↗
  3. JSON Schema - Validation, Draft 2020-12 ↗

Источники нужны для самостоятельной проверки. Примеры и интерпретации на сайте могут быть учебными упрощениями.