/rlebe[dev].dev
Виджет маркетплейса МойСклад · newopiu.pro-terminal.ru

МойСклад знает всё про ваши операции. И ничего — про вашу прибыль

ОПиУ, ДДС и управленческий баланс — прямо внутри МойСклада, из живых данных, по вашим статьям доходов и расходов. Ставится из маркетплейса в один клик: без отдельного логина, без выгрузок в Excel и без «сведём в конце месяца».

ОПиУ · ЯНВАРЬ — ИЮНЬ · ВСЕ ОРГАНИЗАЦИИ ₽, ТЫС.
Выручка18 420
Себестоимость продаж−11 240
Валовая прибыль7 180
Операционные расходы−4 415
Налоги и сборы−905
Чистая прибыль1 860
3
отчёта из коробки
5
разрезов за один прогон
1 клик
установка из маркетплейса
123
теста в проекте
Задача

Учётная система считает документы. Собственник считает деньги

Отгрузки, приходы, платежи и остатки в МойСкладе есть — а ответа на вопрос «сколько мы заработали в мае и почему меньше, чем в апреле» нет. Его собирают вручную: выгрузка, сводная таблица, неделя работы бухгалтера и цифра, которой уже месяц.

ОПиУ
Прибыли и убытки · P&L

Выручка, себестоимость, валовая прибыль, операционные расходы по вашим статьям, налоги, чистая прибыль. С раскрытием каждой строки до документов.

ДДС
Движение денег · Cash Flow

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

Баланс
Управленческий баланс

Запасы по складам с роллапом вложенности, деньги по счетам, дебиторка и кредиторка по каждому юрлицу отдельно. Активы минус пассивы.

Скорость

Разрез переключается мгновенно, потому что он уже посчитан

Наивная реализация ходит в API МойСклада заново на каждое переключение — и пользователь ждёт по полминуты за каждый клик. Здесь все разрезы предсчитываются за один прогон, а переключение — это выбор из готового результата на клиенте.

Итог По периодам По организациям По проектам По каналам продаж
Один проход

Документы выкачиваются один раз

Дальше они фильтруются в памяти на каждую колонку. По сети на колонку остаётся только себестоимость — то, что МойСклад считает сам и отдать пачкой не может.

Прогресс

Видно, что происходит, и первые цифры — раньше

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

Ваши статьи

Справочник статей — в самом МойСкладе

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

Комментарии

Заметки к строкам отчёта

«Почему в марте маркетинг вырос вдвое» — записано рядом с цифрой и остаётся там при следующем построении отчёта.

Инженерные решения

Четыре места, где «работает у меня» ломается у клиента

Виджет молча выкидывал пользователей Safari и Яндекс.Браузера

Приложение живёт в iframe внутри МойСклада, значит его cookie для браузера — сторонняя. Safari без поддержки CHIPS и «Протект» Яндекса просто не сохраняли её — даже с SameSite=None; Secure; Partitioned. Пользователь входил и тут же получал 401.

Сессия уехала из cookie в подписанный HMAC-SHA256 токен: сервер отдаёт его в теле ответа на вход, вкладка держит в памяти и шлёт заголовком Authorization: Bearer. Авторизация перестала зависеть от политики сторонних cookie в каком бы то ни было браузере.

Токен живёт только в памяти вкладки — не в localStorage. Тариф в сессию не кладётся: сессия отвечает на вопрос «кто пришёл», а не «что ему продано».

Лимиты МойСклада считаются по токену, а не по процессу

Веб-процесс тянет фильтры, воркер считает отчёт, рядом идёт второй прогон по тому же аккаунту — и каждый держит свой счётчик частоты. Их сумма уверенно пробивает лимит МС, и клиент получает 429 в середине отчёта.

Ограничитель вынесен в Redis: атомарный токен-бакет, общий для всех процессов, с ключом на аккаунт. Если Redis не сконфигурирован или лёг — деградация в локальный бакет, а не отказ.

