Синтаксис хаоса
о канале

За два дня собрал сервис, который рисует QR на счетах. Самое интересное началось

11.09.2026, 16:54 · ⏱ 4 мин

За два дня собрал сервис, который рисует QR на счетах. Самое интересное началось, когда тест насрал в боевую базу.

Цальбург, от немецкого Zahlung, «оплата». Юрлицо получает из RetailCRM счёт в PDF, сканирует QR в приложении своего банка, и реквизиты, назначение и сумма подставляются сами. Формат по ГОСТ Р 56042-2014, тот же, что на квитанциях ЖКХ. Его понимают приложения всех крупных банков, договор с банком не нужен.

Как было. Строку ГОСТ собирал сам шаблон печатной формы на Twig, а картинку рисовал публичный api.qrserver.com. Реквизиты зашиты в шаблоны, по одному на каждое юрлицо. Сменил банк, лезь руками в Twig, и одна кривая цифра отправит деньги хуй знает куда. Чужому серверу при этом улетали реквизиты и сумма каждого счёта (Заебись, чётко! Секурно!). Текст счёта с QR никто не сверял, журнала, какой счёт ушёл с кодом, а какой без, не было вообще.

Готовый модуль в маркетплейсе RetailCRM стоит 490–990 рублей в месяц, держит одно юрлицо, и картинку всё так же рисует чужой сервер.

Как стало. Реквизиты живут в сервисе и правятся через интерфейс, каждое изменение пишется в журнал: кто, когда, что было, что стало. Удалить юрлицо нельзя, только выключить. На каждый запрос сервис проверяет контрольные разряды счетов по алгоритму ЦБ и контрольную сумму ИНН, сверяет ИНН и счёт из печатной формы со своими и только потом рисует код.

Всегда 200. Если что-то не сошлось, сервис отвечает кодом 200 и картинкой-заглушкой того же размера, где по-русски написана причина: «реквизиты не совпадают», «юрлицо выключено». PDF-рендерер RetailCRM при ошибке загрузки рисует пустое место, и бухгалтер в жизни не узнал бы, что счёт ушёл без кода. Проверил на живом: испортил цифру в печатной форме, пришла заглушка.

300 байт. Стандарт режет строку на 300 знаках. В UTF-8 русская буква весит два байта, а наименование получателя бывает длиннющим. В худшем случае у одного из юрлиц выходит 299 байт, и на «за смартфон» в назначении места нет совсем. Слово о товаре сервис берёт из дерева товарных групп по самой дорогой позиции, девяти слов хватило на весь ассортимент. На обычном счёте у самого тесного юрлица запас 26 байт, это 12 русских букв. На длинном счёте слово выкидывается, в журнале остаётся отметка.

А теперь как тест насрал в бой. После запуска я попросил полный обзор кода. Группа агентов прошла его с восьми точек зрения, из 41 кандидата подтвердилось 10 находок. Один из проверяющих агентов запустил свой тест из временной папки. Подмена пути к базе живёт в настройках тестов и действует только внутри их папки, поэтому тест открыл боевую базу, записал туда два тестовых юрлица и переписал восемь строк справочника групп. Реальная группа «Dyson» стала «Apple» 🤣

Агент сам заметил, остановился и доложил. Claude Code снял копию повреждённой базы, сверил каждую строку с живым справочником RetailCRM, только чтением, и вернул всё одной транзакцией.

Смешно другое. Агенту было сказано «временные файлы клади вот сюда» и «проверь тестом». Две инструкции вместе дали запись в боевую базу. Теперь боевой файл сам отказывается открываться из любого теста и любого скрипта вне процесса сервиса, и это проверено на точном сценарии инцидента. Запрет в коде держит, инструкция нихуя не держит.

По цифрам: два дня от задачи до боя, 64 коммита, около 1800 строк кода и 1900 строк тестов, 213 тестов. Тесты декодируют готовый PNG обратно в строку и проверяют, что в картинке ровно та строка, которая должна там быть. Сейчас перевожу печатные формы на Цальбург и гоняю приёмку: печать, сканирование банками, пачка счетов.

Где у вас лежат банковские реквизиты для счетов: в шаблоне документа или там, где видно, кто их последний раз менял?

✍️ Синтаксис хаоса
Подпишись: TG / DZEN / VK
44
Оригинал в Telegram: @syntax_of_chaos
📚 Читать дальше