Перейти к содержимому

11. Карта связей между модулями

Полная карта зависимостей

┌────────────────────────────────────────────────────────────────────────────┐
│ │
│ ОРГАНИЗАЦИЯ │
│ (корпорация, юрлица, │
│ подразделения) │
│ │
│ Даёт: таймзону, даты блокировки, структуру │
│ │
│ ┌──────────┼──────────┼──────────┼──────────┐ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ МЕНЮ │ │ СКЛАД │ │ БУХГАЛ.│ │ КАДРЫ │ │ POS │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │ │ │
│ │ │ │ │ │ │
│ номенкл. │ ────────┤ │ событие │ │ │
│ цены │ │ платежи │ увольн. │ чеки │ │
│ техкарты │ │ ────────┤ ────────┤ смены │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ЗАКУПКИ │ │ОТЧЁТН. │ │ АВТО- │ │ OUTBOX │ │ KASSA- │ │
│ │(методы │ │(выручка│ │РИЗАЦИЯ │ │(Kafka) │ │ SYNC │ │
│ │оплаты) │ │ отчёт) │ │(сессии)│ │ │ │ │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────┘

Связи: кто от кого зависит

Организация → все модули

Организация — фундамент. Все модули читают из неё через ACL (Anti-Corruption Layer — изолированный интерфейс, чтобы модули не зависели друг от друга напрямую).

┌──────────────┐
│ ОРГАНИЗАЦИЯ │
│ │
│ Что даёт: │
│ • timezone ├──────► Бухгалтерия (для расчёта дат блокировки)
│ • блокировка├──────► Склад (для проверки дат документов)
│ • юрлица ├──────► Кадры (для расчёта дат увольнения)
│ • контраг. ├──────► Меню (для проверки перед удалением)
│ • подраздел.├──────► POS (для привязки терминалов)
│ ├──────► Отчётность (для группировки)
│ ├──────► Закупки (для привязки платежей)
└──────────────┘

Меню → Склад, POS, Отчётность

┌──────────────┐
│ УПРАВЛЕНИЕ │
│ МЕНЮ │
│ │
│ Что даёт: │
│ • номенклат.├──────► Склад (позиции накладных, остатки)
│ • цены ├──────► POS (позиции чеков, цены продажи)
│ • техкарты ├──────► Склад (себестоимость для списания)
│ • модификат.├──────► POS (добавки к блюдам)
│ • группы ├──────► POS (структура меню)
└──────────────┘

Склад ↔ Бухгалтерия

┌──────────────┐ ┌──────────────┐
│ СКЛАД │ │ БУХГАЛТЕРИЯ │
│ │ накладные │ │
│ Накладные ├──────────────────────►│ Платежи │
│ │ (основание для │ (оплата за │
│ │ платежей) │ накладную) │
│ │ │ │
│ Инвентариз. ├──────────────────────►│ Проводки │
│ Списания │ (излишки/недостачи │ (дебет/ │
│ Акты реализ.│ → проводки) │ кредит) │
└──────────────┘ └──────────────┘

Кадры → Авторизация (через события)

┌──────────────┐ СОБЫТИЕ: ┌──────────────┐
│ КАДРЫ │ «Сотрудник уволен» │ АВТОРИЗАЦИЯ │
│ │───────────────────────────►│ │
│ Увольнение │ СОБЫТИЕ: │ Удаление │
│ Смена пароля│ «Пароль изменён» │ всех сессий │
│ │───────────────────────────►│ пользователя│
└──────────────┘ └──────────────┘

Закупки → POS, Склад

┌──────────────┐
│ ЗАКУПКИ │
│ │
│ Методы ├──────► POS (способы оплаты в чеках)
│ оплаты ├──────► Склад (способы оплаты в актах реализации)
└──────────────┘

Меню ← Бухгалтерия (проверка перед удалением)

┌──────────────┐ «Сколько номенклатур ┌──────────────┐
│ БУХГАЛТЕРИЯ │ используют эту │ УПРАВЛЕНИЕ │
│ │ категорию учёта?» │ МЕНЮ │
│ Категории │───────────────────────────►│ │
│ учёта │ │ Номенклатура│
│ │ «5 штук → нельзя │ │
│ │◄── удалять категорию» │ │
└──────────────┘ └──────────────┘

Двусторонняя синхронизация Офис ↔ Касса

