Live · Logistics · 2024 – 2025 · 3 терминала3 terminals

Container Terminal Bot —
учёт контейнеров из Telegram.
Container Terminal Bot —
container tracking via Telegram.

Заменил расшаренный Excel-файл на трёх контейнерных терминалах. ISO 6346 валидация, биллинг на сервере, xlsx-отчёты прямо в чат. p50 латентность 78 мс. Replaced a shared Excel file across three container terminals. ISO 6346 validation, server-side billing, xlsx reports delivered straight to chat. p50 latency 78 ms.

Telegram · @container_bot live
user> /in MSKU 304221 5 40HC
→ Контейнер принят на хранение
  ID:       MSKU3042215 · 40' HC
  Терминал: Sergeli A-2
  Дата:     2026-04-12 14:22
  Тариф:    $4.20 / сутки
  Чек:      #INV-04-2871

user> /report 2026-04
→ Готовлю отчёт...
  📎 sergeli_april_2026.xlsx · 312 KB
  📊 1 247 движений · 38 клиентов
  💰 $42 318 биллинга

user> /in ABC-12
⚠ ABC-12 не похож на ISO 6346.
  Формат: 4 буквы + 7 цифр.
  Пример: MSKU 304221 5

§ 01О чём проектWhat this is

Контейнерный оператор имеет три терминала в Ташкенте и Сергелях. На каждом сидит диспетчер с ноутбуком, на котором — расшаренный по Dropbox контейнеры.xlsx. Когда заезжает контейнер, диспетчер открывает файл, вписывает строку. Когда выезжает — вписывает дату выезда. Раз в месяц бухгалтерия считает биллинг ручкой. The container operator runs three terminals in Tashkent and Sergeli. Each one has a dispatcher with a laptop sharing containers.xlsx via Dropbox. When a container arrives, the dispatcher opens the file and adds a row. When it leaves — enters the departure date. Once a month, accounting calculates billing by hand.

Проблемы Excel-файла: два диспетчера правят одновременно — потеря строк. Кто-то опечатывается в номере контейнера — потеря денег. Биллинг считается раз в месяц — клиенты спорят с цифрами через полтора месяца. Excel file problems: two dispatchers editing simultaneously — lost rows. Someone mistypes a container number — lost revenue. Billing is calculated once a month — clients dispute figures six weeks later.

Задача: заменить картину на что-то, что диспетчер сможет использовать с телефона, без установки приложений и без обучения. Telegram — очевидный выбор, потому что все уже в нём сидят. Goal: replace this setup with something a dispatcher can use from a phone, no app installs, no training needed. Telegram was the obvious choice — everyone already uses it.

§ 02Как устроеноHow it's built

FSM-первый дизайн. Каждая операция (приём, выдача, перемещение, корректировка) — короткий стейт-машинный сценарий из 3 – 5 шагов. Aiogram 3 даёт FSM из коробки, и это идеально ложится на Telegram-UX, где каждое сообщение — шаг. FSM-first design. Each operation (check-in, release, transfer, correction) is a short state-machine scenario of 3–5 steps. Aiogram 3 provides FSM out of the box, and it maps perfectly to Telegram UX where each message is a step.

architecture · operation flow
┌──────────────────────────────────────────────────────────┐
│  Telegram Update                                          │
│       │                                                   │
│       ▼                                                   │
│  ┌─────────┐    invalid    ┌──────────────────────────┐   │
│  │ Router  │ ─────────────▶│ Helpful error + retry    │   │
│  └─────────┘               └──────────────────────────┘   │
│       │                                                   │
│       ▼                                                   │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐                    │
│  │  FSM    │─▶│ Use-    │─▶│ Domain  │                    │
│  │ scene   │  │ case    │  │ + Rules │                    │
│  └─────────┘  └─────────┘  └────┬────┘                    │
│       ▲                          │                        │
│       │                          ▼                        │
│       │      ┌──────────────────────────────────────┐     │
│       └──────│ Postgres · Outbox · Audit log        │     │
│              └──────────────────────────────────────┘     │
└──────────────────────────────────────────────────────────┘
Рис. 1 · Поток одной операции. Доменный слой не знает про Telegram.Fig. 1 · Flow of a single operation. The domain layer knows nothing about Telegram.

