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

Минимальная модель франчайзинга

Статус документа

Документ фиксирует согласованную целевую модель. В модуле franchise реализованы Franchisee и LegalEntity. Для модуля restaurant создан каркас и публичная граница API; его доменная модель ещё не реализована. Реализация выполняется поэтапно; после каждого этапа результат принимается отдельно.

Основная модель

Franchisee
└── LegalEntity
    └── IikoConnection
        └── IikoOrganization
            └── Restaurant

Связи:

Franchisee       1 ── N LegalEntity
LegalEntity      1 ── N IikoConnection
IikoConnection   1 ── N IikoOrganization
IikoOrganization 1 ── 0..1 Restaurant

Термины:

  • ГК — головная компания, управляющая франчайзинговой сетью.
  • Франчайзи — партнёр ГК. Он может работать через одно или несколько юридических лиц.
  • Юридическое лицо — сторона договора с iiko и будущий продавец при оформлении заказа.
  • iiko-подключение — техническое подключение к аккаунту iiko, принадлежащее конкретному юридическому лицу.
  • Организация iiko — импортированный внешний объект внутри iiko-подключения.
  • Ресторан — самостоятельная сущность платформы, доступная витрине после активации.

Роли сотрудников, Админка ГК и Личный кабинет франчайзи описаны в модели административного доступа. Единая для всей сети учётная запись покупателя и изоляция покупательских данных между ФЧ описаны в модели доступа покупателей.

Границы модулей

Модель разделяется на три модуля.

franchise

Модуль отвечает на вопрос: кто является партнёром ГК и через какие юридические лица он работает?

Владеет:

  • франчайзи;
  • юридическими лицами;
  • их статусами и реквизитами;
  • в будущем — договорами, сотрудниками франчайзи и политиками доступа.

Не знает об организациях iiko, импорте меню и жизненном цикле ресторанов.

iiko

Модуль отвечает за подключение к iiko Transport и зеркало внешних данных.

Владеет:

  • iiko-подключениями;
  • учётными данными подключений (apiLogin, хранится зашифрованным, см. «Секреты юридического лица»);
  • токенами и HTTP-взаимодействием;
  • импортированными организациями;
  • в будущем — терминалами, меню и стоп-листами.

Каждое подключение содержит legal_entity_id, указывающий владельца. Модуль iiko не читает таблицы модуля franchise напрямую.

restaurant

Модуль отвечает за представление ресторана на платформе и его жизненный цикл.

Владеет:

  • рестораном;
  • публичным названием и slug;
  • адресом и часовым поясом;
  • статусом публикации;
  • неизменяемой привязкой к организации iiko;
  • операциями активации, временного отключения и окончательного закрытия.

Зависимости модулей

storefront ──▶ restaurant
storefront ──▶ catalog
storefront ──▶ customer::api
restaurant ──▶ franchise::api
restaurant ──▶ iiko::api
admin ──▶ franchise::api, restaurant::api, iiko::api

Правила:

  • franchise не зависит от restaurant и iiko;
  • iiko не зависит от restaurant;
  • модули не читают таблицы друг друга;
  • проверки и команды выполняются через публичные интерфейсы;
  • обратная связь между iiko и restaurant выполняется событиями.

Изменение статуса ФЧ публикуется как FranchiseeStatusChanged. Это событие является сигналом для зависящих модулей немедленно пересчитать операционную доступность принадлежащих ему данных.

Административный экран может показывать единое дерево франчайзи → юридическое лицо → подключение → организация → ресторан, хотя данные принадлежат разным модулям. Представление интерфейса не определяет границы модулей.

Минимальные сущности

Franchisee

id
name
active
created_at
updated_at

LegalEntity

id
franchisee_id
name
inn
kpp
legal_address
active
created_at
updated_at

Одно юридическое лицо принадлежит ровно одному франчайзи. Один франчайзи может иметь несколько юридических лиц. Модель также покрывает частный случай «один франчайзи — одно юридическое лицо».

IikoConnection

id
legal_entity_id
name
api_login
status: ACTIVE | DISABLED
created_at
updated_at
version

