Files
clickstream-data-platform/docs/architecture/orders/fate.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

15 KiB
Raw Blame History

Судьба заказа и расхождения

Резолюция развилки «Расхождения A–D и опоздания: механика, доли и что обещано».

Судьба заказа

Рамка. Расхождение — не грязь и не шум, а часть мира: судьба заказа, решённая при его рождении и уложенная в окно K целиком.

Два подпотока дня. Заказная сторона — судьба заказа: исход, моменты, дельта, опоздание. Событийная сторона — порча событийного потока: потеря и дубль. Довод за разделение — различимость по описи: правка заказной механики не двигает хеши событий, правка событийной не двигает байты слепка, и по покрасневшим хешам видно, какую сторону трогали. Прежние имена DISCREPANCIES и LATECOMERS решения не переживают — они названы по классам витрины, а компонент называет часть мира; новые стороны занимают те же позиции. Заказная сторона названа при реализации — подпоток ORDERS на позиции 2; событийная получит имя вместе с содержимым на этапе 6, до тех пор позиция 3 занята мёртвым именем прежней нарезки. Отклонено: один компонент на всю судьбу — правка событийной механики молча меняла бы байты слепка; компонент на класс — пять имён под ручки калибровки, которые крутятся разом.

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

Путь по статусам. Три исхода, все внутри окна: оплачен; оплачен и отменён; не оплачен и отменён. Инвариант на выходе из окна: заказ либо paid, либо cancelled; created — только промежуточное состояние. За окном будущего у заказа нет, а заказ, навсегда застрявший в created, — модельная небрежность, которой в выгрузке живого магазина соответствия нет. Поэтому нового значения mismatch_class не нужно: cancelled покрывает обе дороги отмены (шестое значение занято awaiting_order).

