# Финансовая модель селлера Wildberries: архитектура и инструкция для ИИ

> Автор: Евгений Змерзлый, материалы выступления «Финансы селлера» — https://zmerzlyy.ru/speech/ · вопросы и обратная связь: Telegram https://t.me/zmerzlyy
> Версия 1.1, октябрь 2026. Методы API Wildberries сверены с официальными спецификациями на 16.10.2026.
> Все числа в примерах круглые и условные. Реальных данных какого-либо продавца в файле нет.
> Пометка **⚠️ не подтверждено** означает, что пункт выведен из практики или косвенных источников. ИИ должен проверить его на данных владельца.

---

## Для человека: что это и зачем

Деньги с Wildberries приходят не в день продажи. Между заказом покупателя и деньгами на расчётном счёте проходит 4–6 недель. Сначала покупатель забирает товар или отказывается от него. Потом ВБ начисляет выручку за вычетом комиссии, логистики, хранения, рекламы и штрафов. Раз в неделю ВБ формирует отчёт, через две недели открывает кнопку «Вывести», и после этого платёж идёт ещё от одной до трёх недель. А фабрике, логистам, сотрудникам и налоговой вы платите в это же время.

Если разложить всё это по календарю, **кассовый разрыв виден за недели**, а не в день, когда нечем заплатить поставщику.

В этом файле описано, как устроена такая модель:

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

**Как пользоваться.** Отдайте этот файл ИИ-ассистенту, который умеет запускать код, и напишите: «Построй мою финмодель по этому файлу». Ассистент попросит у вас доступы и данные (список в разделе 2.2), скачает их и всё посчитает. Вы получите таблицу будущих поступлений по датам, прогноз кассы и список предупреждений.

Без ИИ формулы из раздела 7 можно перенести в Google Таблицы или Excel, а данные взять из выгрузок личного кабинета.

---

## Оглавление

1. Кратко для ИИ
2. Как пользоваться этим файлом (для ИИ): протокол, что запросить, безопасность
3. Глоссарий
4. Архитектура модели
5. Получение данных: токен, методы API, план выгрузки
6. Нормализация данных и ловушки
7. Формулы и псевдокод
8. Параметры: значения по умолчанию и калибровка
9. Валидация (бэк-тест)
10. Что показать владельцу
11. Ограничения
- Приложение А. Что не подтверждено в API

---

## 1. Кратко для ИИ

- **Цель:** дневной прогноз входящих денег от ВБ по календарным датам и прогноз кассы с оплатами поставщику. Горизонт: 90–180 дней.
- **Цепочка расчёта:**
  `план заказов → выкупы по кривой выкупа → выручка до СПП → удержания ВБ и реклама → начислено на баланс → недельный отчёт → кнопка «Вывести» → деньги в банке → касса`.
- **Факт главнее модели.** Иерархия источников: банк, затем «Итого к оплате» закрытого отчёта, затем строки недельного отчёта, затем дневные отчёты, и только потом модель (раздел 4.6).
- **Обязательная проверка до прогноза:** собери «Итого к оплате» из строк отчёта и сверь с `bankPaymentSum` каждого отчёта с допуском 1 ₽ (раздел 7.1). Пока не сошлось, не показывай юнит-экономику по статьям и дневные начисления из строк. Прогноз выплат по шапкам (`bankPaymentSum`) строить можно, с предупреждением «формула строк не сверена».
- **Главные ловушки:** не суммировать дневные и недельные отчёты; неделя на стыке месяцев приходит двумя отчётами, а платёж по ней один; ВБ переименовывает операции («Логистика» стала «Доставкой»); ответ HTTP 204 не означает «данных нет навсегда»; сегодняшний день ещё не факт; время московское.
- **Безопасность:** токен только на чтение, минимум категорий. Токен не просить в чат, не хранить и не печатать. После работы владелец удаляет токен.
- **Честность:** у каждого числа должен быть источник: факт, калибровка, значение по умолчанию или слова владельца. Допущения нужно показывать явно.

---

## 2. Как пользоваться этим файлом (для ИИ)

### 2.1 Протокол работы

Работай по шагам. На каждой отметке **СТОП** покажи владельцу промежуточный результат и дождись подтверждения.

| Шаг | Что делать | Результат |
|---|---|---|
| 0. Подготовка | Прочитай файл целиком. Определи свою среду: можешь ли ты запускать код и ходить в интернет. От этого зависит способ доступа (раздел 2.4). | выбранный способ доступа |
| 1. Запрос | Отправь владельцу чек-лист (2.2) по шаблону (2.3). Для старта нужен минимум: доступ к данным ВБ, поступления от ВБ по выписке за 6 месяцев (минимум — с 20.07.2026, с начала текущего регламента выплат), остаток денег на сегодня. Остальное можно получить позже и пока работать на допущениях. | доступы и файлы |
| 2. Выгрузка | Проверь токен через `/ping` и пробный вызов `sales-reports/list` (5.4, шаг 1), затем выгрузи данные по плану из раздела 5.4. Сырые ответы сохраняй без изменений, отдельно от обработанных. | сырые данные |
| 3. Очистка | Нормализуй данные по разделу 6. Построй справочник значений `sellerOperName`, `docTypeName`, `bonusTypeName` с суммами по каждому. **СТОП:** покажи владельцу непонятные и новые значения. | чистые таблицы |
| 4. Сверка | Собери «Итого к оплате» из строк и сверь с `bankPaymentSum` (7.1). **СТОП**, если расхождение больше 1 ₽ хотя бы в одном отчёте. Покажи владельцу расхождения. Если после перебора кандидатов (7.1) не сошлось, продолжай по шапкам (см. раздел 1) и отметь это в предупреждениях. | подтверждённая формула |
| 5. Расчёт | Посчитай кривую выкупа (7.2–7.3), кривую отказов (7.4), юнит-экономику (7.5), ставку удержаний и ДРР (7.6), лаги выплат по банковской выписке (7.9). | параметры модели |
| 6. Прогноз | Построй план заказов (7.7), выкупы и начисления по дням (7.8), календарь выплат (7.9), долг ВБ (7.10), кассу и разрывы (7.11). | прогноз |
| 7. Валидация | Проведи бэк-тест на последних 8–12 закрытых неделях (раздел 9). Если допуски не выполнены, найди причину и не показывай прогноз как надёжный. | метрики качества |
| 8. Отчёт | Покажи результат в формате раздела 10: ключевые числа, таблицы, предупреждения, допущения, вопросы владельцу. | отчёт владельцу |
| 9. Уборка | Напомни владельцу удалить токен (не только перевыпустить). Спроси владельца, где хранить сырые файлы. Рекомендуй держать их у него на компьютере: ВБ хранит заказы только 90 дней, а без архива регулярный режим теряет историю. С серверов и из облачных хранилищ ИИ файлы удали. | — |

**Регулярный режим** (если модель обновляется каждую неделю). Каждый понедельник после прихода недельного отчёта:
1. догрузи новые данные;
2. перечитай историю: поздние возвраты меняют закрытые дни;
3. сравни прогноз прошлой недели с фактом;
4. перекалибруй параметры;
5. обнови прогноз;
6. сообщи владельцу, что изменилось и почему.

### 2.2 Что запросить у владельца (чек-лист)

| № | Что | Зачем | Обязательно? |
|---|---|---|---|
| 1 | Доступ к API ВБ только на чтение (способ — раздел 2.4) или выгрузки из ЛК | заказы, отчёты реализации, реклама, баланс | да |
| 2 | Банковская выписка, только поступления от ВБ за 6 месяцев (минимум — с 20.07.2026, с начала текущего регламента выплат): дата, сумма, счёт (CSV или xlsx) | фактические даты денег и лаг «заявка → деньги»; в API этого нет | да |
| 3 | Остаток денег на сегодня по всем счетам | стартовая точка кассы | да |
| 4 | Себестоимость по артикулам в ₽ за штуку, с доставкой до склада (выгрузка из МойСклад, 1С или таблицы) | юнит-экономика, маржа | желательно |
| 5 | Партии в производстве и в пути: артикул, количество, сумма, даты заказа, отгрузки и прихода (план или факт) | будущие остатки и оплаты поставщику | желательно |
| 6 | Условия оплаты поставщику (транши, сроки) и фактические оплаты за 3–6 месяцев | график оттоков | желательно |
| 7 | Расходы по месяцам: зарплаты, аренда, фулфилмент, сервисы; налоговый режим и ставка; кредиты с графиком платежей | полная касса | желательно |
| 8 | План продаж: заказы в день или ожидаемый рост, цены, акции, сезонность, новые товары | прогноз (иначе протяжка факта) | желательно |
| 9 | Скриншот истории заявок на вывод из ЛК (последние 8–10 заявок со статусами) | калибровка задержек выплат | по возможности |
| 10 | Неснижаемый остаток денег: ниже какой суммы считать разрыв | порог тревоги | по возможности |
| 11 | Работает ли продавец через посредника (агента или комиссионера) и на каких условиях; сколько у него кабинетов | правильная база удержаний | если применимо |

**Никогда не проси** логин, пароль или SMS-код от личного кабинета, данные карт и банковские пароли. Не проси токен с правом записи и категорию «Пользователи».

### 2.3 Шаблон сообщения владельцу

```text
Здравствуйте! Чтобы спрогнозировать поступления от Wildberries по датам и увидеть
кассовый разрыв заранее, мне нужны данные. Ниже — что и как подготовить.

1. Доступ к данным ВБ (только чтение, менять в кабинете я ничего не смогу).
   В личном кабинете: Профиль → «Интеграции по API» → «+ Создать токен» →
   «Для интеграции вручную» → тип «Персональный» → уровень доступа «Только чтение» →
   категории: «Статистика», «Финансы», «Аналитика».
   Второй токен (по желанию, для рекламы): «Продвижение» (+ «Контент», «Цены и скидки»).
   Токен показывается один раз, живёт 180 дней.
   ВАЖНО: не присылайте токен в чат. Я подготовлю скрипт выгрузки — вы запустите
   его на своём компьютере, токен останется у вас, а мне вы передадите готовые файлы.
   [Если я работаю на вашем компьютере: скрипт сам спросит токен скрытым вводом,
   или задайте его командой, которую я дам. В чат и в текстовые файлы токен
   не вставляйте.]

2. Выписку из банка: только поступления от Wildberries за 6 месяцев (дата, сумма).
3. Остаток денег на сегодня по всем счетам.
4. Себестоимость по артикулам (выгрузка из МойСклад/1С/таблицы): артикул → ₽/шт.
5. Товар в производстве и в пути: артикул, количество, сумма, даты отгрузки и прихода.
6. Условия оплаты поставщику (например, 20% предоплата, 30% при приходе, 50% через 45 дней)
   и фактические оплаты за последние месяцы.
7. Ежемесячные расходы, налоговый режим, кредиты.
8. План: сколько заказов в день ожидаете, цены, акции, сезонность.
9. По возможности — скриншот истории заявок на вывод денег из личного кабинета.

Если чего-то нет — не страшно: начну с того, что есть, и помечу допущения.
После работы токен лучше удалить в «Интеграциях по API» (кнопка «Дублировать»
старый токен не отзывает — его нужно удалить отдельно).
```

### 2.4 Способы доступа к API (с учётом правил ВБ)

По правилам ВБ персональный токен **нельзя передавать третьим лицам и использовать в облачных сервисах**. По справке ВБ такой токен из облака может просто не работать технически. Облачный чат-ассистент формально и есть облачный сервис. Поэтому выбирай способ по своей среде:

