krona-staff/docs/adr/ADR-002-payroll-domain-and-platform-alignment.md
Ochenstarik 3464998168
Some checks are pending
ci / verify (push) Waiting to run
feat: add secure attendance portal and payroll foundation
2026-09-24 15:59:38 +07:00

11 KiB
Raw Permalink Blame History

ADR-002: Домен расчёта зарплаты и выравнивание с контрактом платформы

  • Status: Accepted for REQ-001
  • Date: 2026-09-23
  • Контекст требований: REQ-001

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.

Consequences

Расчёт воспроизводим, история ставок сохраняется, ведомости прошлых периодов не «плывут» при изменении ставок. Append-only реестр исключает тихую правку денег. Цена: больше таблиц и явная работа с версиями ставок вместо редактирования одного поля.

Временное владение Employee и OrganizationUnit — сознательный долг перед контрактом платформы; он погашается миграцией на Core, когда там появятся эти сущности.

Отклонено: считать зарплату на клиенте; хранить деньги в NUMERIC; редактировать ставки без версий; переносить шаблоны отпечатков на сервер; ждать готовности рантайма модулей платформы, ничего не делая.