Как за оставшиеся восемь дней собрать в MAX продукт, который ведёт электронную транспортную накладную от отгрузки в учётной системе до подписанного четвёртого титула, что в нём будет настоящим, что моделью, и что понадобится после хакатона, чтобы закрыть задачу целиком.
Полностью означает три уровня, и честная презентация показывает все три: что работает на хакатоне, что включается на пилоте и что требует внешних сторон. Жюри ставит баллы за первый уровень и за понимание двух других.
| Возможность | MVP хакатона, 30.09 | Пилот на заводе | Продакшен |
|---|---|---|---|
| Отгрузка из учётной системы | живое: МойСклад по API или 1С по OData, плюс эмулятор | ERP клиента через тот же адаптер | любая система через интерфейс адаптера |
| Черновик Т1 и проверка реквизитов | живое: правила формата ФНС, ИНН, масса, адреса, доверенность | то же | полная XSD-валидация формата ЕД-7-26/1065@ |
| Передача титулов оператору и ГИС ЭПД | модель: эмулятор оператора | тестовый контур Диадока или песочница Астрала по договору | боевой оператор из реестра Минтранса, роуминг |
| Подпись Т1 от организации | модель: заглушка подписи в эмуляторе | УКЭП компании через криптопровайдер или облачную подпись оператора, МЧД диспетчера | то же плюс ротация сертификатов |
| Подпись водителя и получателя | живое: PDF титула подписывается в «Госключе» через бота MAX; в эмуляторе фиксируется факт | ПЭП или УНЭП «Госключа» в приложении оператора по app-to-app | интеграция с «Госключом» по API ЕПГУ и СМЭВ, 2–4 недели с Минцифры |
| QR-код для проверки на дороге | модель: картинка с идентификатором эмулятора | настоящий QR из приложения оператора | QR из ГИС ЭПД через API оператора (событие с вложением QR) |
| Уведомления, эскалации, сводка | живое | живое | живое |
| «Нужна ли накладная» | живое: дерево по разъяснениям Минтранса и приказу № 262 | то же | обновление при выходе новых приказов |
@maxhub/max-ui, PostgreSQL 16, миграции через Drizzle или Prisma. Официальный TypeScript SDK бота из репозитория max-messenger/max-bot-api-client-ts. Никаких закрытых библиотек, все зависимости с версиями в package-lock.docker compose up поднимает шесть сервисов. Наружу нужен HTTPS: бот получает вебхуки, мини-приложение открывается по URL. На хакатон свой сервер с nginx или Caddy, домен вида epd.<домен>, сертификат Let's Encrypt. Держать до 14 октября.request_contact присылает вложение с полем hash, по нему сервер сверяет, что контакт настоящий.https://max.ru/<бот>?start=trip_<id> приходит в bot_started.payload, или диспетчер выбирает водителя из привязанных. Карточка: адреса, груз, контакт получателя, чек-лист «QR должен быть до выезда». Кнопка «Погружено» с request_geo_location (параметр quick) фиксирует точку и время, эмулятор оператора формирует Т2 и QR.openMaxLink ведёт в @goskey_bot, водитель отправляет файл, выбирает УНЭП, получает подписанный файл и пересылает его нашему боту. Бот принимает вложение file, сохраняет и переводит титул в «подписан водителем». В продакшене этот шаг делает приложение оператора по app-to-app.| Таблица | Ключевые поля |
|---|---|
| company | id, name, inn, kpp, role (shipper | carrier | consignee), erp_kind (moysklad | odata | emulator), erp_credentials (шифрованы), epd_operator_kind (emulator | diadoc | taxcom) |
| person | id, company_id, max_user_id, phone (из вложения контакта), role (dispatcher | driver | receiver | admin), poa_valid_to (срок МЧД), driver_license |
| shipment | id, company_id, erp_id, erp_number, consignee_inn, loading_address, unloading_address, planned_at, cargo (места, масса брутто, описание), vehicle (регномер, тип), driver_id, raw (снимок из ERP) |
| waybill | id, shipment_id, number, date, state (см. схему), operator_doc_id, mt_id, qr_file_id, kl_id, requires_waybill (результат дерева), reason |
| title | id, waybill_id, index (1–8), function (reception | delivery | cost | readdress | relay), status (draft | sent | signed | rejected), signed_by_person_id, signature_kind (ukep | unep_goskey | pep | emulated), signed_at, xml_file_id, signed_file_id, file_name (ON_TRNACLGROT_… по формату) |
| event | id, waybill_id, kind, payload, source (erp | operator | bot | timer), created_at — журнал, из него строится лента в мини-приложении |
| outbox | id, target (max | erp | operator), payload, attempts, next_try_at, last_error — очередь с экспоненциальным повтором, идемпотентность по ключу события |
| timer | id, waybill_id, kind, fire_at, fired — эскалации; обрабатывает воркер раз в минуту |
Интерфейс адаптера один для всех: listShipments(since), getShipment(id), subscribe(webhookUrl), writeBack(shipmentId, status, comment). Три реализации.
entity/demand: контрагент (agent), организация, склад, адрес отгрузки, позиции с количеством и весом из карточек товара. Перевозчик, водитель и машина берутся из дополнительных полей отгрузки (заводим три атрибута) или вводятся диспетчером в карточке. Вебхуки создаются через entity/webhook на entityType: demand, действия CREATE и UPDATE; доступны на платных и пробных тарифах, лимит запросов около 45 за 3 секунды, так что после вебхука читаем документ одним запросом. Обратная запись: комментарий и статус отгрузки «накладная Т4 подписана».Document_РеализацияТоваровУслуг с табличной частью Товары, ключами контрагента и организации; вебхуков нет, поэтому адаптер опрашивает изменения по дате с курсором. Базовая авторизация, HTTPS./demo отгрузка в боте создаёт отгрузку именно здесь. Данные: обезличенный завод масел, 20 контрагентов, 5 машин, 6 водителей.Интерфейс адаптера повторяет реальный поток титулов, чтобы боевой оператор потом встал без переделки ядра: createT1(waybill) → operatorDocId, signAndSend(titleIndex, xml, signature), onDriverConfirm(cb), getStatus(docId) → {stage, mt_id, qr}, reject(docId, reason), readdress(T7), relay(T8).
sent → signed → registered, генерирует mt-id и PNG с QR, в котором закодирован идентификатор. Имена файлов по формату ФНС: ON_TRNACLGROT_… для Т1, ON_TRNACLPPRIN_… для Т2, ON_TRNACLGRPO_… для Т3, ON_TRNACLPVYN_… для Т4. Ручки «хаоса»: задержка QR на N минут, недоступность на M минут, отказ в Т2, чтобы показать обработку ошибок.diadoc-api-test.kontur.ru, два тестовых ящика через форму easyregistration, ключ разработчика и тестовый сертификат по заявке. Методы: GenerateTitleXml с documentTypeNamedId=LogisticsWaybill, функции reception, delivery, cost, версия kl_trn_mt_05_01; PostMessage V3 для Т1 и Т3, PostMessagePatch V4 для Т2, Т4, Т6; событие KlDriverConfirm с данными водителя; QR через GetEntityContent V4 по вложению с NamedId = QR; поиск по mt-id в SearchDocflows. Отказ: GenerateSignatureRejectionXml.Черновик Т1 собирается из тех же блоков, что и в реальном API: сведения о грузоотправителе и грузополучателе, груз с массой и упаковкой, перевозчик, водитель с номером удостоверения и телефоном, машина с регномером, время и адрес погрузки, указания отправителя, подписант. Полная проверка по XSD формата ЕД-7-26/1065@ в MVP заменяется проверкой обязательных полей, XSD берётся из документации оператора на пилоте.
| Механика | Как используем | Что в API |
|---|---|---|
| Вебхук бота | Все обновления в Bot Gateway; секрет в заголовке проверяется на каждом запросе | POST /subscriptions с url, secret (заголовок X-Max-Bot-Api-Secret), update_types; long polling GET /updates только для отладки |
| Кнопки | Действия по накладной, привязка телефона, точка погрузки, открытие мини-приложения | callback с payload; request_contact; request_geo_location с quick; open_app с web_app (публичное имя бота) и payload до 512 символов; link; до 7 кнопок в ряду |
| Ответ на нажатие | Меняем сообщение на «согласовано» или шлём одноразовое уведомление | message_callback с callback_id, payload, user; POST /answers с message или notification |
| Deep link | Рейс водителю, ссылка получателю | ?start= приходит в bot_started.payload до 512 символов; ?startapp= для мини-приложения, читается из initDataUnsafe.start_param |
| Контакт и геолокация | Проверяем подлинность контакта, фиксируем точку погрузки | вложение контакта с hash и max_info; вложение location с latitude, longitude |
| Файлы | QR-картинка и PDF титула в чат | POST /uploads типов image и file, токен вложения в POST /messages |
| Мини-приложение | Списки перевозок, карточка, настройки; авторизация без логина | Bridge window.WebApp, проверка initData на сервере; requestContact, openMaxLink на @goskey_bot, BackButton, platform для fallback в web |
Проверка initData по официальной документации: секрет равен HMAC-SHA256("WebAppData", BOT_TOKEN), из строки запуска исключается hash, значения декодируются, пары сортируются по имени и склеиваются через перевод строки, подпись HMAC-SHA256(secret, строка) сравнивается с hash в hex, а auth_date в секундах Unix проверяется на свежесть. Строку передаём в заголовке X-Max-Init-Data при каждом запросе к API.
const secret = hmacSha256("WebAppData", BOT_TOKEN);
const params = new URLSearchParams(initData);
const hash = params.get("hash"); params.delete("hash");
const line = [...params.entries()].sort(([a],[b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${decodeURIComponent(v)}`).join("\n");
const ok = timingSafeEqual(hex(hmacSha256(secret, line)), hash)
&& now() - Number(params.get("auth_date")) < 24 * 3600;
docker compose up --build поднимает postgres, api, bot, web (статика за nginx), erp-emulator, epd-emulator. Сборка укладывается в 5 минут: многослойные образы, кэш npm, без тестовых зависимостей в рантайме./demo отгрузка создаёт отгрузку в эмуляторе учётной системы, /demo роль водитель переключает роль текущего пользователя, /demo сбой оператора 5 включает недоступность на 5 минут, /demo ночь сдвигает таймеры, чтобы эскалации сработали при проверке..env; в репозитории .env.example; рабочие значения на первом слайде презентации.# compose.yaml (скелет)
services:
postgres: { image: postgres:16, env_file: .env, volumes: [pg:/var/lib/postgresql/data] }
api: { build: ./apps/api, env_file: .env, depends_on: [postgres], ports: ["8080:8080"] }
bot: { build: ./apps/bot, env_file: .env, depends_on: [api] }
web: { build: ./apps/web, ports: ["8081:80"] } # статика мини-приложения
erp-emulator: { build: ./apps/erp-emulator, ports: ["8090:8090"] }
epd-emulator: { build: ./apps/epd-emulator, ports: ["8091:8091"] }
volumes: { pg: {} }
# .env.example
MAX_BOT_TOKEN= # выдаёт организатор, в репозиторий не класть
MAX_WEBHOOK_SECRET= # 5–256 символов, проверяется в заголовке
PUBLIC_BASE_URL=https://epd.example.ru
DATABASE_URL=postgres://app:app@postgres:5432/app
ERP_KIND=emulator # emulator | moysklad | odata
MOYSKLAD_TOKEN=
ODATA_URL= ODATA_USER= ODATA_PASSWORD=
EPD_OPERATOR=emulator # emulator | diadoc
ENCRYPTION_KEY= # для токенов учётных систем в базе
README по двенадцати пунктам Задания: назначение; основной сценарий; состав и архитектура (схема из раздела 2); одна команда запуска; параметры и переменные окружения; порты 8080, 8081, 8090, 8091; зависимости; внешние сервисы (MAX, учётная система, эмуляторы); работа с данными и тестовыми данными; пошаговая проверка с ожидаемым поведением; известные ограничения (эмулятор оператора, заглушка УКЭП, без проверки подписи «Госключа»); остановка и перезапуск.
Три роли в команде: интеграции и ядро (два человека), бот и мини-приложение, продукт и документация. Заморозка функционала вечером 27 сентября.
/start через вебхук, мини-приложение открывается и проходит проверку initData, seed данных завода./answers..env.example, схема архитектуры, известные ограничения, видео сценария на 3 минуты, презентация PDF со служебным первым слайдом, замер сборки.