| Способ | Когда | Как |
|---|---|---|
| **А. Скрипт у владельца** (рекомендуется) | ИИ работает в облаке (чат) | ИИ пишет скрипт выгрузки, владелец запускает его на своём компьютере. На выходе получаются файлы, владелец передаёт их ИИ. Токен не покидает компьютер владельца. Требования к скрипту: один файл на Python 3 только с `requests`. Токен спрашивается скрытым вводом (`getpass`) или читается из переменной окружения и нигде не печатается. Скрипт возобновляемый: продолжает с последней сохранённой страницы (`rrdId`, `offset`, окно дат). Печатает прогресс. На выходе — одна папка `wb_export/` с файлами `report_headers.json`, `report_rows_weekly.jsonl`, `report_rows_daily.jsonl`, `orders.jsonl`, `order_feed.jsonl`, `funnel.json`, `ads_upd.json`, `ads_payments.json`, `balance.json`, `stocks.json`, `cards.json`, `prices.json` и `manifest.json` (периоды, число строк, ошибки). Вместе со скриптом дай владельцу пошаговую инструкцию запуска для его ОС. |
| **Б. Локальный агент** | ИИ запущен на компьютере или сервере владельца и умеет выполнять команды | Агент читает токены из переменных окружения `WB_TOKEN` (статистика, финансы, аналитика) и `WB_TOKEN_ADS` (продвижение). Токен не выводится в чат, логи и командную строку процессов: запросы делай из кода (Python `requests`, заголовок из `os.environ`), а не через `curl -H …`. Наличие токена проверяй так: `test -n "$WB_TOKEN"`, никогда не через `echo`. Владельцу предложи задать переменную без записи в историю: `read -s WB_TOKEN && export WB_TOKEN` (macOS/Linux) или запусти скрипт, который спросит токен скрытым вводом. |
| **В. Без API** | токена нет или его нельзя выпустить | Владелец выгружает из ЛК детализацию еженедельных отчётов реализации (xlsx) за 6–12 месяцев, отчёты по рекламе и выгрузку воронки продаж по дням или неделям (раздел «Аналитика» → воронка продаж): из неё берутся заказы. Этого хватит для выкупов, удержаний и начислений. Лаг выплат берётся из выписки. |
| Базовый токен | крайний случай | Лимиты на порядки ниже: детализация доступна 2 раза в сутки, список отчётов недоступен совсем. Для первичной выгрузки он не годится. |

### 2.5 Правила безопасности

1. **Только чтение.** Токен с уровнем «Только чтение», минимум категорий (ВБ советует не больше трёх в одном токене). Не нужны: «Пользователи», «Маркетплейс», «Чат с покупателем», «Вопросы и отзывы».
2. **Токен не попадает в чат, файлы проекта, логи, URL, историю команд и аргументы процессов.** Только переменная окружения или защищённое хранилище владельца. Заголовок запроса: `Authorization: <токен>`.
3. **Не храни токен после работы.** Владелец удаляет его в «Интеграциях по API». Кнопка «Дублировать» создаёт новый токен, но старый не отзывает: его нужно удалить отдельно. Срок жизни токена 180 дней, продлить его нельзя.
4. **Данные владельца коммерчески чувствительны.** Не передавай их третьим сервисам. Сырые данные храни у владельца, у себя — только на время работы. В отчёт попадают агрегаты, а не построчные выгрузки.
5. **Не трать лимиты без пользы.** Лимит общий на профиль продавца, и его делят все сервисы, подключённые к кабинету. Агрессивные повторы после ошибки 429 могут временно заблокировать API для всего кабинета, включая сервисы владельца (раздел 5.2). Если у владельца уже работают другие сервисы на тех же методах, спроси расписание и разведи запросы по минутам.
6. **Ничего не делай в кабинете:** не меняй цены, не создавай заявки, не трогай рекламу. Модель только читает данные.
7. **Минимум данных.** Выгружай только поля из списка в 5.4 (шаг 3). ФИО и ИНН перевозчиков, партнёров и покупателей-юрлиц (`rebillLogisticOrg`, `ppvzSupplierName`, `ppvzSupplierInn`, `b2bCustomerTin`, `buyerTaxRegistrationReasonCode`), адреса ПВЗ (`ppvzOfficeName`), коды маркировки (`kiz`) и города доставки (`destinationCity` в ленте заказов) модели не нужны: не запрашивай их или удаляй до передачи файлов ИИ.

### 2.6 Правила поведения при расчётах

- Деньги считай в `Decimal`, а не во `float`. В новых финансовых методах суммы приходят строками (`"forPay": "376.99"`).
- Идентификаторы (`reportId`, `rrdId`) могут быть больше 2^53. Парси их как строки или BigInt.
- Не выдумывай. Если данных нет, бери значение по умолчанию из раздела 8 и помечай его «допущение».
- Каждый показатель в отчёте сопровождай пометкой об источнике: `факт API`, `факт банка`, `калибровка за N недель`, `по умолчанию`, `со слов владельца`.
- Прогноз давай диапазоном (реалистичный и осторожный), а не одной точкой.
- Если факт противоречит модели, прав факт. Ищи причину в модели.

---

## 3. Глоссарий

| Термин | Значение |
|---|---|
| **Заказ** | Покупатель оформил покупку. Это ещё не деньги. 100 % воронки. |
| **Отмена** | Заказ отменён до выдачи покупателю. |
| **Невыкуп (отказ)** | Покупатель не забрал товар на ПВЗ или отказался при получении. Денег нет, товар едет обратно на склад. |
| **Выкуп (продажа)** | Покупатель забрал товар. В отчёте это строка «Продажа». |
| **Возврат** | Покупатель забрал товар, а потом вернул. В отчёте это строка «Возврат», сумма с обратным знаком. |
| **Чистый выкуп** | Выкупы минус возвраты. |
| **Доля выкупа** | Чистые выкупы когорты, делённые на её заказы. Считается по зрелым когортам (7.2). У ВБ `buyoutPercent` считается без товаров, которые ещё в доставке, поэтому цифры могут отличаться. `buyoutPercent` округлён до целого. |
| **Лаг выкупа** | Число дней от заказа до выкупа: `saleDt − orderDt`. |
| **Кривая выкупа p[L]** | Доля заказов, выкупленных ровно на L-й день после заказа. Сумма p[L] равна доле выкупа. |
| **Лесенка** | Упрощение кривой: две ступени, например 20 % на 5-й день и 7 % на 13-й. |
| **Когорта** | Все заказы одного дня (или недели). |
| **«Потеряшки»** | Заказы без исхода: не выкуплены и не вернулись на склад. Обычно доли процента или несколько процентов. |
| **srid** | Идентификатор позиции заказа. Есть и в заказах, и в отчёте реализации, по нему они связываются. В FBS совпадает с `rid`. |
| **nmId** | Артикул ВБ (карточка товара). |
| **СПП** | Скидка постоянного покупателя. Её финансирует ВБ, на выплату продавцу она не влияет. |
| **Цена до СПП** | `retailPriceWithDisc`: цена продавца со скидкой продавца. **База модели.** |
| **«ВБ реализовал»** | `retailAmount`: сумма, которую заплатил покупатель, то есть цена после СПП. |
| **К перечислению** | `forPay`: сумма продавцу за товар после комиссии ВБ. |
| **кВВ, комиссия** | Вознаграждение ВБ (`commissionPercent`, `vw`, `ppvzSalesCommission`). В отдельных строках может быть отрицательным: так ВБ компенсирует СПП. |
| **Эквайринг** | Плата за приём платежа (`acquiringFee`). |
| **Логистика (доставка)** | `deliveryService`: доставка до покупателя, а также обратная доставка при невыкупе и возврате. Примерно с сентября 2026 года операция называется «Доставка» (⚠️ в строках официально не подтверждено, см. 6.3). |
| **Хранение** | `paidStorage`: платное хранение на складах ВБ. |
| **Платная приёмка** | `paidAcceptance`: операции на приёмке, в рублях. |
| **Штрафы** | `penalty`. |
| **Удержания** | `deduction`: прочие удержания и выплаты (в шапке поле называется «Прочие удержания/выплаты»; отрицательное значение — выплата продавцу), в том числе реклама, оплаченная с баланса. |
| **Корректировка вознаграждения** | `additionalPayment`. |
| **ДРР** | Доля рекламных расходов. В модели считается от выручки выкупов. ДРР от заказов примерно равна ДРР от выкупов, умноженной на долю выкупа. |
| **Отчёт реализации** | Еженедельный отчёт ВБ за неделю с понедельника по воскресенье. Приходит в понедельник. Бывает двух типов: основной (`reportType = 1`) и по выкупам (`reportType = 2`). На стыке месяцев неделя приходит двумя отчётами (6.2). |
| **Дневной отчёт** | Детализация за один день (`period = daily`). Предварительная, после закрытия недели её заменяет недельный отчёт. |
| **Итого к оплате** | `bankPaymentSum`: сколько ВБ должен перечислить по отчёту. Уже за вычетом всего, включая рекламу с баланса. |
| **Баланс кабинета** | `current` (текущий баланс) и `for_withdraw` (доступно к выводу). Это снимок на момент запроса. |
| **Кнопка «Вывести» (заявка)** | Становится доступна через определённый срок после конца недели. Выводится вся сумма недели целиком, частичный вывод невозможен. |
| **Начислено на баланс** | Выкупы минус удержания ВБ минус реклама с баланса. Это ещё не деньги на счёте. |
| **Долг ВБ** | Начислено, но ещё не пришло в банк. В устойчивом режиме равен начислениям примерно за 4–5 недель. |
| **Ставка удержаний (take rate)** | Все удержания ВБ без рекламы в процентах от выручки до СПП: комиссия, логистика, эквайринг, хранение, приёмка, штрафы, прочее. |
| **Граница факта** | Последний день, по которому есть фактические данные. У заказов и у выкупов она разная. |
| **Кабинет (плоскость)** | Один кабинет ВБ или одно юрлицо со своими параметрами. |
| **FBW (FBO) / FBS** | Товар хранится на складе ВБ / товар хранится у продавца, ВБ его доставляет. |
| **Транш** | Часть оплаты поставщику: предоплата, второй платёж, постоплата. |
| **Опекс (операционные расходы)** | Постоянные расходы: зарплаты, аренда, сервисы. |
| **Кассовый разрыв** | День, когда прогнозный остаток денег опускается ниже порога. |

---

## 4. Архитектура модели

### 4.1 Слои

| Слой | Содержимое | Правило |
|---|---|---|
| L0. Источники | API ВБ, выгрузки ЛК, учётная система (МойСклад, 1С), банк, владелец (план) | — |
| L1. Сырые данные | ответы API и файлы как есть, с датой выгрузки | не изменяются, только дополняются |
| L2. Факты | нормализованные таблицы (раздел 5.7) | пересобираются из L1; канон заменяет предварительные данные |
| L3. Параметры | кривые, ставки, лаги, план; каждый параметр хранится ступенями с даты | калибруются на L2, ручные правки отмечаются |
| L4. Движок | дневная ось: товар → выкупы → начисления → выплаты → долг ВБ → склад → поставщик → касса | факт слева от границы, модель справа |
| L5. Выходы | календарь выплат, касса, разрывы, юнит-экономика, метрики качества | у каждого числа есть пометка об источнике |

### 4.2 Поток данных

```mermaid
flowchart LR
  subgraph SRC["Источники"]
    API["API ВБ: заказы, отчёты, реклама, баланс"]
    BANK["Банк: поступления от ВБ"]
    ACC["Учёт: себестоимость, партии, оплаты"]
    OWN["Владелец: план, расходы"]
  end
  API --> F["Факты (L2)"]
  BANK --> F
  ACC --> F
  F --> P["Параметры (L3): кривая выкупа, ставка удержаний, ДРР, лаги выплат"]
  OWN --> PLAN["План заказов"]
  PLAN --> E1["Выкупы по дням = заказы ⊛ кривая"]
  P --> E1
  E1 --> E2["Выручка до СПП"]
  E2 --> E3["− удержания ВБ − реклама с баланса"]
  E3 --> E4["Начислено на баланс"]
  E4 --> E5["Недельный отчёт (пн)"]
  E5 --> E6["Кнопка «Вывести» (+14 дн → пн)"]
  E6 --> E7["Деньги в банке (+N дней)"]
  E7 --> CASH["Касса"]
  ACC --> SUP["Оплаты поставщику"] --> CASH
  OWN --> OPX["Опекс, налоги, кредиты"] --> CASH
  CASH --> GAP{"Ниже порога?"}
```

Та же цепочка текстом:
`заказы[d] → (кривая p[L]) → выкупы[d+L] → выручка до СПП → × (1 − take) − реклама → начислено[d] → Σ за неделю пн–вс → отчёт в пн → кнопка = конец недели + 14 дней, ближайший пн → деньги = кнопка + лаг банка → касса[d] = касса[d−1] + приходы − оплаты`.

