feat(generator): торговые события — корзина, покупка, сырой ecommerce

- Зачем:
  - клиентская сторона мира становится целой: без add_to_cart и purchase
    в данных нет ни таксономии событий, ни вложенного JSON, ни денег,
    а метка «покупатель» из плана состава ни на что не влияла (#40).
- Что:
  - добавлен модуль commerce: корзина шире заказа, номер заказа вида
    ГГГГММДД-NNNN, промокод без скидки в сумме, сырой ecommerce через orjson;
  - метка покупателя получила два рычага — долгую жизнь куки в плане и
    свою воронку в дне; CART_PERCENT опущен с 8 до 6, чтобы конверсия
    мира осталась около 2%;
  - часть цен каталога получила копейки: productPrice округляется форматом,
    purchaseRevenue несёт точную сумму — расхождение живёт внутри события;
  - граница суток забирает страницу подтверждения вместе с её покупкой:
    потерь на клиентской стороне этот этап не заводит;
  - решения и перемеренные числа мира записаны в спеку генератора, §9.
- Проверка:
  - make lint && make typecheck && make test — 383 passed (было 353).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 19:13:30 +03:00
co-authored by Claude Opus 5
parent bfbaa96696
commit 722dbe22b7
19 changed files with 1591 additions and 210 deletions
+62 -3
View File
@@ -82,6 +82,15 @@ BUYER_PERCENT = 5
# этом стоит лаба про склейку личности.
PAIRED_BUYER_PERCENT = 15
# Первый рычаг метки покупателя: помеченный дольше живёт и чаще возвращается.
# Одноразовым он бывает много реже прочих, а возвращается вдвое чаще —
# отсюда кука, которая ходит неделями. Без этого рычага «постоянный
# покупатель» в данных не читается: метки в событии нет и не будет
# (кликстрим анонимен), а кука живёт меньше двух визитов за снимок — второй
# покупке негде случиться (спека генератора, раздел 9).
BUYER_ONE_SHOT_PERCENT = 35
BUYER_RETURN_COUNT_WEIGHTS = (10, 11, 12, 12, 11, 10, 9, 7, 5, 4, 3, 2)
# --- Числа дня: суточная волна, визиты, воронка ---------------------------
# Суточная волна буднего дня: проценты от среднего часа, от 00 до 23 часов
@@ -111,7 +120,7 @@ VISITS_PER_ACTIVE_DAY_WEIGHTS = (70, 22, 8)
# Длина визита в страницах: веса для 1, 2, 3 … страниц. Первая доля — отказы
# (посмотрел одну страницу и ушёл), дальше затухающий хвост. В среднем ≈4,8
# страницы: вместе с числом визитов это ~45 тыс. pageview в средний день,
# и до ~50 тыс. добирают торговые события (#40).
# и до ~50 тыс. добирают торговые события.
VISIT_PAGES_WEIGHTS = (250, 150, 120, 100, 88, 78, 68, 58, 50, 42, 35, 28, 22, 16)
# Таймаут визита: пауза дольше этой рвёт визит надвое. Правило резки, по
@@ -128,9 +137,59 @@ LONG_PAUSE_SECONDS = (300, 1500)
LONG_PAUSE_PERCENT = 12
# Воронка: доля визитов, дошедших до корзины, и доли следующих шагов от
# предыдущего. Произведение — конверсия визита в оформленный заказ: 2%
# предыдущего. Произведение — конверсия визита в оформленный заказ
# (спека генератора, раздел 9). Гарантированные планом заказы двухкуковых
# пар проходят воронку целиком независимо от этих долей.
CART_PERCENT = 8
CART_PERCENT = 6
CHECKOUT_OF_CART_PERCENT = 45
CONFIRMATION_OF_CHECKOUT_PERCENT = 55
# Второй рычаг метки покупателя: помеченный отличается на обоих шагах
# воронки — и до корзины доходит чаще, и бросает её реже. В жизни
# различаются оба: кто пришёл смотреть, тот и кладёт реже, и до конца
# доводит реже; один шаг дал бы половину картины. Шаг подтверждения общий:
# оплата — про магазин, а не про склонность покупать.
BUYER_CART_PERCENT = 18
BUYER_CHECKOUT_OF_CART_PERCENT = 60
# --- Числа торговых событий: корзина, заказ, деньги ------------------------
# Торговое событие встаёт на несколько секунд позже своей страницы: корзина
# позже карточки, покупка позже подтверждения. Задержка короче самой
# короткой паузы между страницами — иначе событие корзины обогнало бы
# страницу, на которой посетитель его нажал.
TRADE_DELAY_SECONDS = (2, 8)
# Доля позиций корзины, которые остались брошенными: заказ уже корзины.
# Иначе событие корзины не рассказывало бы ничего сверх покупки — заказ был
# бы её точной копией, и сравнивать было бы нечего. Пустым заказ не бывает:
# если брошены все позиции, одна остаётся (спека генератора, раздел 9).
ABANDONED_POSITION_PERCENT = 15
# Сколько штук одного товара берут: веса для 1, 2, 3 штук. Обычно одна,
# изредка две-три — непродовольственная розница.
ITEM_QUANTITY_WEIGHTS = (85, 11, 4)
# Валюта магазина: один регион присутствия — одна валюта.
CURRENCY = "RUB"
# Доля заказов с промокодом.
COUPON_PERCENT = 20
# Промокоды: код и скидка в процентах. Таблица — число мира, а не выдумка
# бэкенда: этап 3 берёт её готовой и обязан дать заказу с кодом скидку,
# иначе данные соврут. В клиентскую выручку скидка не входит — код на сайте
# знает корзину, а не итог расчёта (спека генератора, раздел 9). Цифры в
# коде — те же проценты: скидка в рублях потребовала бы второго правила
# чтения таблицы, а вместе с ним и второго вида скидки на стороне бэкенда.
COUPONS = (
("VESNA10", 10),
("DOMASHNIY5", 5),
("PERVYY15", 15),
("UYUT7", 7),
)
# Цели счётчика: корзина и покупка. Цели дублируют торговые события — в бою
# так и бывает (мастер-спека, раздел 1.2).
GOAL_CART_ID = 42150001
GOAL_PURCHASE_ID = 42150002