Подключение принадлежит юридическому лицу, поскольку договор с iiko заключается от конкретного юридического лица. На этапе ручного тестирования apiLogin временно хранится в базе открытым текстом, но не публикуется через API и не пишется в логи. До production отдельным обязательным этапом добавляется прикладное шифрование с мастер-ключом из Kubernetes/Docker Compose Secret. Общие требования ко всем секретам юрлица — в разделе «Секреты юридического лица».

IikoOrganization

id
connection_id
name
code
address
latitude
longitude
synchronized_at
inactive_at

Внешний идентификатор организации уникален в паре с connection_id.

Restaurant

id
iiko_connection_id
iiko_organization_id
name
slug
address
timezone
status
closed_at
close_reason
created_at
updated_at

Пара (iiko_connection_id, iiko_organization_id) уникальна. Юридическое лицо и франчайзи ресторана однозначно определяются по цепочке:

Restaurant
→ IikoOrganization
→ IikoConnection
→ LegalEntity
→ Franchisee

legal_entity_id и franchisee_id не дублируются в ресторане, чтобы не создавать противоречивые источники истины.

Создание подключения и импорт организаций

Согласованный сценарий:

  1. Сотрудник ГК создаёт франчайзи.
  2. Внутри франчайзи создаёт юридическое лицо.
  3. Для юридического лица создаёт одно или несколько iiko-подключений.
  4. Модуль iiko импортирует организации каждого подключения.
  5. Для новой организации публикуется событие.
  6. Модуль restaurant создаёт ресторан в статусе DRAFT, если привязки ещё нет.
  7. Сотрудник ГК проверяет данные, заполняет обязательные настройки и активирует ресторан.
Franchisee
    ↓
LegalEntity
    ↓
IikoConnection
    ↓ import
IikoOrganization
    ↓ event
Restaurant(DRAFT)
    ↓ approval by ГК
Restaurant(ACTIVE)

Модуль iiko публикует событие с connectionId и organizationId, но ничего не знает о созданном ресторане. Обработчик события находится в модуле restaurant.

Жизненный цикл ресторана

Минимальные статусы:

Статус Значение
DRAFT создан после импорта, ещё не доступен витрине
ACTIVE работает и доступен витрине
DISABLED временно отключён и может быть восстановлен
CLOSED закрыт окончательно, терминальный статус

Переходы:

DRAFT ──▶ ACTIVE ⇄ DISABLED
  │          │          │
  └──────────┴──────────┴──▶ CLOSED

Ресторан в статусе CLOSED нельзя активировать повторно.

Исчезновение организации из ответа iiko не закрывает ресторан автоматически. Это может быть ошибка или временная недоступность внешней системы. В такой ситуации ресторан становится технически недоступным, но окончательное закрытие выполняется отдельной командой сотрудника ГК.

Операционная блокировка владельцем

Собственный статус ресторана не перезаписывается при отключении ФЧ. Эффективная доступность вычисляется из собственного статуса и операционных блокировок:

restaurant.status = ACTIVE
operational blocks = [FRANCHISEE_DISABLED]
effective availability = UNAVAILABLE

Модуль restaurant в будущем обрабатывает FranchiseeStatusChanged и устанавливает либо снимает FRANCHISEE_DISABLED. Поэтому после повторной активации ФЧ восстанавливаются только рестораны, которые были активны сами по себе; DRAFT, вручную DISABLED и CLOSED не меняют свой статус.

Плановый импорт iiko выполняется только если одновременно активны подключение, его юридическое лицо и ФЧ. Неактивный владелец приводит к пропуску запуска, а не к ошибке импорта. Конкретный контракт пакетной проверки операционной активности будет согласован на этапе LegalEntity.

Неизменяемость привязки

После создания ресторана его привязка неизменяема:

iiko_connection_id
iiko_organization_id

Запрещены:

  • перенос ресторана на другое iiko-подключение;
  • замена организации iiko;
  • перенос ресторана к другому юридическому лицу;
  • восстановление окончательно закрытого ресторана.

Юридическое лицо ресторана определяется подключением iiko. Изменение подключения фактически означало бы смену юридического лица и создание другого операционного объекта.