### 4.3 Дневная ось и границы факта

- **Ось дневная:** от начала истории до горизонта (по умолчанию сегодня + 180 дней). Месячные и недельные формы получаются агрегацией: потоки суммируются, остатки берутся на последний день периода.
- **Границ факта несколько:**
  - `lastOrd` — последний день с фактическими заказами (известны на следующий день);
  - `lastSal` — последний день с фактическими выкупами (приходят позже, с отчётами);
  - `bankFactUntil` — последний день, сверенный с банком.
- На отрезке от `lastSal` до `lastOrd` выкупы считаются **по кривой от фактических заказов**. Это точнее, чем брать весь хвост по плану.
- **Сегодня ещё не факт:** сутки не закончились. Граница факта — вчера по московскому времени.

### 4.4 Строки дневной модели (на один кабинет)

| Строка | Ед. | Как считается |
|---|---|---|
| Заказы | шт | факт или план, урезанный остатком (7.7) |
| Заказы | ₽ | факт или шт × средний чек до СПП |
| Выкупы | шт, ₽ | факт или свёртка заказов с кривой (7.8) |
| Удержания ВБ | ₽ | −выкупы₽ × take (ступени по датам) |
| Реклама с баланса | ₽ | факт или −выкупы₽ × ДРР |
| Начислено на баланс | ₽ | выкупы₽ + удержания + реклама с баланса |
| Поступления от ВБ (модель) | ₽ | по календарю выплат (7.9) |
| Поступления от ВБ (факт) | ₽ | банковская выписка |
| Долг ВБ | ₽ | долг вчера + начислено − поступления (7.10) |
| Склад МП на конец дня | шт | начало + поставки − заказы + возвраты на склад − потери |
| Оплаты поставщику | ₽ | факт в прошлом, транши или план в будущем (7.11) |
| Касса | ₽ | касса вчера + все потоки дня |

### 4.5 Параметры ступенями с даты

Почти каждый параметр хранится не константой, а списком ступеней `{effective_from, value}` для каждого кабинета: ставка удержаний, кривая выкупа, лаг возврата, цикл выплат, ДРР, средний чек.

```python
def stage_at(stages, day):
    """Последняя ступень с effective_from <= day; если таких нет — первая."""
    stages = sorted(stages, key=lambda s: s.effective_from)
    cur = stages[0]
    for s in stages:
        if s.effective_from <= day:
            cur = s
    return cur.value
```

Если ВБ поменял правила, добавь новую ступень с даты изменения, прошлое не пересчитывай.
**У цикла выплат два ключа.** Срок до кнопки (`lag`) выбирай по дате начала недели продаж: ВБ объявляет правила «с недели продаж X». Задержку «кнопка → деньги» выбирай по дате кнопки «Вывести». Новая ступень не сдвигает кнопки, которые уже открыты или нажаты.

### 4.6 Иерархия источников (от сильного к слабому)

1. **Банковская выписка.** Главный источник для прошлых денег.
2. **«Итого к оплате» закрытого отчёта** (`bankPaymentSum`). Сумма будущей выплаты за закрытую неделю.
3. **Строки недельного отчёта.** Канон выкупов и удержаний по дате продажи.
4. **Дневные отчёты.** Оценка свежих дней, пока не пришёл недельный. Потом их перезаписывает канон.
5. **Модель:** кривые, ставки, план. Используется для всего, чего ещё нет в фактах.

### 4.7 Несколько кабинетов

Каждый кабинет считается отдельной плоскостью: у него свои заказы, чек, ДРР, ставка удержаний, кривая и график выплат. Итог по бизнесу — это сумма денежных строк всех плоскостей. Склад и поставщик обычно общие и считаются один раз на уровне итога. Пока у кабинета нет первого факта (он открыт позже), его строки равны нулю.

---

## 5. Получение данных

### 5.1 Токен

- **Где выпускается:** Профиль → «Интеграции по API» → «+ Создать токен». Создавать и удалять токены может только владелец профиля.
- **Типы:** персональный (собственные программы, без передачи третьим лицам и облачным сервисам), сервисный (для сервиса из Каталога решений ВБ), базовый (очень низкие лимиты), тестовый (только песочница).
- **Уровень доступа:** выбрать «Только чтение».
- **Срок жизни** 180 дней, продлить нельзя. Токен показывается один раз. На профиль можно выпустить до 20 токенов. Категории после создания не меняются: для других категорий нужен новый токен.
- **Рекомендуемый набор:**
  - токен 1, только чтение: **Статистика + Финансы + Аналитика**;
  - токен 2 (по желанию), только чтение: **Продвижение** (+ Контент, Цены и скидки).
  - Тарифы и комиссии доступны с любым токеном, кроме тестового.
- **⚠️ не подтверждено:** справочная статья ВБ относит детализацию отчётов к категории «Статистика», а спецификация новых методов `sales-reports/*` указывает категорию «Финансы». Поэтому включай обе. Проверь `/ping` на `finance-api`, затем сделай один пробный `POST …/sales-reports/list` за последнюю неделю. Ответ 403 `scope is not allowed` значит, что не хватает категории (`/ping` проверяет только, что категория токена подходит к хосту).

### 5.2 Общие правила API

- **Авторизация:** заголовок `Authorization: <токен>`.
- **Проверка токена:** `GET https://<хост>/ping` на каждом хосте. Лимит: 3 запроса за 30 секунд.
- **Лимиты считаются на профиль продавца,** а не на токен. Несколько токенов и сервисов одного продавца делят общий лимит. Алгоритм — token bucket: «интервал» означает паузу между запросами, «всплеск» — сколько запросов можно сделать подряд.
- **Ответ 429.** Прочитай заголовок `X-Ratelimit-Retry` (через сколько секунд повторить) или `X-Ratelimit-Reset`, остановись и повтори после окна. **⚠️ не подтверждено напрямую:** набор заголовков и правило «ответ 409 списывается как 5 запросов». Практика продавцов: если повторять запросы до конца окна, блокировка растёт ступенями, от минут до суток и дольше. Отправляй запросы последовательно, а не параллельно.
- **Ошибки:** 401 — токен битый или истёк; 403 `scope is not allowed` — нет нужной категории; 403 `base token is not allowed` — метод недоступен базовому токену.
- **Даты:** RFC3339, можно `2026-06-20` или `2026-06-20T23:59:59`. **Время московское (UTC+3),** если зона не указана. **⚠️ не подтверждено:** в примерах ответов `orderDt` и `saleDt` записаны с суффиксом `Z`. Проверь на 100 srid, которые есть и в отчёте, и в `supplier/orders`: метод описан в 6.1, п. 1.
- **Деньги приходят строками**, ID — большими числами (раздел 2.6).

### 5.3 Методы API для финмодели

Лимиты указаны для персонального токена, в скобках — для базового, если отличаются.

| # | Что | Метод и URL | Категория | Лимит | Период и пагинация | Зачем модели |
|---|---|---|---|---|---|---|
| 1 | Проверка токена | `GET https://<хост>/ping` | любая | 3 за 30 с | — | проверить категорию на каждом хосте |
| 2 | Список отчётов реализации | `POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/list` | Финансы ⚠️ | 1/мин; только персональный и сервисный | `dateFrom`, `dateTo`, `period` = `weekly`/`daily`, `limit` ≤ 1000 + `offset`; данные с 29.01.2024; 204 = данных нет | шапки отчётов: `reportId`, `dateFrom`/`dateTo`, `reportType` (1 основной, 2 по выкупам), **`bankPaymentSum`**, суммы по статьям |
| 3 | Детализация за период | `POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed` | Финансы ⚠️ | 1/мин (2 за 24 ч) | `dateFrom`, `dateTo` (МСК), `period`, `limit` ≤ 100 000, курсор `rrdId`: начать с 0, затем передавать `rrdId` последней строки, пока не придёт 204; `fields` — нужные колонки | строки продаж, возвратов, логистики и удержаний |
| 4 | Детализация одного отчёта | `POST https://finance-api.wildberries.ru/api/finance/v1/sales-reports/detailed/{reportId}` | Финансы ⚠️ | 1/мин; персональный и сервисный | курсор `rrdId` как в № 3; 404 = отчёта нет | точечная перекачка отчёта |
| 5 | Баланс кабинета | `GET https://finance-api.wildberries.ru/api/v1/account/balance` | Финансы | 1/мин (1 за 24 ч) | снимок | `current`, `for_withdraw`: сверка долга ВБ, момент открытия кнопки |
| 6 | Заказы | `GET https://statistics-api.wildberries.ru/api/v1/supplier/orders` | Статистика | 1/мин, всплеск 10 (1 за 3 ч) | `dateFrom`; `flag=0` — строки с `lastChangeDate > dateFrom`, до ~80 000 строк, следующий запрос с `dateFrom` = `lastChangeDate` последней строки, пока не придёт `[]`; **хранятся 90 дней**, обновление раз в 30 мин; **может не содержать заказы с неподтверждённой оплатой** (рассрочка) — их выкупы всё равно придут в детализацию | знаменатель кривой: `srid`, `date`, `nmId`, `priceWithDisc`, `isCancel`, `cancelDate` |
| 7 | Продажи и возвраты (оперативно) | `GET https://statistics-api.wildberries.ru/api/v1/supplier/sales` | Статистика | 1/мин (1 за 2 ч) | как у заказов | свежие выкупы до прихода отчёта. Деньги здесь приблизительные, `forPay` до суток бывает 0. Для денег используй детализацию. |
| 8 | Лента заказов | `POST https://seller-analytics-api.wildberries.ru/api/analytics/v1/order-feed` | Аналитика | 1/мин (1 за 3 ч) | **только последние 31 день**, фильтр по дате **текущего статуса** (не по дате заказа); время передавай с зоной `+03:00`; `limit` до 10 000 (по умолчанию 50: без явного `limit` выгрузка займёт сутки); `offset` + `snapshotTime` из первого ответа | статусы `created`/`buyout`/`cancel`/`return`/`returnDefective`; `cancelType`: `app`, `receipt`, `expire`, `other` (техническая — не считать отказом покупателя). Когорты строй по `createdAt`. Нужна для кривой отказов |
| 9 | Воронка продаж | `POST https://seller-analytics-api.wildberries.ru/api/analytics/v3/sales-funnel/products` | Аналитика | 3/мин (2 в час) | данные не старше 365 дней от сегодня; `nmIds` ≤ 1000 или пусто; `limit` до 1000 (по умолчанию 50) + `offset` | `orderCount`, `buyoutCount`, `cancelCount`, `buyoutPercent`; выкупы привязаны **к дате заказа** — это доля выкупа когорты |
| 10 | Остатки на складах ВБ | `POST https://seller-analytics-api.wildberries.ru/api/analytics/v1/stocks-report/wb-warehouses` | Аналитика | 3/мин, всплеск 1; персональный, сервисный, базовый с секретом | `nmIds` ≤ 1000, `limit` ≤ 250 000 | `quantity`, `inWayToClient`, `inWayFromClient`: остаток для ограничения прогноза |
| 11 | Платное хранение | `GET https://seller-analytics-api.wildberries.ru/api/v1/paid_storage` → `/tasks/{id}/status` → `/tasks/{id}/download` | Аналитика | создать 1/мин; статус 1 за 5 с; скачать 1/мин | окно ≤ 8 дней | хранение по дням и артикулам (по желанию) |
| 12 | Платная приёмка | `GET https://seller-analytics-api.wildberries.ru/api/v1/acceptance_report` (асинхронно) | Аналитика | 1/мин | ≤ 31 дня | приёмка по поставкам (по желанию) |
| 13 | Удержания по причинам | `GET https://seller-analytics-api.wildberries.ru/api/analytics/v1/deductions`, `GET https://seller-analytics-api.wildberries.ru/api/analytics/v1/measurement-penalties` | Аналитика | 1/мин | период | частично разложить удержания: подмены и неверные вложения, занижение габаритов (не все виды удержаний; по желанию) |
| 14 | Рекламные кампании | `GET https://advert-api.wildberries.ru/adv/v1/promotion/count`, `GET https://advert-api.wildberries.ru/api/advert/v2/adverts` | Продвижение | 5 в секунду (1–4 в час) | `ids` ≤ 50 | список кампаний |
| 15 | Статистика кампаний | `GET https://advert-api.wildberries.ru/adv/v3/fullstats` | Продвижение | 3/мин, интервал 20 с (1 в час) | `ids` ≤ 50, `beginDate`/`endDate`, **≤ 31 день** | расход по дням и артикулам (`sum`), заказы, клики |
| 16 | Списания на рекламу | `GET https://advert-api.wildberries.ru/adv/v1/upd` | Продвижение | 1 в секунду (1 в час) | `from`/`to`, 1–31 день | **`updSum` и `paymentType`** (`Баланс` / `Счёт` / `Бонусы` / `Кэшбэк`): как именно реклама влияет на деньги |
| 17 | Пополнения рекламного счёта | `GET https://advert-api.wildberries.ru/adv/v1/payments` | Продвижение | 1 в секунду | 1–31 день | пополнения счёта ВБ Продвижения: поля `date`, `sum`, `type` (`0` — Счёт, `1` — Баланс, `3` — Картой), `statusId` (`1` — обработано). В кассу оттоком идут только `type` 0 и 3 со `statusId = 1`. Пополнение с `type = 1` уже удержано из выплаты ВБ |
| 18 | Комиссии по категориям | `GET https://common-api.wildberries.ru/api/v1/tariffs/commission` | любая | 1/мин | — | справочная комиссия для новых товаров: FBW — поле `paidStorageKgvp` (не путать с хранением), FBS — `kgvpMarketplace`, DBS/DBW — `kgvpSupplier`; всё в % |
| 19 | Тарифы логистики и хранения | `GET https://common-api.wildberries.ru/api/v1/tariffs/box` (+ `/pallet`, `/return`) | любая | 60/мин | `date` | прогноз логистики для товаров без истории |
| 20 | Карточки товаров | `POST https://content-api.wildberries.ru/content/v2/get/cards/list` | Контент | 100/мин | курсор `settings.cursor` | `nmID` ↔ артикул продавца, баркоды, габариты (литраж) |
| 21 | Цены | `GET https://discounts-prices-api.wildberries.ru/api/v2/list/goods/filter` | Цены и скидки | 10 за 6 с | `limit` ≤ 1000, `offset` | текущие цены для среднего чека в прогнозе |

