Что за задача
Компания обязана регистрировать акты выполненных работ в государственной информационной системе. Руками это открыть кабинет, заполнить форму, подписать электронным ключом — и так на каждый акт. Мы делали мост: страница в нашей админке отдаёт команду, локальный агент подписывает её ключом владельца и разговаривает с государственной системой.
Ключевое ограничение задачи, определившее всю архитектуру: ключ подписи, ПИН и пароль кабинета не должны покидать компьютер владельца. Отсюда локальный агент, слушающий порт только на самой машине, а не облачный сервис с хранилищем секретов.
Через пунктир секреты не проходят ни в одну сторону. Наружу уходит уже подписанный документ, обратно — либо идентификатор, либо конкретные нарушения контроля. Цена решения — агент надо запустить руками; выигрыш — нет централизованного хранилища ключей, то есть нет ни новой точки отказа, ни нового предмета разговора с проверяющим.
Что пришлось выяснить опытным путём
Ни один из пунктов ниже не выводится из чтения описания сервиса. Каждый стоил прогонов.
- Вход — двухступенчатый и с двумя авторизациями одновременно. Сначала запрашивается тикет, тикет подписывается ключом, и только потом открывается сессия — причём в запросе нужны и заголовок с логином-паролем кабинета, и тело с подписанным тикетом. Любой один из двух — отказ.
- Перед входом надо закрыть чужую сессию. Профиль допускает одну активную сессию. Если владелец открыл кабинет в браузере, агент войти не сможет — и наоборот. Пока это не выяснено, поведение выглядит как случайные сбои.
- Пространство имён решает, ответит сервер или промолчит. С правильным — работает. С похожим, но другим — сервер отдаёт пустой успешный ответ. Не ошибку, не отказ: код успеха и пустое тело. Отладка такого без подсказки занимает дни.
- Версию защищённого соединения приходится задавать принудительно. Иначе соединение обрывается на середине без внятной причины.
- Подпись считается только по той сериализации, которую делает сама библиотека сервера. Собрать тот же документ строкой или стандартным механизмом подписи XML — получить «ошибка подписи». Проверяющая сторона сверяет со своим представлением документа, а не с текстом, который вы отправили.
Три правила из пяти — в коде. Адреса, имена и версии обезличены, механика настоящая
// ── 1. Вход: две авторизации ОДНОВРЕМЕННО ────────────────────────────────
String ticket = api.requestTicket(taxId); // шаг 1: получить тикет
String signed = signer.sign(ticket, keyStore, pin); // шаг 2: подписать локально
Session s = api.login(
authHeader(cabinetLogin, cabinetPassword), // нет заголовка — отказ
body(signed) // нет тела — тоже отказ
);
// ── 2. Версия защищённого соединения — принудительно ─────────────────────
// «Договориться самим» не выходит: соединение рвётся на середине,
// и в логе нет ничего, кроме обрыва.
SSLContext ctx = SSLContext.getInstance("TLSv1.2"); // версия явно, не "TLS"
// ── 3. Подпись — по сериализации ИХ библиотеки, не по нашей строке ───────
// byte[] wrong = document.toString().getBytes(); // «ошибка подписи»
byte[] canonical = serverLib.marshal(document); // только так
String signature = signer.sign(canonical);Четвёртое правило в код не помещается, потому что это не строка, а поведение: пространство имён должно совпадать с ожидаемым дословно. С похожим, но другим сервер отвечает кодом успеха и пустым телом — клиент считает, что всё прошло, а на той стороне не появилось ничего.
Общее у всех пяти — сервер не говорит, что не так. Он отвечает пустотой, обрывом или общей формулировкой. Это и есть настоящая стоимость интеграции: не написать запрос, а построить гипотезу, почему тишина, и проверить её. Модель здесь помогает ровно так же, как человеку — перебирать варианты быстрее, но не угадывать за сервер.
Отдельный слой — правила приёмки на той стороне
Даже когда запрос уходит и подпись принята, документ может быть отклонён форматно-логическим контролем. Правила выясняются так же — отказом:
- дата выписки должна быть сегодняшней, вчерашняя не принимается;
- поле дополнительной информации ограничено 256 символами — длиннее отклоняется целиком;
- если организация не плательщик налога на добавленную стоимость, поле ставки должно отсутствовать, а не быть нулевым.
Каждое такое правило — отдельная итерация «отправили, получили отказ, поняли, поправили». Их нельзя запланировать заранее, но можно заложить на них время — и это честнее, чем назвать срок без них и потом объяснять просрочку.
Как это устроено в результате
| Часть | Что делает |
|---|---|
| Страница админки | журнал документов, защита от дублей, мастер выставления, реестр |
| Локальный агент | слушает порт только на машине владельца, доступ разрешён единственному источнику; три операции: жив ли агент, выгрузить журнал за период, зарегистрировать документ |
| Реестр документов | серверная часть с добавлением без дублей и двумя каналами авторизации |
| Личный кабинет клиента | тот же реестр, отфильтрованный под клиента |
Ответ агента на регистрацию — либо идентификатор документа, либо список конкретных нарушений контроля. Второе важнее первого: без разбора отказов интеграция была бы чёрным ящиком, который иногда работает.
Что из этого следует для оценки сроков
- Пока нет доступа к живому ответу — нет оценки. Мы просим доступ или тестовый контур до того, как называть срок. Если ни того, ни другого нет, называется срок разведки, а не срок работы.
- Интеграция оценивается отдельно от остального проекта. Смешивать её с понятными задачами в одну сумму — способ спрятать риск, который потом всплывёт целиком.
- Разбор отказов — часть работы, а не непредвиденные обстоятельства. Он закладывается в план явной строкой.
- Готовая интеграция описывается построчно. Иначе следующий человек — или следующая модель — «упростит» принудительную версию соединения и потратит день заново.