Смена юридического лица

При смене юридического лица ресторан не переносится:

  1. Старый ресторан закрывается и получает статус CLOSED.
  2. Для нового юридического лица создаётся или выбирается другое iiko-подключение.
  3. Импортируется другая организация iiko.
  4. Создаётся новый ресторан с новым restaurant.id и отдельным жизненным циклом.

Старый и новый рестораны никак не связываются в модели. Поля replaces_restaurant_id или другие ссылки преемственности не создаются. С юридической и операционной точки зрения это разные объекты.

Старый ресторан остаётся в системе ради собственной истории заказов, платежей, чеков и отчётности.

Адрес

Старый и новый рестораны могут иметь одинаковый адрес, но адрес является только атрибутом:

oldRestaurant.address == newRestaurant.address

Совпадение адресов не создаёт связь и не означает преемственность. Адрес нельзя использовать как идентификатор, поскольку он может быть исправлен, нормализован или совпадать у нескольких объектов.

Секреты юридического лица

Секретами юрлица являются apiLogin iiko, shopId и секретный ключ ЮKassa, доступ к данным оператора фискальных данных. Требования ко всем:

  • хранятся в БД, зашифрованные прикладным шифрованием с мастер-ключом из Kubernetes/Docker Compose Secret;
  • после сохранения не отображаются, не возвращаются через API и не попадают в логи и аудит;
  • доступны только внутренним компонентам интеграции: transport и импорту iiko, созданию платежа, получению чеков;
  • вводятся только сотрудником ФЧ в Личном кабинете; сотрудник ГК секреты ФЧ не видит;
  • в переменных окружения остаются только мастер-ключ и учётные данные глобальных сервисов ГК.

До реализации шифрования боевые учётные данные не используются и доступ к таблицам с секретами никому не выдаётся.

Денежный контур

Платформа не участвует в расчётах (см. Франшиза). Для реализации это означает:

  • продавец определяется по ресторану корзины до создания платежа; платёж создаётся по реквизитам ЮKassa юрлица этого ресторана, после чего ресторан заказа не меняется;
  • платформа хранит платёж и его статусы, но не деньги; возврат инициируется в Личном кабинете ФЧ, исполняется в ЮKassa ФЧ, а платформа хранит основание, сумму и статус;
  • чек платформа не пробивает. Для показа покупателю чек ищется в данных ОФД касс ресторана заказа по сумме, признаку оплаты, времени в окне вокруг заказа и полному составу, включая модификаторы. Привязывается только единственный полностью совпавший чек; при нескольких одинаковых не привязывается ничего. Если iiko отдаёт фискальные реквизиты закрытого заказа, сопоставление по составу и доступ к ОФД станут не нужны;
  • реестры онлайн-канала строятся по данным платформы; выручку из iiko платформа не выгружает и не хранит.

Поэтапная реализация

Каждый этап реализуется и принимается отдельно. Переход к следующему этапу выполняется только после явного подтверждения результата предыдущего.

  1. Выполнено: создать каркас Spring Modulith-модуля franchise и его публичный API без бизнес-сущностей.
  2. Выполнено: реализовать Franchisee: миграцию, доменную модель, операции и тесты.
  3. Выполнено: реализовать LegalEntity: принадлежность франчайзи, операции, проверки и тесты.
  4. Выполнено: создать каркас Spring Modulith-модуля restaurant и его публичный API.
  5. Выполнено: добавить владельца legal_entity_id для iiko.connection, доменный сценарий создания и проверку юридического лица через franchise::api.
  6. Выполнено: добавить HTTP ownership-сценарии создания, списка и отключения подключений в franchiseoffice без раскрытия apiLogin.
  7. После ручного тестирования зашифровать apiLogin с мастер-ключом из Secret.
  8. Реализовать Restaurant, его статусы, неизменяемую привязку и закрытие.
  9. Добавить событие появления организации iiko и автоматическое создание ресторана DRAFT.
  10. Добавить публичное read-only API активных ресторанов для витрины.

На каждом этапе обновляются соответствующая документация модуля и этот архитектурный документ.