Официальная документация: `https://dev.wildberries.ru/openapi/` (разделы reports, analytics, documents-and-accounting, promotion, rates, item-management). Если страница закрыта антибот-проверкой, используй YAML-спецификации: `https://dev.wildberries.ru/api/swagger/yaml/ru/<файл>.yaml?region=ru` (`11-analytics`, `12-reports`, `13-finances`, `08-promotion`, `10-rates`, `02-items`).

**Истории выплат в API нет.** Фактические даты и суммы поступлений на счёт бери из банковской выписки. Задержки заявок смотри в ЛК, на скриншоте владельца.

### 5.4 План выгрузки (порядок и периоды)

Делай запросы последовательно. Между запросами к одному методу выдерживай паузу не меньше интервала лимита, при лимите 1/мин это 61 секунда. Скажи владельцу, что первая выгрузка займёт от 30 минут до 2 часов.

1. `/ping` на `finance-api`, `statistics-api`, `seller-analytics-api`, `advert-api`. Пинг каждого хоста делай тем токеном, который к нему относится: токен 1 (`WB_TOKEN`) — `finance-api`, `statistics-api`, `seller-analytics-api`; токен 2 (`WB_TOKEN_ADS`) — `advert-api` (и `content-api`, `discounts-prices-api`, если они включены). После пинга сделай пробный вызов `sales-reports/list` (5.1). Нет доступа — остановись и сообщи владельцу, какой категории не хватает.
2. **Список отчётов** (№ 2) за 12 месяцев (минимум 6), `period=weekly`. Сохрани шапки.
3. **Детализация** (№ 3) за тот же период, `period=weekly`, с пагинацией `rrdId` и набором `fields`: `rrdId, reportId, reportType, dateFrom, dateTo, createDate, docTypeName, sellerOperName, bonusTypeName, srid, nmId, vendorCode, sku, techSize, quantity, retailPrice, retailPriceWithDisc, retailAmount, commissionPercent, ppvzSalesCommission, vw, vwNds, forPay, acquiringFee, deliveryService, rebillLogisticCost, paidStorage, paidAcceptance, penalty, deduction, additionalPayment, cashbackAmount, cashbackDiscount, cashbackCommissionChange, installmentCofinancingAmount, paymentSchedule, deliveryAmount, returnAmount, orderDt, saleDt, rrDate, isB2b, deliveryMethod` (без `rrdId` пагинация не работает). На 1 млн строк уйдёт около 10 запросов.
4. **Дневные детализации** (№ 3, `period=daily`, `dateFrom = dateTo = день`) за дни после конца последней закрытой недели.
5. **Заказы** (№ 6) за последние 90 дней, `flag=0`, с пагинацией по `lastChangeDate`. Обновляй строки по `srid`: новую добавляй, существующую заменяй (upsert). Строки с изменениями приходят повторно.
6. **Лента заказов** (№ 8) за 31 день — для кривой отказов (по желанию).
7. **Воронка** (№ 9, `limit` = 1000) понедельно за 52 недели — доля выкупа по когортам старше 90 дней. 52 вызова при лимите 3/мин занимают около 18 минут.
8. **Реклама:** кампании (№ 14), затем `upd` (№ 16) и `payments` (№ 17) окнами по 31 дню за 6–12 месяцев. `fullstats` (№ 15) — если нужен расход по артикулам.
9. **Баланс** (№ 5): один снимок сейчас. В регулярном режиме снимай его каждый день.
10. **Остатки** (№ 10), **карточки** (№ 20), **цены** (№ 21), **тарифы** (№ 18–19) — текущие снимки.

**Устойчивость выгрузки:**
- Не записывай частично скачанное: пиши в файл с суффиксом `.tmp` и переименовывай после последней страницы.
- **Не уменьшать (never-shrink).** Если при повторной загрузке **того же источника** (недельный поверх недельного, дневной поверх дневного) пришло меньше строк, чем уже сохранено, не перезаписывай данные. Пометь день и перекачай позже. Замена дневных данных недельным отчётом — это не уменьшение: канон заменяет их всегда.
- Ответ 204 или пустой ответ не затирает сохранённые данные. 204 на запросе с `rrdId > 0` означает конец выборки. 204 на первом запросе (`rrdId = 0`) значит, что за период данных нет или отчёт ещё не сформирован. Для периода, где данные должны быть, повтори запрос позже, а сохранённое не трогай.

### 5.5 Что изменилось в 2025–2026 (если твои знания старее)

| Когда | Изменение |
|---|---|
| 11.02.2026 | `POST /adv/v2/fullstats` удалён, вместо него `GET /adv/v3/fullstats`; старые списки кампаний заменены на `GET /api/advert/v2/adverts` |
| 19.03.2026 | удалён `GET /api/v1/supplier/incomes` (поставки) |
| 16.04.2026 | появились `finance-api …/sales-reports/list`, `…/detailed`, `…/detailed/{reportId}` |
| 26.06.2026 | удалён `GET /api/v1/supplier/stocks`; замена — `stocks-report/wb-warehouses` или `warehouse_remains` |
| июль 2026 | **удалён `GET /api/v5/supplier/reportDetailByPeriod`** (старый отчёт реализации, snake_case-поля). Используй `sales-reports/detailed` (camelCase, деньги строками). |
| 08.08.2026 | появилась «Лента заказов» `order-feed` |
| ~18.08.2026 | склады ВБ в отчётах обезличены (`Склад WB`, id −999999 или 0); разбивки по складам нет |
| ~09.2026 | «Логистика» в описаниях переименована в «Доставку» (`deliveryServiceSum` теперь «Стоимость доставки»); `reportType = 3` удалён |
| 26.09.2026 | `goods-return` устарел, замена — `GET /api/analytics/v1/item-returns` |

Даты взяты из неофициальной истории изменений спецификаций, сами изменения подтверждены текущими официальными YAML.

**Соответствие старых и новых полей детализации** (⚠️ сопоставление по описаниям, официальной таблицы нет):
`ppvz_for_pay → forPay`, `retail_price_withdisc_rub → retailPriceWithDisc`, `retail_amount → retailAmount`, `delivery_rub → deliveryService`, `storage_fee → paidStorage`, `acceptance → paidAcceptance`, `deduction → deduction`, `penalty → penalty`, `additional_payment → additionalPayment`, `acquiring_fee → acquiringFee`, `doc_type_name → docTypeName`, `supplier_oper_name → sellerOperName`, `bonus_type_name → bonusTypeName`, `order_dt → orderDt`, `sale_dt → saleDt`, `nm_id → nmId`, `sa_name → vendorCode`, `barcode → sku`, `realizationreport_id → reportId`, `rrd_id → rrdId`, `rr_dt → rrDate`, `rebill_logistic_cost → rebillLogisticCost`, `ppvz_vw → vw`, `ppvz_sales_commission → ppvzSalesCommission`, `cashback_amount → cashbackAmount`.

### 5.6 Данные вне API

| Данные | Откуда |
|---|---|
| Поступления от ВБ (дата, сумма) | банковская выписка или учётная система. Бери только **банковские** счета: технические и корректировочные счета в учёте похожи на приходы ВБ, но ими не являются. |
| Статусы заявок на вывод | только ЛК (скриншот). API историю заявок не отдаёт. |
| Себестоимость, партии, оплаты поставщику | МойСклад, 1С или таблица закупок |
| Опекс, налоги, кредиты | P&L, бюджет движения денег, бухгалтерия |
| Детализация в xlsx (способ В) | ЛК, раздел финансовых отчётов. Большой отчёт бывает разбит на несколько листов: читай все. В Excel возвраты в колонке «реализовал» могут стоять плюсом, поэтому сумма по колонке даёт брутто. Знак ставь по типу документа. Порядок и названия колонок в Excel и в API различаются: сопоставляй колонки по заголовкам, а не по номеру. |

### 5.7 Минимальная схема хранения

```text
orders(cabinet, srid, srid_core, order_dt, nm_id, price_before_spp, is_cancel, cancel_dt, last_change_dt)
report_headers(cabinet, report_id, date_from, date_to, report_type, create_date, bank_payment_sum, payment_schedule, ...сумм. статьи)
report_rows(cabinet, report_id, rrd_id, period[weekly|daily], doc_type, oper_name, bonus_type,
            srid, srid_core, nm_id, quantity, retail_price_withdisc, retail_amount, for_pay,
            acquiring_fee, delivery_service, rebill_logistic_cost, paid_storage, paid_acceptance,
            penalty, deduction, additional_payment, cashback_*, order_dt, sale_dt, rr_dt)
ads_spend(cabinet, date, advert_id, nm_id?, amount, payment_type[Баланс|Счёт|Бонусы|Кэшбэк])
ads_topups(cabinet, date, amount, source[0 Счёт|1 Баланс|3 Картой], status)
balance_snapshots(cabinet, ts, current, for_withdraw)
bank_inflows(cabinet, date, amount, account)
batches(batch_id, nm_id, qty, cost_rub, order_dt, ship_dt, arrive_dt, status)
supplier_payments(date, amount, batch_id?, kind[факт|план], tranche)
params(cabinet, name, effective_from, value, source[калибровка|по умолчанию|владелец|ручная])
```

---

## 6. Нормализация данных и ловушки

### 6.1 Время и границы

1. Все даты приводи к московскому дню. До этого проверь зону `orderDt`: возьми 100 srid, которые есть и в отчёте, и в `supplier/orders` (там время московское по документации), и сравни `orderDt` с `date`. Разница ровно 3 часа — в отчёте UTC, переводи. Разницы нет — время уже московское, суффикс `Z` игнорируй.
2. Сегодняшний день не считается фактом. Граница факта — вчера.
3. У заказов и выкупов разные границы факта (`lastOrd`, `lastSal`).

### 6.2 Отчёты: дневные, недельные, стык месяцев

