Перейти к содержанию

Модуль 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 или логи.

Другие модули получают организации через интерфейс:

interface IikoOrganizations {
    fun findActive(connectionId: Long = 1): List<IikoOrganization>
}

Метод возвращает только организации без 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.

Авторизация и запрос организаций

Клиент выполняет следующие запросы:

  1. POST /api/1/access_token с apiLogin.
  2. POST /api/1/organizations с Bearer-токеном.

Токен кешируется отдельно для каждого connection.id в памяти процесса на 55 минут. При ответе 401 кеш принудительно обновляется, после чего исходный запрос повторяется один раз. Параллельное получение токенов внутри одного процесса защищено синхронизацией.

RestClient.Builder берётся из автоконфигурации Spring Boot: модуль app зависит от spring-boot-restclient, поэтому клиент получает стандартные customizers, а транспорт настраивается через spring.http.client.*. Собственный bean builder'а не объявляется — он бы заменил автоконфигурацию и лишил бы настройки транспорта возможности применяться.

Запрос организаций включает:

{
  "organizationIds": null,
  "returnAdditionalInfo": true,
  "includeDisabled": true
}

Сценарий импорта

OrganizationImportService запускает импорт каждый час для каждого активного подключения из БД:

  1. Проверяет через franchise::api, что юридическое лицо и его ФЧ операционно активны.
  2. Пропускает подключение без сетевого вызова, если владелец неоперационен.
  3. Получает полный список организаций из iiko.
  4. Проверяет наличие непустых строковых id и name у каждого элемента.
  5. Открывает транзакцию базы данных.
  6. Создаёт или обновляет каждую организацию.
  7. Снимает inactive_at у вернувшихся организаций.
  8. Устанавливает inactive_at организациям, отсутствующим в новом полном снимке.
  9. Фиксирует транзакцию и пишет статистику в лог без 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:

./gradlew jooqCodegen

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.

Полный запуск:

task test

Текущие ограничения

  • Токены кешируются только в памяти; 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.

Следующие этапы

  1. После ручной проверки зашифровать apiLogin в БД отдельной миграцией и мастер-ключом из Secret.
  2. Добавить наблюдаемость через iiko.sync_run и метрики.
  3. Добавить защиту полного снимка от подозрительного массового исчезновения организаций.
  4. Реализовать устойчивость HTTP-вызовов: таймауты, retry/backoff и rate limiting.
  5. Реализовать импорт терминальных групп.
  6. Реализовать импорт внешних меню, меню и стоп-листов.
  7. Публиковать события модуля после успешных импортов, когда появятся потребители.