fix(generator): исправлен расчёт скидки после складской дельты

- Зачем:
  - слепок D7 содержал отрицательный итог заказа и попадал в брак ODS.
- Что:
  - скидка пересчитана от корзины после удаления отсутствующей позиции.
  - сериализатор отклоняет отрицательные деньги через ValueError.
  - добавлены проверки восьми стартовых дней и обновлена опись мира.
- Проверка:
  - make lint, make typecheck и make test в generator/.
  - живой world_next_day: 1694 строки приняты, таблица ошибок пуста.
This commit is contained in:
2026-08-20 13:13:14 +03:00
parent 7b89ccb529
commit 64bcd3d596
8 changed files with 123 additions and 72 deletions
+20 -16
View File
@@ -78,22 +78,26 @@
**Дельта суммы (C) — вычеркнутая позиция.** Товара не оказалось в наличии,
позицию сняли: у заказа на одну позицию меньше, чем в клиентских массивах, а
`items_total` меньше на её стоимость. Уменьшаются ровно на полную стоимость
вычеркнутой строки и `items_total`, и `total`; скидка остаётся рассчитанной от
исходной клиентской покупки и после складского вычёркивания не пересчитывается.
Момента у неё нет — заказ приезжает
урезанным во всех своих слепках: первый слепок снимается на границе суток,
когда склад заказ уже собрал. Заказ из одной позиции дельты не получает —
пустых заказов не бывает. Позиция выбирается равновероятно: корреляция со
спросом на доле 1–2% статистически ненаблюдаема — менти платил бы за неё
таблицей чисел мира, а увидеть не мог бы ничем. Доводы за вычёркивание:
остаток — сотни рублей, он торчит в витрине сверки сам; расхождение
объясняется сравнением позиций — разбором вложенного JSON и `ARRAY JOIN`,
ровно тем навыком, ради которого позиции разбираются; история рассказывается
словами без легенды про генератор. Отклонено: *переоценка позиции* и *другое
количество* — дельта в десятки рублей, её надо захотеть заметить; *чистая
дельта без истории* — тупик, объяснить нечем; *врёт клиент, а не бэкенд*
заказ у нас проекция той же корзины.
`items_total` меньше на её полную стоимость. После вычёркивания деньги заказа
считаются от оставшейся корзины по [общему
правилу](snapshot.md#откуда-берётся-заказ), поэтому `total` остаётся
неотрицательным по построению. Момента у дельты нет — заказ приезжает урезанным
во всех своих слепках: первый слепок снимается на границе суток, когда склад
заказ уже собрал. Заказ из одной позиции дельты не получает — пустых заказов
не бывает. Позиция выбирается равновероятно: корреляция со спросом на доле
1–2% статистически ненаблюдаема — менти платил бы за неё таблицей чисел мира,
а увидеть не мог бы ничем. Доводы за вычёркивание: остаток — сотни рублей, он
торчит в витрине сверки сам; расхождение объясняется сравнением позиций —
разбором вложенного JSON и `ARRAY JOIN`, ровно тем навыком, ради которого
позиции разбираются; история рассказывается словами без легенды про генератор.
Отклонено: *сохранять скидку исходной корзины и отбирать только безопасные
позиции* — отсутствие товара стало бы зависеть от купона и доставки, а выбор
позиции перестал бы быть равновероятным; *разрешить отрицательный `total` или
ослабить строгий приём* — ошибка источника превратилась бы в брак, который
молча терпит хранилище; *переоценка позиции* и *другое количество* — дельта в
десятки рублей, её надо захотеть заметить; *чистая дельта без истории*
тупик, объяснить нечем; *врёт клиент, а не бэкенд* — заказ у нас проекция той
же корзины.
**Потеря события (B) — точечная.** Уходит строка `purchase`, просмотр
`/confirmation` остаётся: события уезжают разными запросами, потерять один и
+6 -7
View File
@@ -171,13 +171,12 @@ JSON-объектом с точным набором ключей: `order_id`, `
опытов убраны. Числа, механика опытов и поведение ClickHouse, на которое всё это
опирается, — [документ хранилища](../storage.md), «Что проверено».
Одно расхождение с ожиданием осталось, и оно снаружи приёма: восемь настоящих
строк слепка получили класс `field_invalid` — все версии одного заказа с
отрицательным `total`. Класс заслужен, граница верна, дефект в генераторе и
заведён отдельным issue
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102).
Пока он не починен, критерий «честный прогон дня даёт пустой `_errors`» на
стартовом мире не выполняется.
После исправления
[#102](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/102)
честный прогон дня снят на живом стенде 20 августа 2026 года: все 1694 строки
слепка дня 7 приняты, `ods.order_snapshot_errors` осталась пустой. У заказа
`20260608-0007`, который прежде приходил с отрицательным итогом, теперь
`5490.00 823.50 + 299.00 = 4965.50`.
Забор из Kafka в STG снят на живом стенде 18 августа 2026 года при исполнении
#93: одно прямое чтение приносит весь слепок дня, метаданные доставки доступны,
+9 -3
View File
@@ -32,11 +32,12 @@
позиций, посчитанная торговой половиной, то же число, что уехало клиентским
`purchaseRevenue`, но у заказа с вычеркнутой позицией он меньше на её полную
стоимость ([судьба заказа](fate.md)); `discount` — процент промокода от
исходной суммы клиентской покупки, округлённый вниз, и дельта его не
пересчитывает;
оставшегося `items_total`, округлённый вниз: склад сначала определяет, какие
позиции есть в заказе, затем бэкенд считает скидку от итоговой корзины;
`delivery` — бросок заказной стороны по таблице целых весов, единственные
деньги заказа, которых нет ни в одном событии; `total` = `items_total`
`discount` + `delivery`. Отсюда и правило витрин «деньги считаем по
`discount` + `delivery`. Скидка не превышает `items_total`, поэтому итог
неотрицателен по построению. Отсюда и правило витрин «деньги считаем по
бэкенду»: про скидку и доставку клиент не знает вовсе.
Отклонено: *выводить заказ разбором собственного вывода* (`purchaseID`, сырой
@@ -102,6 +103,11 @@
развилки
[«Форма записи слепка на проводе»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/81)).
Отрицательные копейки канонический сериализатор отвергает через `ValueError`
до сборки записи: [контракт строгого
приёма](ingestion.md#граница-строгого-приёма) их не допускает, а появление
такого значения означает ошибку арифметики источника.
Сверх контракта здесь живёт одно правило: **порядок строк внутри слепка —
порядок рождения заказов, он же возрастание `order_id`**. Детерминизм даёт
его даром, а хешу слепка в описи нужен именно названный порядок.