4. **Никогда не суммируй дневные и недельные отчёты за одну неделю:** это двойной счёт. После закрытия недели каноном считается недельный отчёт, дневные нужны только для свежих дней.
5. **Дневные данные предварительные:** ВБ потом сторнирует и корректирует их. Свежие дни перечитывай до прихода недельного отчёта.
6. В дневном ответе `saleDt` может быть раньше запрошенного дня: возврат датируется исходной продажей. Не фильтруй ответ по дате запроса.
7. **Неделя на стыке месяцев приходит двумя отчётами** (например, 31.08–31.08 и 01.09–06.09). Заявка и платёж по ней одни. Не считай, что отчёт всегда охватывает пн–вс: бери `dateFrom`/`dateTo` из списка отчётов. Для календаря выплат склеивай куски по ISO-неделе даты `dateFrom`. Проверку «отчёт уже загружен» делай по `reportId`, а не по совпадению дат. ⚠️ ВБ это официально не подтверждал, но так наблюдается на практике.
8. За неделю бывает два типа отчётов: основной и по выкупам. Сумма к выводу равна сумме `bankPaymentSum` всех отчётов недели: двух типов и, на стыке месяцев, обоих кусков (до четырёх отчётов).
9. **Возврат по старой продаже меняет закрытый день.** Регулярно перечитывай всю историю из недельных отчётов и записывай журнал изменений «было → стало».

### 6.3 Классификация строк отчёта

10. **Не фильтруй по названию операции.** ВБ переименовывает операции («Логистика» → «Доставка» около 01.09.2026, ⚠️ значение в строках официально не подтверждено). После такого фильтр по имени молча обнуляет статью, и начисление завышается. Классифицируй строки **по денежной колонке**: `deliveryService`, `paidStorage`, `paidAcceptance`, `penalty`, `deduction`, `additionalPayment`, `forPay`. Если без названия не обойтись, используй список всех известных вариантов.
11. **ВБ может молча перенести признак в другую колонку.** Например, подпись рекламного удержания может переехать из одного поля в другое. Если статья вдруг стала нулевой там, где раньше были деньги, скорее всего модель перестала её видеть, а расход никуда не делся. Ищи признак сразу в нескольких полях (`bonusTypeName`, `sellerOperName`).
12. **Товарные строки** — это строки с `docTypeName` из {Продажа, Возврат}, `quantity ≠ 0`, у которых `sellerOperName` по справочнику (п. 14) относится к обычной продаже или возврату. Компенсации («Компенсация ущерба», «Компенсация подменённого товара», «Добровольная компенсация при возврате»), «Коррекция продаж» и «Сторно продаж» — это деньги, но не выкупы: в сверку 7.1 они идут, в кривую выкупа и в штуки юнит-экономики — нет. Это не противоречит п. 10: здесь строки классифицируются по справочнику, а не статьи фильтруются по одному имени. Строки логистики, хранения и удержаний обычно идут с `quantity = 0`. Проверь это на своём справочнике значений.
13. **Строки без товара** (`nmId = 0`: хранение, возмещения, баллы за отзывы) не распределяй по догадке. Хранение раздели пропорционально выручке или объёму, остальное покажи отдельной строкой «нераспределённое». Если srid есть, а товара нет, подожди: строка-ключ (продажа с тем же srid) может прийти позже.
14. Каждую выгрузку начинай со справочника: (`docTypeName`, `sellerOperName`, `bonusTypeName`) → количество строк, Σ `forPay`, Σ `deliveryService`, Σ `deduction`. Новые значения показывай владельцу.

### 6.4 Цены и СПП

15. **База модели — цена до СПП** (`retailPriceWithDisc`). `retailAmount` — это цена после СПП, её платит покупатель. Коэффициент `k = Σ retailAmount / Σ retailPriceWithDisc` считается по факту.
16. **Отрицательная комиссия в отдельных строках — это норма:** так ВБ компенсирует СПП. Итог недели при этом положительный.
17. Если какая-то ставка (например, посредника) задана от выручки после СПП, а модель считает от выручки до СПП, переведи её: `rate_model = rate × k`, где k — скользящее значение по 4 закрытым неделям.

### 6.5 srid и сопоставление

18. Нормализуй `srid`: приведи его к ядру — раздели строку по точкам и возьми самый длинный сегмент. В официальных примерах встречаются формы `11.<ядро>.3.0`, `<ядро>` и `mp.<ядро>.r`. Длину ядра не предполагай. Проверь долю совпадений srid между отчётом и заказами: она должна быть больше 95 %. Считай долю только по строкам отчёта, у которых `orderDt` попадает в окно хранения заказов (последние 90 дней минус 3 дня). Более старые заказы в методе уже удалены, и совпадения по ним не будет. Несовпавшие строки моложе 90 дней сравни с лентой заказов: это могут быть заказы в рассрочку. Если доля всё равно меньше 95 %, ищи другой формат.
19. Повторный заказ той же физической единицы получает новый srid. Физическую единицу отслеживает только код маркировки (`kiz`).

### 6.6 Возвраты и знаки

20. Выкуп и возврат храни **раздельно**, а чистый выкуп считай отдельно. Иначе при повторном импорте всё перепутается.
21. Знак ставь по `docTypeName`: у строк «Возврат» суммы вычитаются. **Проверь,** не приходят ли суммы уже отрицательными, чтобы не инвертировать их дважды. Окончательная проверка — сверка с `bankPaymentSum` (7.1).
22. Отрицательное дневное начисление в воскресенье нормально: в последний день недели ложатся хвосты корректировок. Минусы в середине недели — повод разобраться.

### 6.7 Реклама

23. Смотри `paymentType` в `adv/v1/upd`:
    - `Баланс` — реклама удерживается из будущей выплаты ВБ, то есть уменьшает начисление;
    - `Счёт` — списание с рекламного счёта, который продавец пополнил заранее. Деньги ушли в момент пополнения: смотри `adv/v1/payments` с `type` 0 (счёт) или 3 (карта) и `statusId = 1`. Если счёт пополнен с баланса ВБ (`type = 1`), деньги уже удержаны в отчёте реализации (`deduction`), второй раз их не вычитай. В ответе ВБ значение может прийти как `Счет` (через «е», так в официальном примере): сравнивай строки после замены «ё» на «е»;
    - `Бонусы` и `Кэшбэк` — не живые деньги, в кассу они не идут.
24. Реклама с баланса видна в отчёте в `deduction`, признак — в `bonusTypeName` (например, со словами «Продвижение», «Медиа»). ⚠️ Где именно она лежит, официально не описано: сверяй суммы и даты с `upd`. У части строк `upd` нет `updTime` (`null`): такие строки относи к дате конца запрошенного окна и помечай как «дата приблизительная». Суммы `updSum` и `sum` — целые рубли.
25. Прогноз «продажи минус комиссия» без учёта рекламы с баланса завышает приход на всю рекламу недели.

### 6.8 Ловушки учёта денег

26. **Банк, ЛК и учёт расходятся.** Статус в ЛК опережает банк на день. Падение `current` на сумму недели означает, что заявка **создана**, а не оплачена.
27. **Платная услуга ускоренного вывода** (если появится): вычти её либо из выплаты, либо отдельным оттоком, но не дважды. В P&L это расход.
28. **Комиссию посредника** считай внутри ставки удержаний или начислений, а не вычитай повторно в момент выплаты. Если сумма «к переводу» уже нетто, второй раз её не уменьшай.
29. **Оплата поставщику** не должна попадать ещё и в «прочие расходы».
30. **Смена метода расчёта** (кривая возвратов, цикл выплат) вводится только с даты активации. Иначе один и тот же день посчитается дважды. При смене режима вывода (ежедневный → еженедельный) не подставляй полную сумму недельного отчёта, если часть недели уже ушла ежедневным выводом: бери только остаток. При смене лага возврата на склад один и тот же день отправки не должен вернуться дважды: недосчёт честнее задвоения.
31. **Бюджеты владельца.** Из опекса владельца исключи рекламу на ВБ (она уже в начислениях или в `ads_topups`) и закупки (они в оплатах поставщику). Плановые поступления от ВБ из бюджета бухгалтерии в кассу не ставь: будущие поступления строит только модель (7.9). План бухгалтерии можно показать рядом для сравнения.

---

## 7. Формулы и псевдокод

Псевдокод написан в стиле Python. `D(x)` означает Decimal. Даты — московские дни.

### 7.1 Сверка «Итого к оплате» (обязательный первый шаг)

Формулу `bankPaymentSum` ВБ не публикует (⚠️). Восстанови её из строк и сверь (допуск 1 ₽ на отчёт):

```python
def payout_from_rows(rows):
    s = D(0)
    for r in rows:
        sgn = RETURN_SIGN if r.doc_type == 'Возврат' else 1   # RETURN_SIGN = -1, если у всех строк «Возврат» forPay >= 0; иначе +1 — определи один раз на весь набор
        s += sgn * r.for_pay
        s -= r.delivery_service + r.paid_storage + r.paid_acceptance + r.penalty + r.deduction
        s += SIGN_ADDITIONAL * r.additional_payment      # знак подобрать сверкой
        s -= SIGN_REBILL * r.rebill_logistic_cost        # кандидат: включать или нет — по сверке
        s += S_CA*D(r.cashback_amount) + S_CD*D(r.cashback_discount) + S_CC*D(r.cashback_commission_change)   # знаки S_* подобрать сверкой; ожидание: S_CA=-1, S_CD=+1, S_CC=-1
    return s

for rep in closed_reports:                               # минимум 8 отчётов, оба типа
    diff = payout_from_rows(rows_of(rep)) - rep.bank_payment_sum
    if abs(diff) > D('1.00'):
        flag(rep, diff)   # найди колонку, которая закрывает разницу на ВСЕХ отчётах
```

Пустые значения и `null` превращай в `D(0)`. Кандидаты для перебора, если не сходится: `rebillLogisticCost`, `additionalPayment`, три поля `cashback*`, `installmentCofinancingAmount`, `ppvzReward`, знак строк «Возврат».

Спорные знаки и колонки подбирай перебором на 8 и более отчётах. Правильная комбинация даёт ноль на всех отчётах сразу. Пока формула не сошлась, данным по удержаниям из строк доверять нельзя (прогноз выплат по шапкам строить можно, см. раздел 1).

### 7.2 Кривая выкупа по когортам srid

Основной метод. Для каждого лага L считаем, какая доля заказов выкупается ровно через L дней. Учитываем только **зрелые** когорты: незрелые когорты занижают хвост кривой.

```python
def buyout_curve(orders, rows, T, window=90, max_lag=45, delay=3):
    """
    orders: заказы (srid_core, order_day), все заказы, включая отменённые, — как в плане
    rows:   товарные строки детализации (Продажа/Возврат) с sale_day
    T:      граница факта выкупов (lastSal) + 1 день
    delay:  задержка данных: 3 дня с дневными отчётами, 8 — только с недельными
    """
    order_day = {o.srid_core: o.order_day for o in orders
                 if T - days(window) <= o.order_day < T}
    n_orders = Counter(order_day.values())                 # заказов в когорте дня
    events = defaultdict(D)                                # (день когорты, лаг) -> чистые шт
    for r in rows:
        d0 = order_day.get(r.srid_core)
        if d0 is None:
            continue
        lag = (r.sale_day - d0).days
        if 0 <= lag <= max_lag:
            events[(d0, lag)] += (+1 if r.doc_type == 'Продажа' else -1) * abs(r.quantity)
    p = []
    for L in range(max_lag + 1):
        mature = [d for d in n_orders if (T - d).days >= L + delay]
        den = sum(n_orders[d] for d in mature)
        num = sum(events.get((d, L), 0) for d in mature)
        p.append(num / den if den else 0)
    return p          # sum(p) = чистая доля выкупа; медиана лага — первый L, где cumsum >= sum/2
```

**Если заказов за 90 дней нет** (способ В, только xlsx), форму кривой бери из детализации: `q[L]` — доля продаж с лагом `saleDt − orderDt = L`. Долю выкупа считай по воронке как `buyoutCount / orderCount` по когортам старше 30 дней. Готовый `buyoutPercent` не бери: он округлён до целого и без товаров в доставке. ⚠️ Вычтены ли возвраты из `buyoutCount`, не описано: сверь с чистой долей по отчёту реализации на тех же неделях. Если воронки нет, оцени долю как «чистые выкупы недели / заказы недели, сдвинутые на средний лаг». Тогда `p[L] = доля × q[L]`.

