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

Модуль franchise

Статус

Создан Spring Modulith-модуль и публичная граница franchise::api. Реализованы агрегаты Franchisee и LegalEntity.

Назначение

Модуль franchise отвечает за договорную структуру франчайзинговой сети: партнёров ГК и юридические лица, через которые они работают.

Модуль владеет:

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

Модуль не отвечает за:

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

Границы

franchise
├── Franchisee       агрегат партнёра ГК
└── LegalEntity      юридическое лицо франчайзи

franchise не зависит от других бизнес-модулей. Другие модули смогут получать необходимые данные только через named interface franchise::api или реагировать на опубликованные события.

Прямое чтение таблиц модуля из iiko, restaurant, admin и других модулей запрещено.

Структура пакетов

ru.panampizza.monolith.franchise
├── FranchiseeEntity
├── FranchiseeRepository
├── FranchiseeService
├── LegalEntityEntity
├── LegalEntityRepository
├── LegalEntityService
└── api
    ├── Franchisees
    └── LegalEntities

Корневой пакет объявлен через @ApplicationModule. Пакет api объявлен через @NamedInterface("api"). Всё, что в дальнейшем появится вне пакета api, является внутренней реализацией модуля.

Franchisee

Франчайзи — партнёр ГК, внутри которого в дальнейшем создаются юридические лица.

Модель:

id: UUID
name: String(1..200)
status: ACTIVE | DISABLED
createdAt: Instant
updatedAt: Instant
version: Long

Правила:

  • новый франчайзи создаётся активным;
  • имя обрезается по краям и не может быть пустым или длиннее 200 символов;
  • имя не уникально и не используется как идентификатор;
  • физическое удаление не поддерживается;
  • отключение обратимо;
  • имя можно изменить;
  • оптимистическая версия защищает конкурентное редактирование;
  • при фактической смене статуса публикуется FranchiseeStatusChanged;
  • повторная установка текущего статуса является идемпотентной и не публикует событие.

Таблица franchise.franchisee создаётся Flyway-миграцией V3__create_franchisees.sql. Обычный CRUD реализован через Spring Data JPA.

JPA-сущность использует явный field access: persistent-поля объявлены как var, поскольку JPA изменяет их при materialization, но setter'ы имеют видимость protected для Hibernate-прокси. Прикладной код меняет состояние только через методы rename и changeStatus. Неизменяемая публичная модель отделена от JPA-сущности.

Публичный API

Named interface franchise::api предоставляет контракт Franchisees:

interface Franchisees {
    fun create(name: String): Franchisee
    fun findById(id: UUID): Franchisee?
    fun findAll(): List<Franchisee>
    fun rename(id: UUID, name: String): Franchisee
    fun disable(id: UUID): Franchisee
    fun activate(id: UUID): Franchisee
}

Наружу возвращается неизменяемая модель Franchisee; внутренняя JPA-сущность не пересекает границу модуля. Для обязательных операций отсутствующий ID приводит к FranchiseeNotFoundException.

События

FranchiseeStatusChanged входит в публичный контракт franchise::api:

data class FranchiseeStatusChanged(
    val franchiseeId: UUID,
    val previousStatus: FranchiseeStatus,
    val currentStatus: FranchiseeStatus,
    val occurredAt: Instant,
)

Событие публикуется в транзакции изменения статуса. После появления потребителей Spring Modulith будет регистрировать предназначенные им публикации в Event Publication Registry.

Отключение ФЧ не должно перезаписывать собственные статусы его ресторанов. Будущий модуль restaurant установит операционную блокировку FRANCHISEE_DISABLED; после активации ФЧ блокировка снимается, а прежний статус ресторана сохраняется. Ресторан со статусом CLOSED не восстанавливается.

Будущий модуль iiko должен пропускать плановый импорт подключений, если их юридическое лицо или ФЧ операционно неактивны. Проверка выполняется через franchise::api, без чтения таблиц модуля.

LegalEntity

Юридическое лицо принадлежит одному ФЧ; принадлежность после создания неизменяема. Один ФЧ может иметь несколько юридических лиц.

id: UUID
franchiseeId: UUID
name: String(1..300)
inn: String(10 или 12 цифр)
kpp: String?(9 цифр)
legalAddress: String(1..500)
status: ACTIVE | DISABLED
createdAt: Instant
updatedAt: Instant
version: Long

Пара (inn, kpp) уникальна во всей системе, включая случай отсутствующего КПП. Модель поддерживает организации с десятизначным ИНН и ИП с двенадцатизначным ИНН без отдельного discriminator. Физическое удаление не поддерживается; отключение обратимо.

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

legalEntity.status == ACTIVE && franchisee.status == ACTIVE

Контракт LegalEntities предоставляет создание, чтение, обновление реквизитов, изменение статуса, а также одиночную и пакетную проверку эффективной активности. Пакетный метод findOperationalIds предназначен для планировщика iiko и не создаёт N+1 запросов.

При фактической смене собственного статуса публикуется LegalEntityStatusChanged. Повторная установка текущего статуса не публикует событие.

Таблица franchise.legal_entity создаётся миграцией V4__create_legal_entities.sql. FK обеспечивает существование ФЧ, а check constraints дублируют критичную валидацию ИНН, КПП, статуса и обязательных строк на уровне PostgreSQL.

Зависимости

На текущем этапе разрешённых зависимостей на другие модули нет:

franchise ──X──▶ iiko
franchise ──X──▶ restaurant
franchise ──X──▶ catalog

Целевые потребители публичного API:

restaurant ──▶ franchise::api
admin ───────▶ franchise::api
franchiseoffice ──▶ franchise::api

franchiseoffice использует findByFranchisee для списка юридических лиц текущего сотрудника. Scope определяется его FranchisePrincipal; модуль franchise не зависит от HTTP и IAM.

Проверка модульности

Общий ModularityTests вызывает ApplicationModules.verify() и проверяет в том числе модуль franchise: отсутствие циклов, обращений к внутренним пакетам и неразрешённых зависимостей.

FranchiseeTests использует PostgreSQL Testcontainers и проверяет создание, валидацию имени, переименование, сортировку, отключение, повторную активацию, обработку отсутствующего ID и идемпотентную публикацию события изменения статуса.

LegalEntityTests проверяет принадлежность ФЧ, реквизиты, уникальность, обновление, событие статуса и эффективную активность с учётом состояния ФЧ.

HTTP ownership списка юридических лиц проверяется в модуле franchiseoffice: данные другого ФЧ не попадают в ответ, а клиент не передаёт franchiseeId.

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

iiko.connection уже привязан к LegalEntity, а модуль iiko проверяет существование и операционную активность владельца через franchise::api. Следующий отдельно принимаемый этап — HTTP ownership-сценарий создания подключения в franchiseoffice.

Полная согласованная модель описана в архитектуре франчайзинга.

Права сотрудников ГК и ФЧ на операции модуля описаны в модели административного доступа.