docs(orders): собран черновик спеки второго источника

Зачем: решения семи развилок карты #69 жили только резолюциями тикетов;
перед вычитанием и приёмкой этапу 3 нужна цельная картина одним документом.

Что: docs/specs/2026-08-16-orders.md — черновик спеки заказов бэкенда из
резолюций #70–#74, #80, #81, с отклонёнными вариантами и доводами.
Расхождения с принятым внесены тем же коммитом: мастер-спека §2 (прямой
переход created → cancelled), §4 (механика класса C, чтение долей и
amount_delta), §5 (число пар из описи снято), §8 (строка слепка с хешем,
счётчики наблюдаемых классов, числа идентичности сняты); спека генератора §2
(заказная и событийная стороны вместо «расхождений и опозданий»); ADR 0008
(уточнён сдвиг отправки слепка); CONTEXT.md (термин «судьба заказа»).

Проверка: чтение; вычитание — #84, приёмка владельцем — #88.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 22:13:51 +03:00
co-authored by Claude Fable 5
parent ede1df765c
commit 815f500127
5 changed files with 567 additions and 14 deletions
+20 -13
View File
@@ -196,7 +196,7 @@ Ecommerce (заполнены только у торговых событий):
|---|---|---|
| `order_id` | String | номер заказа; равен клиентскому `purchaseID` |
| `user_id` | UInt64 | пользователь магазина — мост к склейке |
| `status` | String | `created``paid``cancelled` |
| `status` | String | `created``paid``cancelled`; неоплаченный отменяется прямым переходом `created``cancelled` |
| `created_at`, `updated_at` | DateTime64(3, 'UTC') | аудит строки в БД источника: создание и последнее изменение; не бизнес-время покупки и не время загрузки в ClickHouse |
| `items_total`, `discount`, `delivery`, `total` | Decimal(18,2) | деньги бэкенда — в Decimal |
| `items` | String | сырой текст массива позиций `[{sku, qty, price}]`, извлечённый из внешнего JSON |
@@ -233,7 +233,9 @@ Ecommerce (заполнены только у торговых событий):
заказов. Это остаётся носителем навыка «вложенный JSON в ClickHouse», не
превращая приём в преждевременную модель данных.
- Статусы держим все три: смена `created``paid` и есть причина «дыхания»
выручки внутри окна; сужение до двух — резервный срез 1.
выручки внутри окна; сужение до двух — резервный срез 1. На выходе из окна
заказ либо `paid`, либо `cancelled`: неоплаченного отменяют, «навсегда
`created`» не бывает ([спека заказов](2026-08-16-orders.md), раздел 3).
## 3. Каталог товаров
@@ -256,9 +258,9 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
| | Расхождение | Механика в генераторе | Ориентир доли |
|---|---|---|---|
| A | Отмена | заказ дошёл до `cancelled`, `purchase` остался | ~5% заказов |
| A | Отмена | заказ дошёл до `cancelled`, `purchase` остался | ~5% заказов — отменённые вообще, обе дороги отмены вместе |
| B | Потерянное событие | заказ есть, `purchase` не доехал | ~3% |
| C | Дельта суммы | сверка приведена к сравнимой базе (`items_total`, не `total`); `amount_delta`только необъяснённый остаток после этого, и создаёт его генератор намеренно: деньги считаются целыми копейками, поэтому Float64 сам по себе не плывёт | ~12% |
| C | Дельта суммы | вычеркнутая позиция: товара не оказалось в наличии, у заказа на одну позицию меньше, чем в клиентских массивах. Сверка приведена к сравнимой базе (`items_total`, не `total`); `amount_delta`остаток, не объяснённый этим приведением, и сравнением позиций он объясняется — на этом стоит урок класса | ~12% |
| D | Дубль события | повторный `purchase` от обновления `/confirmation`: новый `WatchID` с тем же `purchaseID` — бизнес-дубль, не технический; дедуп ReplacingMergeTree его не съедает и не должен | ~2% |
Классы пересекаются — приоритет: `cancelled` > `lost_event` >
@@ -267,8 +269,8 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов
впервые появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»),
ориентир ~10%. Точные доли фиксируются при пересборке эталонного мира;
опись хранит точные счётчики по каждому классу расхождений (отмены,
потери, дубли).
опись хранит счётчики наблюдаемых классов после приоритета — в заказах и
только по дням с закрытым окном (раздел 8).
Не берём: сироту-фрод (`purchase` есть, а заказа не будет никогда) —
механически дублирует B.
@@ -300,8 +302,9 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
`uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
Константа мира: каждый двухкуковый покупатель делает минимум по одному
заказу с каждой куки — иначе вторая кука не попадает в карту соответствий
(она строится только из покупок) и лаба не воспроизводится. Опись
хранит число именно таких пар.
(она строится только из покупок) и лаба не воспроизводится. Число таких
пар растёт, пока мир едет, и в опись не кладётся
([спека заказов](2026-08-16-orders.md), раздел 5).
- Витрины разводят имена честно: **«посетители»** (`uniq(ClientID)`) и
**«известные пользователи»** (после склейки) — оба числа рядом в дашборде.
@@ -501,11 +504,15 @@ Kafka день переигрывается генератором заново:
карты #10) закрыта этим же ходом: версионируется опись.
Контрольные числа описи:
- заказная сторона: заказы и выручка по дням; опись хранит точные
счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
самопроверка лабы сверки;
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых
покупателей — лаба склейки получает самопроверку.
- по строке на каждый отправленный слепок — хеш байтов: побайтовое обещание
«слепок переснимается и даёт те же байты» опись сторожит наравне с днями;
- заказная сторона: заказы, выручка и счётчики наблюдаемых классов
расхождений после приоритета, в заказах — по дням с закрытым окном
(самопроверка лабы сверки; при N сыгранных днях таких дней N − 7);
- контрольные числа идентичности не заводятся: uniq известных пользователей
и число двухкуковых покупателей растут, пока мир едет, а счётчик без
названного читателя в опись не кладётся
([спека заказов](2026-08-16-orders.md), раздел 5).
Снимок вырастет против v1 (ecommerce-массивы, заказы) — размер проверить
при пересборке.