**Пример формы кривой** (условно, сумма ≈ 26,5 %):

| Лаг, дн | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11–14 | 15–24 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| p, % | 0,5 | 2,0 | 5,5 | 4,5 | 3,5 | 2,5 | 2,0 | 1,5 | 1,0 | 0,8 | 0,6 | 1,6 | 0,5 |

Пик приходится на 2-й день, медиана — на 4-й, 90 % выкупов набирается к 10-му дню, после ~24 дней выкупов почти нет. Форма сильно зависит от категории и от логистики ВБ: **всегда калибруй кривую на своих данных.**

**Матрица выкупа для диагностики.** Строки — когорты последних ~4 недель, колонки — лаги 0..21, в ячейке накопленный процент выкупа и отклонение от нормы. Норма лага — **медиана** по зрелым когортам (возраст ≥ 24 дней, n ≥ 30), а не среднее: среднее завышают быстрые когорты. Красная строка значит, что выкуп действительно отстаёт. Это сигнал для бизнеса, а не ошибка модели.

### 7.3 Лесенка (упрощение кривой)

Удобна для таблиц и ручной проверки:

```python
def ladder_from_curve(p, split=10):
    early, late = p[:split + 1], p[split + 1:]
    p1, p2 = sum(early), sum(late)
    d1 = round(sum(L * x for L, x in enumerate(early)) / p1) if p1 else 5
    d2 = round(sum((split + 1 + i) * x for i, x in enumerate(late)) / p2) if p2 else 13
    return (p1, d1), (p2, d2)

# применение
buyouts[i] = orders[i - d1] * p1 + orders[i - d2] * p2          # дробные штуки, без floor
revenue[i] = orders_rub[i - d1] * p1 + orders_rub[i - d2] * p2
```

Лесенка по умолчанию (условно, одежда): 20 % на 5-й день + 7 % на 13-й, итого 27 %. Это более медленный профиль, чем пример кривой в 7.2: по нему `ladder_from_curve` даёт около 24 % на 4-й день и около 2 % на 14-й. Используй что-то одно.

### 7.4 Кривая отказов и возвраты на склад

Возвраты на склад нужны для прогноза остатка: невыкупленный товар снова поступает в продажу. На деньги они не влияют.

```python
def reject_curve(orders_or_feed, T, window_weeks=8, max_lag=30, delay=3):
    """Отказ = isCancel с cancelDate (заказы) или status cancel/return (лента заказов, 31 день)."""
    cohort = [o for o in orders_or_feed if T - weeks(window_weeks) <= o.order_day < T - days(max_lag + delay)]   # только когорты, у которых отказы завершились
    rejected = [o for o in cohort if o.is_rejected]
    reject_share = len(rejected) / len(cohort)
    pdf = histogram([(o.reject_day - o.order_day).days for o in rejected], 0..max_lag, normalize=True)
    return reject_share, pdf

# Возвраты на склад, вариант «по кривой»:
for d in order_days:
    for k, w in enumerate(pdf):
        returns_stock[d + k + rev_lag] += orders[d] * reject_share * w
# + возвраты после выкупа (в варианте «комком» они уже учтены через net_buyout_share):
for d in order_days:
    for L in range(len(p)):
        returns_stock[d + L + buy_ret_lag + rev_lag] += buyouts_gross[d, L] * return_share
# Вариант «комком» (проще):
returns_stock[i] = orders[i - ret_lag] * (1 - net_buyout_share - lost_share)   # дробные штуки
```

- **⚠️ Проверь на данных,** какие отказы попадают в `isCancel` в методе заказов. В ленте заказов (`cancelType`) типы отказа указаны явно: `app` — отказ до получения, `receipt` — при получении, `expire` — истёк срок хранения, `other` — техническая отмена (исключи её из кривой отказов покупателей).
- Переменные: `rev_lag` — обратная логистика ПВЗ → склад (по умолчанию 4–5 дней); `ret_lag` — лаг «комком» (по умолчанию 16 дней); `buy_ret_lag` — от выкупа до возврата покупателем (по умолчанию ~14 дней); `return_share` — доля возвратов от валовых выкупов; `buyouts_gross[d, L]` — валовые выкупы когорты d на лаге L.
- По ленте заказов (31 день) надёжна только ранняя часть формы (лаги 0–10). Долю отказов бери из метода заказов (90 дней) или как 1 − валовой выкуп − потеряшки.
- Окно кривой отказов — **последние 6–8 недель.** Длинное окно не успевает за сменой режима: при сбоях логистики отказы могут идти вдвое медленнее при той же доле.
- Кривую проверяй по когортам, а не по остаткам ВБ. В остатках шумят переклассификации «склад ↔ в пути», списания и закрытые склады.
- При смене метода «комком → кривая» используй дату активации: заказы до неё возвращаются комком, после неё — по кривой.

### 7.5 Юнит-экономика по отчёту реализации

Считай за период (4 закрытые недели) по каждому `nmId` и в целом. Делить нужно на **чистые выкупы**: логистика невыкупов ложится на выкупленные единицы.

```python
def unit_economics(rows, nm, cogs_per_unit, ads_rub):
    R = [r for r in rows if r.nm_id == nm]
    sgn  = lambda r: -1 if r.doc_type == 'Возврат' else 1            # для штук (с abs)
    msgn = lambda r: RETURN_SIGN if r.doc_type == 'Возврат' else 1   # для денег, как в 7.1
    goods = [r for r in R if is_goods_row(r)]                       # товарные строки, 6.3 п. 12
    units      = sum(sgn(r) * abs(r.quantity) for r in goods)
    if units <= 0:
        return None                                      # показать в «нераспределённом», не делить
    revenue    = sum(sgn(r) * D(r.retail_price_withdisc) * abs(r.quantity) for r in goods)  # до СПП
    for_pay    = sum(msgn(r) * D(r.for_pay) for r in goods)
    commission = revenue - for_pay                       # кВВ + эквайринг − компенсация СПП
    acquiring  = sum(msgn(r) * D(r.acquiring_fee) for r in goods)   # справочно: уже внутри commission
    logistics  = sum(D(r.delivery_service) + SIGN_REBILL * D(r.rebill_logistic_cost) for r in R)   # SIGN_REBILL — тот же, что подобран в 7.1 (0 или 1)
    storage    = sum(r.paid_storage for r in R) + allocated_storage(nm)   # строки с nmId=0
    acceptance = sum(r.paid_acceptance for r in R)
    penalties  = sum(r.penalty for r in R)
    other_ded  = sum(r.deduction for r in R if not is_ads(r))
    additional = sum(D(r.additional_payment) for r in R)
    loyalty    = sum(S_CA*D(r.cashback_amount) + S_CD*D(r.cashback_discount)
                     + S_CC*D(r.cashback_commission_change) for r in R)   # знаки из 7.1
    to_pay     = for_pay - logistics - storage - acceptance - penalties - other_ded + SIGN_ADDITIONAL * additional + loyalty   # те же статьи и знаки, что в 7.1
    margin     = to_pay - ads_rub - cogs_per_unit * units - tax(revenue)
    return {k: (v, v / units, v / revenue) for k, v in locals_money().items()}  # ₽, ₽/шт, % выручки
```

`is_ads(r)` — строка `deduction`, у которой `bonusTypeName` или `sellerOperName` содержит «Продвижение», «Медиа», «Реклама» или «блогер» (список сверь со справочником значений из 6.3, п. 14). `is_goods_row(r)` — товарная строка по правилу 6.3, п. 12. `allocated_storage(nm)` — хранение без товара (`nmId = 0`), распределённое пропорционально выручке до СПП. `tax(revenue)` — ставка из раздела 8 (налоги) × база по режиму владельца. `locals_money()` — все денежные переменные функции (revenue, commission, logistics, …, margin). Эквайринг уже входит в `commission`, второй раз его не вычитай. Если `units ≤ 0`, покажи артикул в «нераспределённом» и не дели.

Логистика на один выкуп складывается так: `(прямая доставка × все доставленные заказы + обратная × невыкупы и возвраты) / чистые выкупы`. Поэтому при низкой доле выкупа логистика на выкупленную единицу в разы выше тарифа. Это главный рычаг для категорий с массовыми отказами.

**Пример** (условно, на 1 чистый выкуп):

| Статья | ₽ / шт | % выручки |
|---|---:|---:|
| Выручка до СПП | 2 000 | 100 % |
| Комиссия ВБ с эквайрингом | −500 | −25 % |
| Логистика (с невыкупами) | −300 | −15 % |
| Хранение | −60 | −3 % |
| Приёмка | −10 | −0,5 % |
| Штрафы и прочие удержания | −30 | −1,5 % |
| **К выплате до рекламы** | **1 100** | **55 %** |
| Реклама (ДРР 10 %) | −200 | −10 % |
| **К выплате после рекламы** | **900** | **45 %** |
| Себестоимость | −600 | −30 % |
| Налог (по режиму продавца, условно) | −100 | −5 % |
| **Маржа до постоянных расходов** | **200** | **10 %** |

### 7.6 Ставка удержаний (take rate) и ДРР

```python
# по каждой закрытой неделе W, затем скользящее значение по 4 неделям
take[W] = 1 - (bank_payment_sum[W] + ads_from_balance_in_report[W]) / revenue_before_spp[W]
drr_buyouts[W] = ads_money[W] / revenue_before_spp[W]         # ads_money = Баланс + Счёт, без бонусов
drr_orders[W]  = ads_money[W] / orders_rub[W]                  # ≈ drr_buyouts × доля выкупа
```

- Все три величины недели W бери из **одних и тех же отчётов**: основного и по выкупам, включая оба куска стыка месяцев. `revenue_before_spp[W] = Σ sgn × retailPriceWithDisc × |quantity|` по товарным строкам этих отчётов. `ads_from_balance_in_report[W]` — рекламные строки `deduction` тех же отчётов. Для калибровки бери недели после 24.07.2026: в эпоху ежедневного вывода в удержаниях могла быть плата за вывод, и реклама шла со счёта.
- Ставка take охватывает все удержания ВБ, кроме рекламы: комиссию, логистику, эквайринг, хранение, приёмку, штрафы и прочее. У разных продавцов он обычно от 35 до 60 % и зависит от категории, габаритов и локализации.
- Шум неделя к неделе ±10 % нормален. Если ошибка держит один знак 3–4 недели подряд, пересчитай ставку и поставь новую ступень с даты.
- Сценарий «ВБ поднял комиссию с 1-го числа на 5 п. п.» задаётся ручной ступенью `take + 0,05` с этой даты.

### 7.7 План заказов и ограничение остатком

```python
def plan_orders(cab, day):
    if day <= cab.last_ord or day < cab.first_fact:
        return 0                                   # прошлое — факт
    if day in cab.calendar and cab.calendar[day].orders is not None:
        return cab.calendar[day].orders            # точное значение от владельца
    return round(cab.orders_per_day * cab.seasonality.get(month(day), 1.0))

avg_check(day) = calendar.avg_check or stage_at(params.avg_check, day)    # до СПП
drr(day)       = calendar.drr or stage_at(params.drr, day)

# нельзя продать больше, чем лежит на складе ВБ
budget = max(stock_start[i] + arrivals[i] + returns_stock[i], 0)
want = {cab: plan_orders(cab, i) for cab in cabinets}
if sum(want.values()) > budget:                     # дефицит делится пропорционально планам
    allow = {c: round(budget * w / sum(want.values())) for c, w in want.items()}
else:
    allow = want
stock_end[i] = stock_start[i] + arrivals[i] - sum(allow.values()) + returns_stock[i] - losses[i]
```

По умолчанию `orders_per_day` — медиана **всех** заказов за последние 28 дней, включая отменённые, — так же, как в знаменателе кривой 7.2. Если владелец даёт план «без отмен», переведи его: план ÷ (1 − доля отмен). `avg_check` по умолчанию — заказы в рублях / заказы в штуках за 28 дней. Склад, ушедший в ноль, — сигнал срочно завозить товар. В прогнозе заказы при этом падают, и вслед за ними, через 4–5 недель, падают выплаты.

