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-сущности уходят на кассу
| Сущность | SyncEntityType | corporationId | subdivisionIds |
|---|---|---|---|
| Employee | employee | из самой записи | активные + default |
| Position | position | из самой записи | [] (на корпорацию) |
| PaymentRate | payment_rate | резолвится через employee | [] |
| AttendanceType | attendance_type | из самой записи | [] (на корпорацию) |
| Attendance | attendance | резолвится через 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/* (алиас) | Прописан, папка не существует | Вероятно, будущий модуль или устаревший |