Доменный слой ничего не знает про Telegram. Это дало возможность через полгода добавить web-админку для бухгалтерии без переписи логики — та же FastAPI поверх тех же use-cases, другой UI. The domain layer knows nothing about Telegram. This made it possible to add a web admin panel for accounting six months later without rewriting any logic — the same FastAPI on top of the same use-cases, different UI.

ISO 6346 валидация на этапе вводаISO 6346 validation at input

Номер контейнера — это 4 буквы + 7 цифр, где последняя — check digit по стандарту ISO 6346. Считается через умножение позиций на степени двойки. Самая частая ошибка диспетчера — перепутать B и 8 или 0 и O. Валидация на входе ловит 100% таких опечаток. A container number is 4 letters + 7 digits, where the last one is a check digit per ISO 6346. It's calculated by multiplying positions by powers of two. The most common dispatcher mistake is confusing B with 8 or 0 with O. Input validation catches 100% of such typos.

domain/container_number.py iso 6346
def validate_iso6346(number: str) -> ContainerNumber:
    # MSKU 304221 5 — 4 letters + 6 digits + check
    norm = number.upper().replace(" ", "")
    if not _PATTERN.match(norm):
        raise InvalidContainerNumber(norm, reason="format")

    owner, serial, check = norm[:4], norm[4:10], norm[10]
    expected = _check_digit(owner + serial)
    if int(check) != expected:
        raise InvalidContainerNumber(
            norm, reason="check_digit",
            expected=expected, got=int(check),
        )
    return ContainerNumber(owner=owner, serial=serial)

§ 03Биллинг как доменBilling as a domain

Биллинг — это всегда соблазн посчитать в Excel. Соблазн нужно задушить рано, потому что «один клиент с нестандартным тарифом» превращается в «пятьдесят клиентов с разными правилами», и Excel становится бомбой замедленного действия. Billing is always tempting to calculate in Excel. That temptation must be killed early, because "one client with a custom tariff" turns into "fifty clients with different rules," and Excel becomes a ticking time bomb.

В доменном слое: Tariff, TariffPeriod, Discount, PartialDay-логика. Любая сумма пересчитывается на лету — я нигде не храню готовое число. Хранится Movement с точными timestamp'ами входа и выхода, а сумма — функция от состояния тарифной сетки на момент движения. In the domain layer: Tariff, TariffPeriod, Discount, PartialDay logic. Every amount is recalculated on the fly — I never store a pre-computed number. What's stored is a Movement with exact entry/exit timestamps, and the amount is a function of the tariff schedule at the time of the movement.

Хранить вычисленное число — значит, гарантированно расходиться с реальностью через три месяца, когда поменяется тариф задним числом. Storing a computed number means guaranteed divergence from reality three months later, when a tariff is changed retroactively.

Что это даёт бизнесу: счёт за хранение всегда сходится с реальным движением контейнеров — даже если тариф пересмотрели задним числом. Никаких «в Excel посчитали одно, клиент увидел другое» и споров по оплате. What this means for the business: the storage invoice always matches the real movement of containers — even if a tariff is revised retroactively. No more "Excel said one thing, the client saw another" and billing disputes.

§ 04ЦифрыNumbers

3k+ CONTAINERS / Y
78ms RESPONSE
3 TERMINALS
0 DATA LOSS

§ 05Что я вынесLessons learned

(а) Telegram — нормальная замена админке для небольших ops-команд. Если все уже в нём сидят, бот окупается в первые две недели. Главное — не строить из него «настоящее приложение» с inline-клавиатурами на десять рядов. (a) Telegram is a viable admin panel replacement for small ops teams. If everyone is already using it, the bot pays for itself in the first two weeks. The key is not to build a "real application" with ten rows of inline keyboards.

(б) FSM в aiogram 3 — идеальное соответствие тому, как человек ведёт диалог в мессенджере. Шаг — сообщение, отмена — /cancel, валидация — сразу. (b) FSM in aiogram 3 is a perfect match for how people converse in a messenger. Step — message, cancel — /cancel, validation — instant.

(в) Биллинг считать как функцию от состояния, а не как поле в БД. Это спасло от двух историй вида «тариф поменялся неделю назад, давайте пересчитаем». (c) Calculate billing as a function of state, not as a field in the DB. This saved us from two incidents of the "tariff changed a week ago, let's recalculate" variety.

Нужно похожее решение для вашего бизнеса?Need a similar solution for your business?

Опишите задачу — отвечу в течение дня, оценку дам бесплатно.Describe your task — I'll reply within a day, the estimate is free.