Ограничение остатком лучше считать по артикулам (или по группам размеров). При общем остатке модель продаёт размеры, которых уже нет.

### 7.8 Выкупы и начисления по дням (свёртка)

```python
for i in axis:
    if i <= last_sal:                              # факт
        buyouts[i], revenue[i] = fact_buyouts[i], fact_revenue[i]
    else:                                          # свёртка с кривой (заказы — факт или план)
        buyouts[i] = sum(p[L] * orders[i - L]     for L in range(len(p)))   # дробные штуки
        revenue[i] = sum(p[L] * orders_rub[i - L] for L in range(len(p)))

    wb_ded[i]  = -revenue[i] * stage_at(take, i)
    ads_bal[i] = -fact_ads_balance[i] if i <= last_ads else -revenue[i] * drr(i) * share_from_balance
    accrued[i] = revenue[i] + wb_ded[i] + ads_bal[i]

    # для дней, покрытых дневной детализацией, берётся факт:
    if i <= last_daily_report:
        accrued[i] = payout_from_rows(daily_rows[i])   # формула из 7.1
        if not ADS_IN_DAILY_ROWS:                      # см. проверку ниже
            accrued[i] -= fact_ads_balance[i]          # upd, paymentType = Баланс
```

**Проверка `ADS_IN_DAILY_ROWS`.** Возьми закрытую неделю. Сравни Σ рекламных строк `deduction` в её дневных отчётах, в недельном отчёте и Σ `upd` с `paymentType = Баланс`. Если в дневных строках реклама есть — `True`, если нет — `False` (⚠️ Приложение А, п. 14).

Штуки храни дробными и округляй только при показе (или округляй накопленную сумму и бери разность дней): `floor` каждый день теряет в среднем 0,5 шт., и у небольшого продавца это до 15 % недосчёта.

`share_from_balance` — доля рекламы, оплачиваемой с баланса ВБ (по `paymentType` за последние недели). Остальная реклама — это пополнения счёта, которые уходят в кассу оттоком (7.11).

Проверка тождества: накопленные выкупы ≈ Σ заказов × Σ p, за вычетом незавершённых лагов в конце периода.

### 7.9 Календарь выплат

**Регламент ВБ меняется**, поэтому держи его параметрами ступенями (срок до кнопки — по неделе продаж, задержку денег — по дате кнопки, см. 4.5). Действующий на октябрь 2026 цикл выглядит так:

1. Неделя продаж: с понедельника по воскресенье.
2. В понедельник после неё приходит отчёт.
3. **Кнопка «Вывести»** появляется в дату `конец недели + 14 дней`, сдвинутую вперёд до ближайшего понедельника. Для полной недели это понедельник через две недели после отчёта.
4. **Деньги** по регламенту приходят в течение 7 рабочих дней после заявки. На практике 2026 года — от 7 до 21 календарного дня, обычно 12–18.
5. Ежедневный и досрочный вывод с 24.07.2026 отключены, вывод только еженедельный. Выводится вся сумма недели (основной отчёт + отчёт по выкупам), частичный вывод невозможен.
6. В шапке отчёта есть `paymentSchedule` — разовое изменение срока перечисления. Если поле не пустое и не `0`, покажи его владельцу и не строй дату выплаты этого отчёта по общему правилу, пока смысл значения не подтверждён (⚠️ Приложение А, п. 12).

**Как вводить ступень.** ВБ объявляет «с недели продаж X новый срок N». Храни ступень с `effective_from = X.start` и выбирай `lag` по дате начала недели продаж. Уже открытую кнопку (`for_withdraw > 0`) не сдвигай. Так выполняется правило 4.5: ступень не двигает кнопки, которые уже открыты или нажаты.

История регламента (для понимания, что он меняется): с недели 16–22.03.2026 кнопка открывалась через 21 день после конца недели, с недели 20–26.07.2026 — через 14 дней. Актуальные правила проверяй в новостях ЛК.

```python
def button_date(week_end, lag):                    # lag = stage_at(cycle.lag, week_start) — обычно 14
    d = week_end + days(lag)
    while d.weekday() != 0:                        # ближайший понедельник, не раньше
        d += days(1)
    return d

def money_date(btn, mode):
    if mode == 'регламент':    return add_business_days(btn, 7)        # производственный календарь РФ
    if mode == 'реалистичный': return next_business_day(btn + days(DELAY_P50))
    if mode == 'осторожный':   return next_business_day(btn + days(DELAY_P90))

# калибровка задержки по истории:
# 1) сначала сопоставь поступления с неделями продаж по сумме (см. «Сопоставление с банком»);
#    пробуй одну неделю, затем суммы 2–3 соседних недель с одной датой кнопки
# 2) задержку считай от кнопки той недели и по ступени, действовавшей для неё
delays = [(m.bank_date - button_date(m.week.end, stage_at(cycle.lag, m.week.start))).days
          for m in matches if m.week.start >= REGIME_FROM]   # только текущий режим
DELAY_P50, DELAY_P90 = percentile(delays, 50), percentile(delays, 90)

# суммы
carry = D(0)
for W in weeks:                                    # W — ISO-неделя; куски стыка месяцев склеены
    if W.closed:   amount = sum(r.bank_payment_sum for r in reports_of(W))   # все отчёты недели
    else:          amount = sum(accrued[d] for d in W.days)
    amount, carry = amount + carry, D(0)
    if amount <= 0:
        carry = amount; continue                   # выплаты нет; минус зачесть в следующую неделю (⚠️ проверить по ЛК)
    if not already_in_bank(W):
        lag = stage_at(cycle.lag, W.start)
        inflow_model[money_date(button_date(W.end, lag), mode)] += amount
```

`REGIME_FROM` — первая неделя продаж текущего регламента (сейчас 20.07.2026). Поступления эпохи ежедневного вывода (до 24.07.2026) для калибровки не используй. Если совпадений меньше 6, бери значения по умолчанию из раздела 8 и помечай их.

**Пример.** Неделя 07.09–13.09.2026. Отчёт приходит в понедельник 14.09. Кнопка: 13.09 + 14 = 27.09 (воскресенье), значит понедельник 28.09. По регламенту деньги придут до 07.10. Реалистично (кнопка + 13 дней = 11.10, воскресенье) — 12.10. От конца недели до денег проходит около 29 дней, а для продажи в середине недели — 4–5 недель.

**Сопоставление с банком** нужно, чтобы досрочно пришедший платёж не учесть дважды. Пары «модельная выплата ↔ банковское поступление» ищи внутри кабинета:
- по сумме ±0,5 % (шире, если между вами и ВБ есть посредник);
- в окне дат [−21; +30] дней;
- по всей истории, сначала для прошлых выплат, потом для будущих;
- каждой выплате — самое раннее подходящее поступление; если одиночная неделя не нашла пары, проверь сумму 2–3 недель, открытых одной кнопкой.

Будущая выплата, для которой нашлась пара, уже пришла: убери её из прогноза.

**Статусы заявки в ЛК** (по наблюдениям, в API их нет):
`Оплата обрабатывается` → (иногда) `Платёж передан в ручную обработку` (плюс 7 и более рабочих дней) → `Оплата успешно отправлена в банк` (деньги придут **на следующее утро**, это единственный точный прогноз) → `Оплата успешно проведена банком <дата>` (ЛК может показывать дату на день раньше зачисления).
Если ВБ отклонил заявку из-за закрывающих документов, он потом создаёт новую заявку на ту же сумму. Не считай это второй выплатой.
В API это видно так: `for_withdraw` растёт с 0 до X — кнопка открылась; `current` падает на сумму недели — заявка создана, но ещё не оплачена.

### 7.10 Долг ВБ и сверка с балансом

```python
debt[t0] = anchor   # t0 — дата первого снимка баланса; anchor = current + заявки, созданные, но не оплаченные;
                    # до t0 долг восстанавливай обратным ходом: debt[i-1] = debt[i] - accrued[i] + inflow[i]
debt[i] = debt[i-1] + accrued[i] - inflow[i]         # inflow: банк в прошлом, модель в будущем
```

- В устойчивом режиме долг примерно равен начислениям за 30–33 дня (4–5 недель).
- Отрицательный долг означает ошибку: двойной вычет, потерянную неделю или двойной учёт выплаты.
- Сверка (на снимках после t0, не в день якоря): `debt ≈ current + Σ заявок, созданных, но не дошедших до банка`, допуск ±10 %. ⚠️ Если разница стабильно равна начислениям текущей недели, в `current` неотчётная неделя не входит: сравнивай долг на конец последней отчётной недели (Приложение А, п. 15).
- Если выплата известна (`bankPaymentSum`), а сумма дневных начислений недели с ней расходится, подгони **начисление** последнего дня недели на разницу. Выплату не трогай, иначе появится фантомный приход.

### 7.11 Касса и кассовый разрыв

**Оплаты поставщику** по траншам (условия договора — параметры):

| Транш | Доля | База | Когда |
|---|---|---|---|
| Предоплата | 20 % | сумма заказа | в день заказа |
| Второй платёж | 30 % | сумма партии | в день прихода |
| Постоплата | 50 % | сумма партии | приход + 45 дней |

```python
supplier[i] = -(po_rub[i] * 0.20 + arrive_rub[i] * 0.30 + arrive_rub[i - 45] * 0.50)   # po_rub — сумма заказа поставщику (purchase order) в ₽ по курсу на дату платежа
# прошлое — факт оплат из банка; будущее — план закупщика или транши
# досрочная фактическая оплата гасит плановую: FIFO по сумме ±1 % в окне 14 дней
# плановую оплату ставить на день, когда её реально исполнят, а не на понедельник недели

cash[0] = bank_balance_today
for i in horizon:
    flow = (inflow_wb[i]                       # банк в прошлом / модель 7.9 в будущем
            + supplier[i]                      # отрицательное
            - ads_topups[i]                    # пополнения рекламного счёта (type 0/3), раздел 8
            - opex_day(i)                      # равномерно по дням или комком в названные владельцем дни
            - taxes[i] - loan_payments[i] - interest[i] + other_in[i])
    cash[i] = cash[i-1] + flow

gaps = [d for d in horizon if cash[d] < SAFETY]
if gaps:
    first, depth = gaps[0], min(cash[d] for d in horizon)
    recovery = next((d for d in horizon if d > first and cash[d] >= SAFETY), None)
```

Для каждого разрыва покажи дату, глубину, причину (какой платёж или какая задержка ВБ) и ближайший приход от ВБ после него. Сценарии «а что если»: ВБ задерживает выплаты на +7 или +14 дней; план заказов ±20 %; перенос платежа поставщику; новая закупка («хватит ли денег, когда закончится склад»); кредит.

### 7.12 Сквозной пример (условные числа)

**Параметры:** 100 заказов в день, средний чек до СПП 2 000 ₽, чистый выкуп 27 %, take 45 %, ДРР 10 % (вся реклама с баланса).

| Шаг | Расчёт | Итог |
|---|---|---|
| Выкупы в день | 100 × 27 % | 27 шт |
| Выручка до СПП | 27 × 2 000 | 54 000 ₽ |
| Удержания ВБ | −54 000 × 45 % | −24 300 ₽ |
| Реклама с баланса | −54 000 × 10 % | −5 400 ₽ |
| Начислено в день | 54 000 − 24 300 − 5 400 | 24 300 ₽ |
| Начислено за неделю | 24 300 × 7 | 170 100 ₽ |
| Деньги за неделю 07.09–13.09 | кнопка 28.09, деньги ~12.10 | +170 100 ₽ |
| Долг ВБ в устойчивом режиме | 24 300 × ~33 дня | ~800 000 ₽ |

**Касса по неделям** (условно): старт 300 000 ₽, приход от ВБ +170 000 в неделю, опекс −120 000 в неделю. Партия приходит 16.11: второй платёж −600 000 в этот день, постоплата −1 000 000 через 45 дней, 31.12. Продажи новой партии для наглядности не учтены.

