81 lines
11 KiB
Markdown
81 lines
11 KiB
Markdown
# 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:<resource>:<action>`, например `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`; редактировать ставки без версий; переносить шаблоны отпечатков на сервер; ждать готовности рантайма модулей платформы, ничего не делая.
|