# ADR-002: Домен расчёта зарплаты и выравнивание с контрактом платформы - Status: Accepted for REQ-001 - Date: 2026-09-23 - Контекст требований: [REQ-001](../REQ-001-PAYROLL.md) ## Context Владелец подтвердил требования к расчёту зарплаты (управленческий контур), мониторингу смен и мобильному кабинету сотрудника. Модуль должен остаться блоком Krona Business Platform. Проверка контрактов платформы (`ochenstarik/business-platform`, ветка `main`) показала: - HR, Employee и payroll в платформе не реализованы; `docs/82-TIME-ATTENDANCE.md` и `docs/83-WORK-TIME-ENGINE.md` — только спецификации со статусом `Pilot 1: Deferred`; - `docs/06-SOURCE-OF-TRUTH.md` объявляет владельцем справочника `Employee` ядро, но в коде ядра такого справочника нет; - сущность «торговая точка» (`Branch` / `Location`) объявлена в `08-MULTITENANCY.md` и `05-CONTEXT-MAP.md`, но в коде отсутствует; в Pilot 1 организация ровно одна на tenant; - рантайм модулей неполон: права из манифеста нельзя выдать роли, доставка событий модулю не работает (`PLAN-RAZRABOTKI.md`, пункты С-6 и С-4); - исторические черновики расходились с нормативным контрактом платформы по формату прав и конверту события; интеграция следует актуальному `109-PLATFORM-BLOCK-CONTRACT.md`. ## Decision ### 1. Домен Модуль универсальный: он не знает ни про повара, ни про кафе. Расчёт строится на сущностях `OrganizationUnit` (подразделение), `Employee`, `PayRule` (правило оплаты), `WorkSession` (смена), `PayrollRun` (ведомость) со строками `PayrollLine`, плюс входные данные: показатели дня (`staff_daily_metrics`) и позиции (`staff_work_items`) — состав того и другого определяет клиент. Деньги — `BIGINT` в копейках плюс `currency_code`. Ни правило, ни его параметры не редактируются на месте: изменение создаёт новую версию с датами действия, поэтому прошлые ведомости воспроизводимы. ### 2. Правило оплаты — дерево блоков, а не текст формулы Правило хранится структурой (JSONB) и собирается визуальным конструктором: клиенты приходят из Excel и должны переносить свои формулы сами, без программиста и без изучения синтаксиса. Из этого следует: - **никакого `eval` и никакого разбора текста** — дерево заведомо безопасно и валидируется перед сохранением; - то же дерево пересказывается словами (`describeRule`) и служит расшифровкой суммы для сотрудника; - набор блоков подобран по практике отраслей: процент от показателя, ставка за час и за единицу, фонд с делением по времени присутствия, агрегаты по позициям с фильтрами, ступенчатая и прогрессивная шкалы, условные премии, гарантия и потолок, округление; - отраслевые пресеты (общепит, розница, доставка, услуги, почасовая) — это заранее собранные деревья, а не ветки в коде. Текстовый редактор формул может появиться позже как второй способ ввода поверх той же структуры. ### 3. Расчёт — чистая функция Движок не обращается к базе: на вход подаётся снимок дня (показатели, позиции, участники с их правилами, часами и параметрами), на выходе — строки с полным следом расчёта (фонд, делитель, доля, параметры, входные показатели, текст правила). След сохраняется в строке ведомости: любую сумму нужно уметь объяснить спустя год, а расчёт обязан быть детерминированным и покрытым тестами на реальных днях. ### 4. Начисления и выплаты — append-only реестр `staff_payroll_ledger` пишется только вставкой (`REVOKE UPDATE, DELETE` для роли приложения, как того требует `PILOT-1-RBAC-MATRIX` платформы). Остаток сотрудника («Перенос» в таблице) — агрегат реестра, а не изменяемое поле. Исправление ошибки — сторнирующая запись, а не правка. ### 5. Биометрия не покидает терминал Сервер принимает от терминала только `employee_id`, тип отметки и время. Шаблоны отпечатков остаются в локальном зашифрованном хранилище терминала. Это снижает объём обрабатываемых биометрических персональных данных на сервере до нуля и соответствует `85-BIOMETRIC-DATA-SECURITY.md` платформы. Требование письменного согласия на обработку биометрии в терминале сохраняется (аудит, пункт A2). ### 6. Временная собственность на `Employee` и `OrganizationUnit` Пока в ядре нет ни справочника сотрудников, ни филиалов, Staff ведёт их сам, но: - `Employee.core_user_id` — nullable-ссылка на учётную запись ядра, `User != Employee`; - таблицы содержат `external_ref` для сопоставления при переносе владения в Core; - модуль не заводит собственные таблицы пользователей, ролей и паролей. Когда ядро получит `Employee`/`Branch`, Staff переходит на них через adapters; это зафиксировано как обязательство, а не «потом разберёмся». ### 7. Форматы платформы: следуем коду, а не документации - права — точечные в нормативном формате платформы `staff::`, например `staff:payroll:read`; существующие права терминала сохраняются; - сущности в манифесте — голый PascalCase (`PayrollRun`), как требует `packages/contracts/src/entity-index.ts`; - события — плоский конверт каталога Pilot 1 (`event_id`, `event_type`, `version`, `tenant_id`, `occurred_at`, `actor_id`, `correlation_id`, `payload`), имена вида `staff.payroll.run_approved`; - ответы API — конверт `data`/`meta`/`errors` платформы, а не RFC 7807; идемпотентность — заголовок `Idempotency-Key`. Расхождения документации и кода платформы вынесены владельцу отдельным списком; выбор в пользу кода сделан потому, что именно он валидирует установку модуля. ### 8. Standalone остаётся рабочим режимом Рантайм модулей платформы сегодня не может ни выдать права модуля роли, ни доставить событие модулю. Поэтому `standalone` — не временная заглушка, а поддерживаемый режим до готовности ядра: свой JWT для мобильного приложения и терминала, свои проверки прав. В `platform` режиме те же порты отдаются адаптерам ядра. Оба режима обязаны давать одинаковый результат расчёта. ### 9. Мобильное приложение — отдельный клиент поверх версионированного API Приложение (React Native + Expo) не получает доступ к базе и не содержит правил расчёта: только вызовы API и отображение объяснения, посчитанного сервером. Контракт для него фиксируется в `api/openapi.yaml` и описан в [APP-CORE.md](../APP-CORE.md). ## Consequences Расчёт воспроизводим, история ставок сохраняется, ведомости прошлых периодов не «плывут» при изменении ставок. Append-only реестр исключает тихую правку денег. Цена: больше таблиц и явная работа с версиями ставок вместо редактирования одного поля. Временное владение `Employee` и `OrganizationUnit` — сознательный долг перед контрактом платформы; он погашается миграцией на Core, когда там появятся эти сущности. Отклонено: считать зарплату на клиенте; хранить деньги в `NUMERIC`; редактировать ставки без версий; переносить шаблоны отпечатков на сервер; ждать готовности рантайма модулей платформы, ничего не делая.