| Неделя с | ВБ | Опекс | Поставщик | Касса на конец |
|---|---:|---:|---:|---:|
| 05.10 | +170 000 | −120 000 | — | 350 000 |
| 12.10 – 09.11 (5 нед.) | +850 000 | −600 000 | — | 600 000 |
| 16.11 | +170 000 | −120 000 | −600 000 | 50 000 |
| 23.11 – 21.12 (5 нед.) | +850 000 | −600 000 | — | 300 000 |
| **28.12** | +170 000 | −120 000 | −1 000 000 | **−650 000 ⚠️** |

Модель показывает разрыв 650 000 ₽ 31.12 уже в начале октября, **за 12 недель.** За это время можно перенести постоплату, взять кредит или ускорить продажи.

---

## 8. Параметры: значения по умолчанию и калибровка

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

| Параметр | По умолчанию (условно) | Как калибровать |
|---|---|---|
| Заказов в день | медиана за 28 дней | факт; план владельца приоритетнее |
| Средний чек до СПП | заказы ₽ / шт за 28 дней | факт; текущие цены (№ 21) для прогноза |
| Сезонность | 1,0 по всем месяцам | история за год или слова владельца; одежда обычно 0,8–1,3 |
| Чистая доля выкупа | одежда и обувь 25–40 %; другие категории часто 70–95 % | кривая по когортам (7.2) за 90 дней, по артикулам |
| Кривая выкупа | лесенка 20 % на 5-й день + 7 % на 13-й (медленнее примера 7.2; используй что-то одно) | 7.2, пересчёт раз в неделю |
| Параметры кривой | окно 90 дней, `max_lag` 45, задержка зрелости 3 дня (8 без дневных отчётов) | 7.2 |
| Возвраты после выкупа | 5–12 % выкупов | Возвраты / Продажи по детализации |
| Доля отказов | 1 − валовая доля выкупа (до возвратов) − потеряшки | 7.4, окно 6–8 недель |
| Медиана отказа | 3-й день | 7.4 |
| Обратная логистика ПВЗ → склад | 4–5 дней | разница «дата отказа → снова в остатке» |
| Лаг возврата на склад (комком) | 16 дней | ≈ точка 90 % отказов когорты |
| Потеряшки | 0–4 % | заказы без исхода в зрелых когортах |
| Потери на складе ВБ (`losses`) | 0 | по актам ВБ, если были |
| take (удержания без рекламы) | 45 % (диапазон 35–60 %) | 7.6, скользящее по 4 неделям |
| ДРР от выкупов | 10 % | 7.6; доля «с баланса» по `paymentType` |
| Доля рекламы с баланса (`share_from_balance`) | 100 % (при еженедельном выводе ВБ чаще удерживает рекламу из выплаты) | по `upd` за последние 4 недели |
| Пополнения рекламного счёта в будущем | `revenue × ДРР × (1 − share_from_balance)`, раз в неделю в понедельник | по `payments` (type 0/3) за 8 недель: средняя сумма и день недели |
| Коэффициент СПП k | по факту | Σ `retailAmount` / Σ `retailPriceWithDisc` |
| Лаг до кнопки | 14 дней → понедельник | новости ЛК; ступени по неделе продаж (4.5) |
| Задержка кнопка → деньги | P50 = 13 кал. дней, P90 = 20 | по банковской выписке (7.9) |
| Транши поставщику | 20 / 30 / 50 %, +45 дней | договор и факт оплат |
| Валюта поставщика | ₽; если контракт в валюте — курс на дату платежа (сценарий ±10 %) | владелец |
| Производство → транзит | 30 + 15 дней | история партий |
| Партия в пути, ETA прошла | среда следующей недели, пересчёт еженедельно, флаг «просрочено» | факт прихода |
| Партия не отгружена | плановая дата, но не раньше сегодня + 21 день | закупщик |
| Налоги | ставку, базу и даты спроси у владельца; по умолчанию — ставка владельца × выручка до СПП, платёж 28-го числа месяца после квартала | бухгалтер владельца |
| Опекс: даты | равномерно по дням; если владелец назвал дни выплат (зарплата, аренда), ставь комком в эти дни | владелец |
| Порог кассы (SAFETY) | 0 ₽ или 2 недели опекса | владелец |
| Горизонт | 180 дней | — |

---

## 9. Валидация (бэк-тест)

Прогноз можно показывать как надёжный, только если пройдены проверки.

| № | Проверка | Как | Допуск |
|---|---|---|---|
| 1 | Формула «Итого к оплате» | 7.1 на всех загруженных отчётах | ≤ 1 ₽ на каждом |
| 2 | Начисления за неделю | Для каждой из последних 8–12 закрытых недель: параметры, откалиброванные **только на данных до этой недели**, плюс фактические заказы → модельное начисление; сравнить с `bankPaymentSum` | по неделе ±10 %; сумма за 8 недель ±5 %; не больше 3 недель подряд с ошибкой одного знака |
| 3 | Выкупы, шт | те же недели, модель и факт | по неделе ±10 % |
| 4 | Кривая выкупа | Σ p против доли выкупа зрелых когорт по отчётам и воронке | ±2–4 п. п. |
| 5 | Даты выплат | модельная дата (реалистичная) против банковской за 8–12 недель | медиана отклонения ≤ 3 дней; суммы ±0,5 % |
| 6 | Долг ВБ | `debt` против `current` + незакрытых заявок | ±10 % |
| 7 | Касса прошлого | модельная касса против банковских остатков (если есть выписка) | ±5 % |

```python
def backtest(weeks_back=12):
    res = []
    for W in last_closed_weeks(weeks_back):
        params = calibrate(data_until=W.start - days(1))      # без подглядывания в будущее
        model  = simulate(params, orders=fact_orders, until=W.end,
                          last_sal=W.start - days(1), last_daily_report=W.start - days(1),
                          last_ads=W.start - days(1))   # выкупы, дневные отчёты и реклама недели W считаются неизвестными
        res.append((W, sum(model.accrued[d] for d in W.days), W.bank_payment_sum))
    err = [(m - f) / abs(f) for _, m, f in res if abs(f) > D('1000')]   # недели около нуля не делят ошибку
    return mape(err), bias(err), max_same_sign_run(err)
```

**Если проверка не пройдена**, возможные причины:

| Симптом | Вероятная причина |
|---|---|
| Начисление систематически выше факта на 20–50 % | не учтена реклама с баланса, или обнулилась логистика после переименования операции |
| Одна неделя из месяца выпадает | неделя на стыке месяцев, второй отчёт потерян |
| Выкупы в прогнозе выше факта | кривая по незрелым когортам или устаревшее окно; отмены не в знаменателе |
| Двойная выплата в кассе | дневные отчёты сложены с недельными, или не сработало сопоставление с банком |
| Долг ВБ отрицательный | двойной вычет комиссии или рекламы, потерянная неделя |
| Даты выплат систематически раньше факта | задержка по регламенту вместо P50 из истории; ступень цикла выбрана не по тому ключу (срок до кнопки — по неделе продаж, задержка денег — по дате кнопки); в калибровку попали поступления эпохи ежедневного вывода |

---

## 10. Что показать владельцу

Структура отчёта (все числа ниже условные):

**1. Ключевые числа**

```text
Доля выкупа (чистая, 90 дн.):        27 %   (медиана выкупа — 4-й день; 90 % выкупов — к 10-му)
Удержания ВБ без рекламы (4 нед.):   45 % выручки до СПП
ДРР от выкупов (4 нед.):             10 %
К выплате на 1 выкуп:                900 ₽ из 2 000 ₽ выручки
Долг ВБ сейчас (начислено, не пришло): ~800 000 ₽
Следующие 4 выплаты:                 12.10 — 170 000 · 19.10 — 165 000 · 26.10 — 172 000 · 02.11 — 160 000
Минимальный остаток кассы:           −650 000 ₽ 31.12 ⚠️ кассовый разрыв
Качество модели (бэк-тест 12 нед.):  ошибка начислений 6 % по неделе, 2 % за период
```

**2. Скорость выкупа:** накопленный процент выкупа по дням после заказа (0, 3, 7, 14, 21, 30) в целом и по топ-10 артикулам, плюс матрица последних 4 недель с отклонениями от нормы.

**3. Юнит-экономика:** таблица как в 7.5 по топ-артикулам — ₽ на выкуп и % выручки. Отдельно отметь артикулы с отрицательной маржой.

**4. Календарь поступлений от ВБ (8–12 недель):**

| Неделя продаж | Начислено | Источник суммы | Кнопка | Деньги (реалистично) | Деньги (осторожно) | Статус |
|---|---:|---|---|---|---|---|
| 07.09–13.09 | 170 100 | отчёт ВБ | 28.09 | 12.10 | 19.10 | заявка создана |
| 14.09–20.09 | 165 000 | отчёт ВБ | 05.10 | 19.10 | 26.10 | ждёт кнопку |
| 12.10–18.10 | 168 000 | модель | 02.11 | 16.11 | 23.11 | прогноз |

**5. Касса по неделям:** таблица как в 7.12 и график. Разрывы с датой, глубиной и причиной.

**6. Предупреждения и допущения:** какие параметры взяты по умолчанию; какие данные не получены; новые значения операций ВБ; артикулы, которые скоро закончатся на складе; изменения регламента.

**7. Вопросы владельцу:** чем уточнить модель (план, условия поставщика, задержки выплат).

---

## 11. Ограничения

- Модель не предсказывает изменения правил ВБ: комиссий, тарифов, графика выплат, ручной обработки платежей. Их можно только задать сценарием.
- Истории выплат в API нет, поэтому задержки калибруются по банку владельца. Без выписки даты прихода денег — оценка по регламенту, обычно слишком оптимистичная.
- Заказы в API хранятся 90 дней, лента заказов — 31 день. Более глубокую историю получишь, только если сохранял её раньше, или через отчёты (`orderDt`) и воронку.
- Для новых товаров без истории кривая и take берутся по похожим товарам или по тарифам ВБ (№ 18–19). Ошибка здесь выше.
- Акции, распродажи и сбои логистики ВБ меняют кривую выкупа за дни, а окно калибровки отстаёт.
- Остатки ВБ шумные, склады обезличены. Модель склада — это оценка, а не учёт. При одном общем остатке модель может «продавать» размеры, которых уже нет: считай остаток по артикулам или группам размеров.
- Налоги в модели упрощены: база и ставка зависят от режима продавца, их нужно уточнить у бухгалтера.
- Это инструмент планирования, а не бухгалтерский или налоговый учёт и не финансовая консультация.

---

## Приложение А. Что не подтверждено в API (проверить на данных)

1. Какая категория токена нужна для `sales-reports/*`: «Финансы», «Статистика» или обе. Проверь пробным вызовом `sales-reports/list` (`/ping` этого не покажет).
2. Заголовки `X-Ratelimit-*` и правило «ответ 409 = 5 запросов».
3. Часовой пояс `orderDt` и `saleDt` (суффикс `Z` в примерах). Метод проверки — 6.1, п. 1.
4. Что старый `reportDetailByPeriod` действительно отключён на сервере (из спецификации он удалён).
5. Смена значения «Логистика» → «Доставка» именно в строках `sellerOperName`.
6. Разбиение недельного отчёта на два на границе месяца.
7. Точная формула `bankPaymentSum` и место рекламы с «Баланса» в детализации.
8. Срок от отчёта до денег: через API не получить.
9. Соответствие старых snake_case-полей новым camelCase.
10. Правило «не суммировать daily и weekly» следует из логики двух представлений одних операций, а не из текста ВБ.
11. Что такое «базовый токен с секретом» и как его выпустить.
12. Смысл значений `paymentSchedule` (пример `-1`): сдвиг в днях, в неделях или код.
13. Что ВБ делает с отрицательным «Итого к оплате» недели (в модели минус переносится на следующую неделю).
14. Есть ли удержание за рекламу с «Баланса» в **дневных** отчётах (`ADS_IN_DAILY_ROWS`, 7.8).
15. Входят ли в `current` начисления текущей, ещё не отчётной недели (7.10).
16. Вычтены ли возвраты из `buyoutCount` воронки.
17. Знак `forPay` и `quantity` в строках «Возврат» (`RETURN_SIGN`, 7.1).