Форма броска.

  1. Исход — таблица долей из трёх строк; доля неоплаченных пишется явной строкой, а не оставляется читателю складывать хвост в уме.
  2. Моменты — таблица целых весов «сколько часов от рождения — с каким весом», строки 0…143, плюс равномерная секунда внутри часа — чтобы разности времён аудита не давали точных равенств (урок правки #50).
  3. Моментов бросается всегда два, на полную длину дня; у одномоментных исходов второй выбрасывается. Где их два по существу, ранний считается оплатой — порядок выходит сортировкой, условной точки отсчёта не нужно.

143 часа — самый узкий край окна: у заказа, рождённого в конце суток, до последнего его слепка 144 часа. Таблица кончается там, где кончается окно у самого невезучего: вылезти нечему, сторожа не нужно. Цена — заказ, рождённый в начале суток, не использует почти сутки своего окна; в хвосте таблицы веса мизерные, в данных это не видно. Форма из #71 — «вес за краем окна означает „не оплачен никогда“» — этим отменена: такой заказ теперь отменяется, а край окна не выражается числом часов — иначе доля неоплаченных стала бы функцией часа покупки, и менти нашёл бы этот наклон первым же разрезом.

Доли. Ориентир мастер-спеки ~5% читается как доля отменённых вообще; как она делится между двумя дорогами — строки таблицы исходов. Точные числа — калибровка этапа 7; проверок вида «отмен от 4 до 6 процентов» не заводим.

Что из двух дорог видно. В dds.order дороги неразличимы — там последняя версия; различает их история версий: сырьё STG и физические версии ods.order_snapshot до фоновых слияний, а в витринах — выручка дня, которая сначала выросла, потом убыла. Полное различение не обещано: слепок — состояние на границе суток, и оплата с отменой в один день в сырье неразличимы; то же у сильно опоздавших, приехавших уже терминальными. Точную форму этого урока решает этап 4. Отклонено: отмена только после оплаты — неоплаченному некуда деться, кроме как остаться брошенным; мгновенная отмена при рождении — «дыхание» окна на отменах исчезает; отмен нет вовсе — страховочный срез 1, он в резерве.

Классы расхождений и опоздание

Классы и ориентиры долей — мастер-спека, раздел 4; здесь — механика каждого.

Дельта суммы (C) — вычеркнутая позиция. Товара не оказалось в наличии, позицию сняли: у заказа на одну позицию меньше, чем в клиентских массивах, а items_total меньше на её стоимость. Момента у неё нет — заказ приезжает урезанным во всех своих слепках: первый слепок снимается на границе суток, когда склад заказ уже собрал. Заказ из одной позиции дельты не получает — пустых заказов не бывает. Позиция выбирается равновероятно: корреляция со спросом на доле 1–2% статистически ненаблюдаема — менти платил бы за неё таблицей чисел мира, а увидеть не мог бы ничем. Доводы за вычёркивание: остаток — сотни рублей, он торчит в витрине сверки сам; расхождение объясняется сравнением позиций — разбором вложенного JSON и ARRAY JOIN, ровно тем навыком, ради которого позиции разбираются; история рассказывается словами без легенды про генератор. Отклонено: переоценка позиции и другое количество — дельта в десятки рублей, её надо захотеть заметить; чистая дельта без истории — тупик, объяснить нечем; врёт клиент, а не бэкенд — заказ у нас проекция той же корзины.

Потеря события (B) — точечная. Уходит строка purchase, просмотр /confirmation остаётся: события уезжают разными запросами, потерять один и сохранить другой — обычное дело. Единственный вариант, при котором потеря видна со стороны трекера: до подтверждения дошли сто, покупок девяносто семь.

Дубль события (D) — сюжетный. Обновление страницы шлёт и просмотр, и покупку. Довод не в связности легенды: точечный дубль ломал бы урок соседнего класса — разрыв воронки, на котором держится потеря, сжался бы втрое; при сюжетном разрыв снова равен доле потерь. purchaseID, суммы и позиции у дубля один в один — посчитал наивно, удвоил выручку. Дубль случается только там, где до следующего визита куки остаётся запас сверх таймаута: иначе сборка сессий у менти разошлась бы с VisitID — сломался бы эталон, ради которого VisitID в потоке лежит. Задержка дубля — секунды-минуты, короче таймаута визита, поэтому VisitID тот же. Полночь режет дубль парой — просмотр вместе с покупкой, по тому же правилу, что у подтверждения с торговым хвостом. Отклонено: обе точечные — дубль затирает урок потери; обе сюжетные — потеря перестаёт быть видна со стороны трекера.

Гарантия моста сильнее порчи. Назначенные планом покупки не теряются, и назначенные планом заказы не опаздывают: dds.identity_map строится из моста «purchase ↔ заказ», и выброшенное событие — как и заказ, не попавший ни в один снятый слепок, — уносит куку из карты. Это был бы отказ лабы склейки, а не расхождение в данных; менти различить не может.

Опоздание — заказ прячется от ранних слепков. created_at не подделывается — строка создана, когда заказ родился, — но в слепках дней d…d+δ−1 её нет, а с d+δ она появляется в том состоянии, до которого заказ дожил: отменённый на второй день и опоздавший на третий приедет в первом же своём слепке как cancelled — «выгрузка догоняет жизнь». Задержка — таблица весов из трёх строк: 0 на подавляющем весе, 1 и 2 — это и есть «D+1/D+2» мастер-спеки. Меряется она в днях снятия слепка, а не отправки: иначе сдвиг отправки удвоился бы, и обещанные D+1/D+2 стали бы D+2/D+3. Дальше таблица не идёт: заказ с δ = 6 приехал бы ровно в одном слепке, и обещание «пропущенный день ничего не ломает» на нём перестало бы быть верным; при δ ≤ 2 у всякого заказа слепков не меньше пяти. Легенда: заказ ушёл в ручную обработку и попал в выгрузку позже. Отклонено: сдвиг created_at — подделка аудита источника: день создания строки разошёлся бы с днём покупки, чьё равенство держит синхронная модель (откуда берётся заказ), заказ уехал бы в чужую партицию, и «выручка дня D» перестала бы отвечать покупкам дня D — сломалась бы та самая сверка, ради которой всё строится.

Пересечения. Броски независимы, пересечения выходят арифметикой, приоритет мастер-спеки работает по-настоящему. Исключений два, и оба названы выше: дубль решается только у выживших покупок, а назначенное планом не теряется и не опаздывает — дельта и отмена ему разрешены, моста они не рвут. Следствие для калибровки: брошенная доля и наблюдаемая в сверке — разные числа (часть заказов забирают победители по приоритету, часть у класса C недоступна — однопозиционных заказов больше половины). Отклонено: один класс на заказ — приоритет в SQL стал бы мёртвой веткой, которую менти читает как живую.