Модуль iiko¶
Назначение и границы¶
Модуль iiko отвечает за управляемые подключения к iiko Transport, синхронизацию внешних
справочников и их локальное зеркало в схеме PostgreSQL iiko. На текущем этапе реализованы
доменная модель подключения и импорт организаций.
Модуль владеет:
- HTTP-взаимодействием с iiko Transport;
- получением и кешированием access token;
- жизненным циклом подключений к аккаунтам iiko;
- таблицами
iiko.connectionиiiko.organization; - расписанием и транзакцией импорта организаций;
- публичными контрактами управления подключениями и чтения активных организаций.
Модуль не управляет каталогом витрины и не должен зависеть от будущего модуля catalog. Связь с
заинтересованными модулями должна появляться через публичный API или события Spring Modulith.
Принадлежность iiko-подключений юридическим лицам и создание ресторанов описаны в минимальной модели франчайзинга. Сценарий создания подключения сотрудником ФЧ и проверки владения описаны в модели административного доступа.
Структура¶
ru.panampizza.monolith.iiko
├── api
│ ├── IikoConnections управление подключениями
│ └── IikoOrganizations read-only контракт организаций
├── IikoProperties настройки интеграции
├── IikoConnectionService жизненный цикл подключения
├── IikoConnectionRepository persistence и credential через jOOQ
├── IikoTransportClient HTTP-клиент и токены
├── OrganizationImportService сценарий и расписание импорта
└── OrganizationRepository запись и чтение через jOOQ
Корневой пакет объявлен модулем Spring Modulith. Пакет iiko.api объявлен named interface api;
остальные классы являются внутренней реализацией.
Публичные контракты¶
IikoConnections предоставляет:
interface IikoConnections {
fun create(legalEntityId: UUID, name: String, apiLogin: String): IikoConnection
fun findByLegalEntity(legalEntityId: UUID): List<IikoConnection>
fun disable(id: Long): IikoConnection
}
Создание разрешено только для существующего операционно активного юридического лица. Владелец
legalEntityId после создания неизменяем. Имя и apiLogin нормализуются через trim, обязательны
и ограничены 200 и 500 символами соответственно. Новое подключение получает статус ACTIVE.
Публичная IikoConnection содержит ID, владельца, имя, статус, временные поля и version, но не
содержит apiLogin. Credential доступен только внутренним transport/import-компонентам и не должен
попадать в API или логи.
Другие модули получают организации через интерфейс:
Метод возвращает только организации без inactive_at, отсортированные по имени. Контракт содержит
нормализованные поля: идентификатор iiko, подключение, название, код, адрес, координаты и время
последней синхронизации. Исходный JSON наружу не предоставляется.
Конфигурация¶
Настройки имеют префикс panam.iiko:
panam:
iiko:
base-url: ${IIKO_BASE_URL:https://api-ru.iiko.services}
organizations-cron: "0 0 * * * *"
| Параметр | Назначение | Значение по умолчанию |
|---|---|---|
base-url |
Адрес iiko Transport | https://api-ru.iiko.services |
organizations-cron |
Расписание импорта организаций | каждый час |
Подключения больше не задаются через конфигурацию приложения. Они создаются динамически и хранятся
в БД. Модель поддерживает несколько подключений одного юридического лица; внешний ID организации
уникален в паре с connection_id.
Авторизация и запрос организаций¶
Клиент выполняет следующие запросы:
POST /api/1/access_tokenсapiLogin.POST /api/1/organizationsс Bearer-токеном.
Токен кешируется отдельно для каждого connection.id в памяти процесса на 55 минут. При ответе
401 кеш принудительно обновляется, после чего исходный запрос повторяется один раз. Параллельное
получение токенов внутри одного процесса защищено синхронизацией.
RestClient.Builder берётся из автоконфигурации Spring Boot: модуль app зависит от
spring-boot-restclient, поэтому клиент получает стандартные customizers, а транспорт
настраивается через spring.http.client.*. Собственный bean builder'а не объявляется — он бы
заменил автоконфигурацию и лишил бы настройки транспорта возможности применяться.
Запрос организаций включает:
Сценарий импорта¶
OrganizationImportService запускает импорт каждый час для каждого активного подключения из БД:
- Проверяет через
franchise::api, что юридическое лицо и его ФЧ операционно активны. - Пропускает подключение без сетевого вызова, если владелец неоперационен.
- Получает полный список организаций из iiko.
- Проверяет наличие непустых строковых
idиnameу каждого элемента. - Открывает транзакцию базы данных.
- Создаёт или обновляет каждую организацию.
- Снимает
inactive_atу вернувшихся организаций. - Устанавливает
inactive_atорганизациям, отсутствующим в новом полном снимке. - Фиксирует транзакцию и пишет статистику в лог без credential.
HTTP-запрос выполняется до открытия транзакции. Ошибка запроса или валидации не изменяет базу. Ошибка записи откатывает весь импорт одного подключения.
Результат импорта содержит счётчики total, created, updated и deactivated. Сейчас результат
используется только для журналирования и не является частью публичного API.
Модель данных¶
iiko.connection¶
| Поле | Описание |
|---|---|
id |
генерируемый стабильный внутренний идентификатор подключения |
legal_entity_id |
неизменяемый владелец, FK на franchise.legal_entity |
name |
имя подключения без секретов |
api_login |
credential iiko; временно хранится открытым текстом |
status |
ACTIVE или DISABLED |
created_at |
время создания |
updated_at |
время последнего обновления настроек |
version |
версия записи |
Отключённое подключение не участвует в импорте. Физическое удаление не поддерживается.
Миграция сохраняет старые технические строки iiko.connection как отключённые legacy-записи без
владельца и credential. Они не видны через IikoConnections и не участвуют в импорте; связанные
ранее импортированные организации не удаляются.
iiko.organization¶
| Поле | Описание |
|---|---|
id |
UUID организации в iiko |
connection_id |
владелец организации, FK на iiko.connection |
name, code |
название и код организации |
address |
нормализованный адрес |
latitude, longitude |
координаты, если переданы iiko |
raw_data |
полный исходный объект организации в jsonb |
synchronized_at |
время снимка, в котором организация присутствовала |
inactive_at |
время исчезновения из полного снимка; null означает активную запись |
Первичный ключ — (connection_id, id). Частичный индекс (connection_id, name) ускоряет чтение
активных организаций.
Адрес читается сначала из корневого поля address, затем из
additionalInfo.restaurantAddress. Координаты поддерживаются как в корне объекта, так и внутри
additionalInfo. Полный raw_data сохраняется для диагностики и добавления новых полей без потери
ранее полученных данных.
jOOQ и миграции¶
Исходная схема создаётся Flyway-миграцией V1__create_iiko_organizations.sql, а управляемые
подключения добавляет V12__create_managed_iiko_connections.sql. Типизированные классы jOOQ
генерируются в build/generated-src/jooq/main:
compileKotlin зависит от jooqCodegen, поэтому отдельный ручной запуск обычно не требуется.
Generated-код не хранится в Git. Генератор читает актуальный snapshot
db/jooq/iiko.sql, содержащий только таблицы схемы iiko. Snapshot поддерживается вместе с Flyway
миграциями: он отделяет codegen от PostgreSQL-конструкций других модулей, которые DDLDatabase не
умеет разбирать. Runtime использует только Flyway, snapshot не исполняется приложением.
Тесты¶
IikoTransportClientTestsпроверяет получение и повторное использование токена, а также однократное обновление после401.OrganizationRepositoryTestsиспользует PostgreSQL Testcontainers и проверяет создание, обновление без дубликата и мягкую деактивацию.IikoConnectionTestsпроверяет владельца, валидацию, сортировку, операционную активность юридического лица, отключение, внутреннее хранение credential и отсутствиеapiLoginв публичной модели.FranchiseOfficeIikoConnectionHttpTestsв Gradle-модулеwebпроверяет ownership и HTTP-сценарии создания, списка и отключения без раскрытия credential.ModularityTestsпроверяет границы Spring Modulith.
Полный запуск:
Текущие ограничения¶
- Токены кешируются только в памяти; Redis и распределённая блокировка ещё не реализованы.
- Нет retry/backoff для
429,5xxи сетевых таймаутов. Таймауты заданы черезspring.http.client.*вweb/src/main/resources/application.yaml:connect-timeout: 5s,read-timeout: 60s— минута учитывает, что ответ/api/1/organizationsможет весить десятки мегабайт и формируется на стороне iiko долго. - Нет записи запуска в
iiko.sync_runи метрик импорта. - Нет предохранителя от неожиданно пустого или резко уменьшившегося списка организаций.
apiLoginвременно хранится в БД открытым текстом для ручного тестирования. До реализации шифрования нельзя использовать production credential или предоставлять доступ к таблицеiiko.connectionпосторонним ролям.- Нет административного API для ручного запуска импорта.
- Терминалы, внешние меню, меню и стоп-листы ещё не реализованы.
Эти ограничения необходимо учитывать перед подключением production-аккаунта iiko.
Следующие этапы¶
- После ручной проверки зашифровать
apiLoginв БД отдельной миграцией и мастер-ключом из Secret. - Добавить наблюдаемость через
iiko.sync_runи метрики. - Добавить защиту полного снимка от подозрительного массового исчезновения организаций.
- Реализовать устойчивость HTTP-вызовов: таймауты, retry/backoff и rate limiting.
- Реализовать импорт терминальных групп.
- Реализовать импорт внешних меню, меню и стоп-листов.
- Публиковать события модуля после успешных импортов, когда появятся потребители.