ОФИС → КАССА (справочные данные)
┌──────────────────────────────────────────────────────────┐
│ │
│ Номенклатура ───► Outbox ───► Kafka ───► Касса ───► POS│
│ Группы ───► │
│ Цены ───► Задержка: ~10 секунд │
│ Техкарты ───► Гарантия: транзакционный outbox │
│ Модификаторы ───► │
│ Сотрудники ───► │
│ Подразделения───► │
│ Терминалы ───► │
│ Методы оплаты───► │
│ │
└──────────────────────────────────────────────────────────┘
КАССА → ОФИС (операционные данные)
┌──────────────────────────────────────────────────────────┐
│ │
│ POS ───► Касса ───► Kafka ───► Офис (kassa-sync модуль)│
│ │
│ Кассовые смены (открытие, закрытие) ───► pos_payments │
│ Чеки (продажи, возвраты) ───► pos_receipts │
│ Платежи по чекам ───► pos_payments │
│ │
│ Гарантия: UPSERT по entityId (идемпотентность) │
│ │
└──────────────────────────────────────────────────────────┘

Два типа событий в Kafka

Система использует два разных механизма отправки событий — с разными гарантиями.

1. Outbox-события (гарантированная доставка)

Для данных, которые ДОЛЖНЫ попасть на кассу.
Сервис ──► outbox_events (в транзакции) ──► Scheduler (каждые 10 сек) ──► Kafka
Гарантии:
├── Событие НЕ потеряется (записано в БД в транзакции)
├── Повторные попытки при ошибке (до 5 раз)
├── Застрявшие события сбрасываются через 10 минут
└── Статусы: PENDING → PUBLISHING → PUBLISHED / FAILED
Используют: номенклатура, группы, цены, техкарты, модификаторы,
сотрудники, подразделения, терминалы, методы оплаты,
должности, ставки оплаты, типы явок, явки

2. Прямые Kafka-события (best-effort)

Для данных, потеря которых НЕ критична.
Сервис ──► kafkaProducer.publish() ──► Kafka (напрямую)
Гарантии:
├── Если Kafka доступна — доставлено
├── Если Kafka недоступна — событие ПОТЕРЯНО (только лог ошибки)
└── Нет повторных попыток
Используют: проводки, настройки корпорации, юрлица, контрагенты,
инвентаризации, накладные, акты списания/реализации

Почему два механизма?

Outbox — для данных, без которых касса НЕ МОЖЕТ РАБОТАТЬ:
Нет номенклатуры → нечего пробивать
Нет цен → не знаем сколько стоит
Нет сотрудников → не пустит на терминал
Direct Kafka — для данных, которые ПОЛЕЗНЫ, но не критичны:
Нет проводки → бухгалтерия не сломается (она в офисе)
Нет настроек корпорации → касса работает со старыми

Кадровый контур: payload и контракт с кассой

Гибрид Employee ↔ LegalEntity

Сотрудник может работать на нескольких должностях от разных юрлиц (совместительство). На кассу из офиса отправляется гибридный payload: один primary-ЮЛ на сотрудника + ЮЛ на каждой позиции.

Employee {
id, systemName, personnelNumber, firstName/lastName/middleName,
gender, birthDate, contractDate, dismissalDate, isDeleted,
corporationId,
primaryLegalEntityId, ← primary-ЮЛ для отчётности (== legalEntityId primary-позиции)
defaultSubdivisionId,
defaultPosTerminalId,
primaryPositionId,
positions: [{
id, positionId, name,
legalEntityId, ← ЮЛ конкретной позиции (для чеков/ФН)
isPrimary, ← одна активная primary-позиция на сотрудника
workScheduleType, ← HOURLY / SCHEDULED / SALARY
canUseProxyCard, ← разрешена ли работа по чужой карте
allowedOvertimeMinutes, ← порог переработки до OVERDUE
noConfirmationForOffScheduleWork,
hourlyRate, salary, ← дефолтные ставки должности
assignedAt, unassignedAt
}],
subdivisions: [{
id, subdivisionId, name,
legalEntityId, ← ЮЛ подразделения
assignedAt, unassignedAt
}],
version
}

Не передаётся: email, mobilePhone — на кассу не нужны (152-ФЗ: минимизация PII). Чувствительные поля (pinHash) идут в отдельный блок sensitiveFields outbox-записи.

Единый UPDATED-контракт

Все CUD-операции по 5 сущностям staffing (Employee, Position, PaymentRate, AttendanceType, Attendance) публикуют в outbox единое действие SYNC_ACTION = UPDATED. Различение «создан / обновлён / удалён / восстановлен» делается на кассе по payload.isDeleted:

isDeleted=false → создать или обновить запись
isDeleted=true → soft-удалить локальную запись

