docs(generator): решения #40 — торговые события
- Зачем:
- перед реализацией торговых событий грилинг закрыл шесть развилок; без
записи решения и отклонённые варианты потерялись бы, а часть из них
выходит за границы тикета и меняет постановку.
- Что:
- в раздел 9 спеки генератора добавлен блок «Решено при исполнении #40»:
корзина шире заказа, метка покупателя становится значимой, место
торговых событий, номер заказа, промокод, единицы денег, разная длина
массивов purchase* и product*, orjson, хвост покупки у границы суток.
- в раздел 8 добавлены хвосты этапам 3 и 4: скидка по промокоду и
намеренное расхождение сумм.
- в словарь добавлены «покупатель» и «торговое событие».
- Проверка:
- чтением: docs/specs/2026-08-01-generator.md, разделы 8 и 9; CONTEXT.md.
This commit is contained in:
+12
@@ -63,11 +63,23 @@ _Избегать_: визит (визит — про сессию)
|
|||||||
устройстве. Держит их план состава, разворачивает в поля события
|
устройстве. Держит их план состава, разворачивает в поля события
|
||||||
день-функция; у двухкуковой пары город один на две куки, устройства разные.
|
день-функция; у двухкуковой пары город один на две куки, устройства разные.
|
||||||
|
|
||||||
|
**Покупатель**:
|
||||||
|
Человек, которого план состава пометил склонным покупать. Метка значима:
|
||||||
|
помеченный доходит до заказа заметно чаще прочих, но и непомеченный иногда
|
||||||
|
покупает. Из покупателей отбираются двухкуковые пары.
|
||||||
|
_Избегать_: «покупатель» про того, кто купил в конкретный день — это визит
|
||||||
|
с заказом.
|
||||||
|
|
||||||
**День-функция**:
|
**День-функция**:
|
||||||
Функция (зерно, номер дня), выдающая упорядоченный поток событий этих
|
Функция (зерно, номер дня), выдающая упорядоченный поток событий этих
|
||||||
модельных суток. Состояния между днями нет: день D не зависит от того,
|
модельных суток. Состояния между днями нет: день D не зависит от того,
|
||||||
прожиты ли дни до него.
|
прожиты ли дни до него.
|
||||||
|
|
||||||
|
**Торговое событие**:
|
||||||
|
Событие корзины или покупки — отдельная строка потока, а не просмотр
|
||||||
|
страницы. Садится на ту страницу, где случилось: корзина — на карточку
|
||||||
|
товара, покупка — на страницу подтверждения заказа.
|
||||||
|
|
||||||
**Каталог товаров**:
|
**Каталог товаров**:
|
||||||
`data/catalog/products.csv` — общий справочник генератора и словаря
|
`data/catalog/products.csv` — общий справочник генератора и словаря
|
||||||
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
|
ClickHouse. Форма файла решена, длина — нет: строки дописываются.
|
||||||
|
|||||||
@@ -390,6 +390,13 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
|||||||
|
|
||||||
## 8. Хвосты следующим этапам
|
## 8. Хвосты следующим этапам
|
||||||
|
|
||||||
|
- **Этап 3 (заказы бэкенда)**: заказу, у которого в клиентском событии стоит
|
||||||
|
промокод, бэкенд обязан дать скидку — иначе данные соврут. Номер заказа обе
|
||||||
|
стороны берут один и тот же (раздел 9).
|
||||||
|
- **Этап 4 (сверка)**: расхождение сумм само не появится — цены каталога
|
||||||
|
кратны рублю, и `Float64` на таких числах не плывёт. Класс `amount_delta`
|
||||||
|
придётся создавать намеренно, вероятностью в генераторе, — мастер-спека
|
||||||
|
(раздел 4) это и допускает.
|
||||||
- **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг
|
- **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг
|
||||||
по расписанию (~раз в 24 минуты); генератор не дорабатывается.
|
по расписанию (~раз в 24 минуты); генератор не дорабатывается.
|
||||||
- **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт;
|
- **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт;
|
||||||
@@ -542,6 +549,73 @@ pytest-тест с маркером `perf` и таймаутом-обрубан
|
|||||||
`/confirmation`, а перед корзиной у него всегда есть карточка. Своей
|
`/confirmation`, а перед корзиной у него всегда есть карточка. Своей
|
||||||
случайности #39 у #40 не занимает: подпоток `COMMERCE` не тронут.
|
случайности #39 у #40 не занимает: подпоток `COMMERCE` не тронут.
|
||||||
|
|
||||||
|
Решено при исполнении #40 (2026-08-02) — решения владельца до реализации:
|
||||||
|
|
||||||
|
- **Корзина шире заказа.** Посетитель кладёт в корзину товары тех карточек,
|
||||||
|
которые открывал в этом визите, а покупает не всё: около 15% позиций
|
||||||
|
остаются брошенными. Иначе событие корзины не рассказывало бы ничего сверх
|
||||||
|
покупки — заказ был бы её точной копией, и сравнивать было бы нечего.
|
||||||
|
Замерено на каноническом мире до правки воронки (ниже): у покупающего
|
||||||
|
визита 2,1 карточки, в заказе
|
||||||
|
выходит 1,9 позиции, однопозиционных заказов около половины. Отклонено:
|
||||||
|
*заказ всегда из одного товара* — список позиций как урок умирает, а на нём
|
||||||
|
стоит единственный носитель навыка «вложенный JSON» (мастер-спека,
|
||||||
|
раздел 2); *досыпать в корзину товары, которых посетитель не открывал* —
|
||||||
|
ломает шов #39 ровно там, ради чего он строился; *бросать много* (40% и
|
||||||
|
выше) — две трети заказов схлопываются в один товар.
|
||||||
|
- **Покупатель перестаёт быть словом.** Метка плана — «этот человек склонен
|
||||||
|
покупать», 5% людей когорты — до сих пор ни на что не влияла: кто купит,
|
||||||
|
решал одинаковый для всех бросок в дне. Свойство человека в данных не
|
||||||
|
читалось, постоянных покупателей в мире не было. Теперь метка правит
|
||||||
|
воронкой: помеченный доходит до заказа заметно чаще прочих, но и
|
||||||
|
непомеченный иногда покупает. Общее число покупок и общая доля визитов с
|
||||||
|
корзиной остаются прежними — меняется лишь то, кто эти визиты совершает;
|
||||||
|
точные доли — калибровка при реализации, требование сторожит тест.
|
||||||
|
Уточнение к разделу 1: вольные покупки по-прежнему решает день, но
|
||||||
|
случайность в них больше не одинакова для всех. Плата
|
||||||
|
названа: воронка живёт в коде #39, поэтому правка выходит за исходные
|
||||||
|
границы тикета и сдвигает трафиковый поток. Сейчас это ничего не стоит —
|
||||||
|
манифест ещё не собран (#42); позже обошлось бы его пересборкой.
|
||||||
|
Отклонено: *оставить как есть* — числа сходятся, но «покупатель» остаётся
|
||||||
|
именем без следа в данных; *покупают только помеченные* — 5% людей дают
|
||||||
|
около 40 заказов в день вместо 220; вернуть их можно, лишь подняв долю
|
||||||
|
покупателей до 10–30% людей, а с ней вырастет число двухкуковых пар и
|
||||||
|
измеренные числа #38 придётся пересчитывать.
|
||||||
|
- **Событие корзины садится на карточку товара**, событие покупки — на
|
||||||
|
страницу подтверждения. В жизни событие корзины шлётся нажатием кнопки на
|
||||||
|
карточке, а не открытием страницы корзины; при нескольких товарах в заказе
|
||||||
|
иначе его и не разложить — событий столько, со скольких карточек положили.
|
||||||
|
- **Номер заказа читаемый**: день модельного времени и порядковый номер
|
||||||
|
покупки в этом дне. Магазины так и нумеруют, и по номеру сразу видно день.
|
||||||
|
Он же — `order_id` бэкенда: обе стороны получают один номер, по нему
|
||||||
|
соединяется сверка (мастер-спека, раздел 4).
|
||||||
|
- **Промокод в событии есть, в сумме его нет.** Часть заказов уходит с
|
||||||
|
промокодом, но выручка, которую шлёт клиент, — сумма позиций без скидки и
|
||||||
|
доставки. Так и в жизни: код на сайте знает корзину, а не итог расчёта. Это
|
||||||
|
ровно то расхождение с бэкендом, которое менти будет раскапывать
|
||||||
|
(мастер-спека, раздел 4). Отклонено: *промокода нет никогда* — колонка
|
||||||
|
формата осталась бы мёртвой.
|
||||||
|
- **Деньги: копейки внутри, рубли в колонках, дробь только в выручке.**
|
||||||
|
Генератор считает целыми копейками; цена товара в событии — целые рубли,
|
||||||
|
как у Метрики (`productPrice` там `Int64`); дробное число одно —
|
||||||
|
`purchaseRevenue` типа `Float64`. Деление на сто верно, лишь пока цены
|
||||||
|
каталога кратны рублю: это сторожит тест, иначе копейки утекали бы молча.
|
||||||
|
- **Массивы `purchase*` и `product*` разной длины между собой.** Внутри своей
|
||||||
|
группы длина общая: `purchase*` — по элементу на заказ (у нас всегда один),
|
||||||
|
`product*` — по элементу на товар. Мастер-спека (раздел 1) говорит о
|
||||||
|
«параллельных массивах одной длины», и прочесть это как «все массивы
|
||||||
|
события одной длины» легко — поэтому в описании выгрузки сказано прямо.
|
||||||
|
- **Сырой `ecommerce` собирает канонический сериализатор.** Поле — строка
|
||||||
|
JSON внутри события, и собирать её руками значит писать второй
|
||||||
|
сериализатор со своим экранированием. Зависимость `orjson`, названная
|
||||||
|
рабочим выбором в разделе 4, появляется здесь, а не в #41.
|
||||||
|
- **Хвост покупки не выходит за полночь.** Событие покупки встаёт на
|
||||||
|
несколько секунд позже просмотра страницы подтверждения, а граница суток
|
||||||
|
режет всё, что за неё вышло. Визит с обещанным планом заказом уже подвинут
|
||||||
|
так, чтобы уместиться в сутки (#39), но подвинут по страницам — торгового
|
||||||
|
хвоста в том расчёте не было. Хвост прижимается к последней секунде суток;
|
||||||
|
иначе гарантия двухкуковых пар порвалась бы молча.
|
||||||
|
|
||||||
Остаётся открытым, за тикетами:
|
Остаётся открытым, за тикетами:
|
||||||
|
|
||||||
- интерфейс запуска генератора (CLI / цели make) и как он делит режимы
|
- интерфейс запуска генератора (CLI / цели make) и как он делит режимы
|
||||||
|
|||||||
Reference in New Issue
Block a user