Поверх — ретраи на 429 и 5xx с экспоненциальной задержкой и пагинация с ограниченной конкурентностью. Долгий отчёт дочитывается до конца, а не падает на середине.

Маркетплейс присылает вебхуки повторно. Дублей быть не должно

Реализованы обе стороны Vendor API 1.0: активация, смена тарифа, продление, приостановка и удаление установки. Входящие вебхуки проверяются по JWT — подпись HS256, срок жизни и одноразовость jti; повторная доставка отсекается идемпотентностью по X-Lognex-RequestId.

Тариф — это таблица соответствия «идентификатор тарифа → набор доступных отчётов». Три отчёта — три независимых флага: в личном кабинете вендора они продаются и по отдельности, и комбинациями, поэтому вывести один из других нельзя.

«Приостановлено» гасит токен, но сохраняет подписку: вернувшийся клиент не начинает с нуля.

Два унаследованных сервиса стали одним

Проект достался как связка отдельного бэкенда и отдельного фронта. Версия, которая работает сейчас, переписана по чистому: доменное ядро вынесено в слой без единой зависимости от фреймворка, интерфейс — FastAPI с серверными шаблонами и лёгким JS.

Ни одной внешней библиотеки с CDN: HTMX, Alpine.js, Tabulator и ECharts лежат в образе. Приложение поднимается и работает там, где до внешних сетей достучаться нельзя.

Один процесс вместо двух сервисов, одна точка сборки зависимостей, одна реализация разбиения периодов вместо двух разошедшихся.

Как это устроено

Ports & Adapters: расчёт прибыли не знает ни про HTTP, ни про базу

Зависимости идут строго снизу вверх. Поэтому логика управленческого баланса и сборки ОПиУ покрывается юнит-тестами без единого внешнего сервиса — а тесты гоняются на SQLite и брокере в памяти.

domain/
Сущности и порты. Dataclass'ы и интерфейсы. Ноль зависимостей — ни от FastAPI, ни от SQLAlchemy, ни от МойСклада.
application/
Use-case'ы. Расчёт ОПиУ, ДДС и баланса, статьи, группы, фильтры, тарифы, синхронизация справочников. Чистые вычисления отделены от выборки данных.
infrastructure/
Реализации портов. HTTP-клиент МойСклада с ретраями и лимитером, репозитории на SQLAlchemy 2 async, фоновые задачи taskiq поверх Redis.
web/
FastAPI. Роутеры, сессии, Jinja2, статика. Берёт готовые сервисы из контейнера и ничего не знает о конкретных реализациях.
Качество

123 теста, ruff и mypy в одном make check

Тесты покрывают ядро расчётов, слой представления, вебхуки вендора, лимитер частоты и фоновые задачи — и не требуют ни Postgres, ни Redis, ни доступа к МойСкладу.

Развёртывание

Пять сервисов, одна команда

Postgres, Redis, миграции, API и воркер — в изолированной сети; наружу торчит только порт API. Миграции применяются автоматически перед стартом.

Стек

FastAPI · SQLAlchemy 2 async · taskiq

Ядро
Python 3.11FastAPIPydantic 2pydantic-settingsloguru
Данные
PostgreSQLSQLAlchemy 2 asyncasyncpgAlembicRedis
Интеграция
МойСклад JSON API 1.2Vendor API 1.0httpxPyJWT
Интерфейс
Jinja2HTMXAlpine.jsTabulatorECharts
Дальше

Нужен виджет в маркетплейс — или отчётность, которой сейчас нет?

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

Разбор вашей задачиЧто считается из имеющихся данных, а чего в них физически нет.
Приложение маркетплейсаVendor API, тарифы, вебхуки, вход из iframe — полный жизненный цикл установки.
Управленческая отчётностьОПиУ, ДДС, баланс по вашим статьям — на вашей учётной системе.
Обсудить проект →