DELETED / RESTORED для этих сущностей не используются — это убирает четырёхбуквенный enum действий и упрощает обработку на кассе (один handler на тип сущности).

Hard-delete для PaymentRate. Ставки удаляются физически (нет soft-delete-флага в БД), но касса должна синхронизировать удаление — поэтому при delete офис всё равно публикует payload удалённой ставки с isDeleted: true.

Какие staffing-сущности уходят на кассу

СущностьSyncEntityTypecorporationIdsubdivisionIds
Employeeemployeeиз самой записиактивные + default
Positionpositionиз самой записи[] (на корпорацию)
PaymentRatepayment_rateрезолвится через employee[]
AttendanceTypeattendance_typeиз самой записи[] (на корпорацию)
Attendanceattendanceрезолвится через employee[subdivisionId]

subdivisionIds = [] означает «событие на всю корпорацию» — каждая касса корпорации применит изменение.


Правила удаления (кого нельзя удалить)

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

┌────────────────────────┬─────────────────────────────────────────────┐
│ Сущность │ Нельзя удалить, если... │
├────────────────────────┼─────────────────────────────────────────────┤
│ Категория учёта │ Есть номенклатуры с этой категорией │
│ Место приготовления │ Есть номенклатуры с этим местом │
│ Должность │ Есть активные сотрудники на должности │
│ Метод оплаты │ Есть привязанные платежи │
│ Контрагент │ Уже удалён (мягкое удаление) │
│ Сотрудник │ Уже удалён → при «удалении» инвалидирует │
│ │ сессии (событие EMPLOYEE_DISMISSED) │
│ Тип внесений/изъятий │ Уже удалён (мягкое удаление) │
└────────────────────────┴─────────────────────────────────────────────┘

Фоновые процессы (планировщики)

Система запускает три автоматических процесса по расписанию:

┌───────────────────────────────────────────────────────────────────────────┐
│ ПЛАНИРОВЩИКИ (APP_ROLE = scheduler) │
├───────────────────────┬───────────────┬───────────────────────────────────┤
│ Процесс │ Расписание │ Что делает │
├───────────────────────┼───────────────┼───────────────────────────────────┤
│ Outbox Publisher │ Каждые 10 сек │ Забирает PENDING события из │
│ │ │ outbox_events, шлёт в Kafka. │
│ │ │ Batch: 100 штук за раз. │
│ │ Каждую минуту │ Сбрасывает застрявшие │
│ │ │ PUBLISHING > 10 мин → PENDING │
├───────────────────────┼───────────────┼───────────────────────────────────┤
│ Просроченные явки │ Каждый час │ Ищет явки OPEN > 24 часов. │
│ │ │ Закрывает: статус → OVERDUE, │
│ │ │ рассчитывает заработок, │
│ │ │ пишет аудит-лог. │
├───────────────────────┼───────────────┼───────────────────────────────────┤
│ Очистка сессий │ Ежедневно │ Удаляет просроченные JWT-сессии │
│ │ в 03:00 │ (>30 дней без активности) │
└───────────────────────┴───────────────┴───────────────────────────────────┘

Автонумерация документов

Все документы получают уникальный порядковый номер автоматически.

Типы нумерации:
├── Накладная приходная: INV-IN-2026-001, 002, 003...
├── Накладная возвратная: INV-OUT-2026-001...
├── Накладная перемещения: INV-TR-2026-001...
├── Акт списания: WO-2026-001...
├── Акт реализации: SA-2026-001...
├── Инвентаризация: INV-2026-001...
├── Акт услуг: SVC-2026-001...
└── Приказ об изменении цен: PCO-2026-001...
Защита от дублей:
Номер генерируется в транзакции с блокировкой (SELECT FOR UPDATE).
Два одновременных документа не получат один номер.

Хранение изображений

Изображения хранятся во внешнем хранилище (S3-совместимое).

Что хранится:
├── Фото номенклатуры (блюда для меню)
└── Логотип контрагента (поставщика)
Операции:
├── Загрузка (upload) — при создании/обновлении
├── Удаление (delete) — при удалении сущности
└── Best-effort: если удаление из S3 не удалось — логируется, не падает

Заложено, но не реализовано

Три механизма подключены, но пока не используются:

МеханизмСтатусДля чего зарезервирован
CQRS (CqrsModule)Подключён, нет обработчиковСложные многошаговые операции (саги)
BullMQ (очереди)Подключён, нет процессоровФоновые задачи: экспорт, тяжёлые расчёты
#product/* (алиас)Прописан, папка не существуетВероятно, будущий модуль или устаревший