Files
clickstream-data-platform/docs/architecture/orders/snapshot.md
T
ddadminandClaude Opus 5 b2fcc7e593 feat(generator): заказы дня — проекция покупок с деньгами магазина
- Зачем:
  - тикет #90: второй источник должен смотреть на торговую половину дня
    глазами бэкенда — заказ есть проекция покупки, а не второе порождение;
    учебный результат — два источника согласованы по построению, а не
    сверкой.
- Что:
  - `commerce` отдаёт вторым выходом покупки дня: номер заказа, корзину,
    выручку клиента, промокод и человека за кукой; новых бросков в
    подпоток `COMMERCE` не добавилось, события дня не сдвинулись.
  - заведена заказная сторона: подпоток `ORDERS` на позиции 2 дерева зерна
    (прежнее мёртвое имя `DISCREPANCIES`), ветвление — по дню рождения
    заказа; `LATECOMERS` не тронут, его наполнит этап 6.
  - новый модуль `orders.py`: деньги заказа целыми копейками —
    `items_total` из корзины, `discount` по таблице «код → скидка»
    (округление вниз), `delivery` броском по таблице весов на полную длину
    дня, `total` = `items_total` − `discount` + `delivery`.
  - план состава завёл `person_id` — последним броском когорты, после
    паспортов: вторая кука пары повторяет ID своего человека, заказ
    показывает его как `user_id`, в событие Метрики он не попадает.
  - `world`: окно изменяемости `ORDER_WINDOW_DAYS` = 7 рядом с D0 и поясом,
    черновая таблица стоимости доставки.
  - слепок дня в тестах сторожит обе половины: заказы сравниваются наравне
    с потоком событий.
  - доки: имя подпотока заказной стороны и состав денег заказа записаны в
    `docs/architecture/orders`, README генератора знает про новый модуль.
- Проверка:
  - из `generator/`: make lint, make typecheck, make test (417 тестов);
    в корне — make lint.
  - опись мира не покраснела: байты восьми дней те же, заказная сторона
    события не сдвинула.

Closes #90

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:30:13 +03:00

9.6 KiB
Raw Blame History

Заказ и его слепок

Резолюция развилки «Генератор слепков: где живёт и чем связан с событиями».

Откуда берётся заказ

Заказ и событие purchase — не два порождения, а две проекции одного факта мира. Корзина, цены, купон и номер заказа посчитаны торговой половиной дня-функции; заказная половина берёт заказы дня готовой структурой — вторым выходом commerce, — навешивает на них жизнь заказа и собирает слепок. В подпоток COMMERCE не добавляется ни одного нового броска: мир не сдвигается, согласованность двух источников не удерживается, а получается по построению.

Следствия, которые уже решены соседями и здесь только связываются:

  • у всякого заказа изначально ровно одно событие purchase; заказ без события в трекере — не отдельная порода, а класс B, и делает его событийная сторона выбрасыванием события после присвоения номера (классы расхождений);
  • номер заказа общий у обеих проекций: order_id = клиентский purchaseID, читаемый номер «день и порядковый номер покупки» (спека генератора, раздел 9); нумеруются все покупки, дошедшие до потока дня, — до всяких потерь;
  • скидка заказа выводится из промокода события по таблице «код → скидка» — числу мира, которое этап 3 берёт готовым (спека генератора, разделы 8 и 9).

В модели строка заказа в базе источника создаётся синхронно с покупкой, поэтому день рождения заказа и день создания строки совпадают.

Деньги заказа складываются целыми копейками: items_total — сумма позиций, посчитанная торговой половиной, то же число, что уехало клиентским purchaseRevenue; discount — процент промокода от неё, округлённый вниз; delivery — бросок заказной стороны по таблице целых весов, единственные деньги заказа, которых нет ни в одном событии; total = items_total discount + delivery. Отсюда и правило витрин «деньги считаем по бэкенду»: про скидку и доставку клиент не знает вовсе.

Отклонено: выводить заказ разбором собственного вывода (purchaseID, сырой ecommerce) — бэкенд стал бы читателем трекера ровно там, где стенд учит, что это разные источники; независимая модель бэкенда (заказ первичен, событие — эхо) — кто купил, решает воронка, а воронка — это трафик, то есть опрокидывание всего генератора; слепок собирает SQL стенда из событий — второй источник исчезает вместе с уроком «две версии правды».

Слепок и его доставка

Запуск. Третья команда того же пакета — snapshot --day D [--days N], свой приёмник, топик orders. Два источника — два запуска: трекер и бэкенд видны глазами как два производителя, каждый со своим топиком. Один прогон с двумя выходами отклонён: экономии он не даёт (со сдвигом отправки окно слепка и сыгранный день не пересекаются вовсе), а правило «приёмник выбирается тем, что для него назвали» ломает. Отклонены также: отдельный пакет и образ — библиотека на двоих ради одной команды; генератор пишет слепок файлом, в топик льёт даг — второй путь доставки и второй сериализатор; слепок едет топиком hits — убивает два режима приёма.

Сборка окна. Слепок дня D несёт заказы, рождённые в дни D−6…D, и собирается переигровкой этих семи дней: заказы дня — производная всей воронки дня, дешёвого пути к ним нет. Цена — семь проигрышей дня (~14 с) на слепок; у начала оси окно усекается само. Отклонено: кэш заказов на томе — состояние между прогонами; окно держит хранилище — топик перестаёт нести слепок; K = 1 — это страховочный срез 1 мастер-спеки, он в резерве.

Отправка. Слепок снимается на границе суток, а отправляется следующим прогоном: даг, играющий день D, отправляет слепок дня D−1 — ночная выгрузка бэкенда за вчера, как в бою. Содержимое слепка — чистая функция (зерно, D), от момента отправки не зависит. Следствия:

  • живой день перестаёт быть особым случаем: своего дага у него нет, слепок живого дня отправит следующий прогон;
  • пропущенный день лечится окном: слепок переснимается и даёт те же байты, отдельного механизма самовосстановления нет;
  • покупки текущего дня в сверке всегда awaiting_order — сюжет «вчера не сходилось, сегодня сошлось», ради которого мастер-спека этот класс завела;
  • цена — один лишний проигрыш дня на прогон (окно и сыгранный день не пересекаются, проигрышей всегда восемь).

На старте оси дня −1 нет, поэтому прогон дня 0 не отправляет ничего; первый слепок — дня 0 — уезжает прогоном дня 1 (исследование формата).

Случайность. Судьбу заказов бросает свой подпоток, ветвящийся по дню рождения заказа: слепок несёт семь дней рождения сразу, и судьбу каждого заказа обязан читать из его собственного дня. Вся судьба решается при рождении, поэтому слепок любого дня — чтение готовой судьбы, а не накопление состояния. Отклонено: дописывать броски в конец COMMERCE — правка заказа и правка торгового поведения стали бы одним рычагом; бросать состояние в подпотоке дня слепка — траектория заказа зависела бы от того, какие слепки снимали.

Запись на проводе

Контракт провода — на заказ один JSON-документ: деньги строками с двумя знаками, времена RFC 3339 в UTC с миллисекундами, items обычным массивом — целиком описан мастер-спекой (раздел 2); основания, отклонённые варианты и проверка разбора — в исследовании формата (резолюция развилки «Форма записи слепка на проводе»).

Сверх контракта здесь живёт одно правило: порядок строк внутри слепка — порядок рождения заказов, он же возрастание order_id. Детерминизм даёт его даром, а хешу слепка в описи нужен именно названный порядок.