Кадры
Назначение
Управление персоналом: сотрудники, должности, учёт рабочего времени (явки), ставки оплаты, расчёт заработной платы. Полный кадровый цикл — от приёма до увольнения.
5 контроллеров, ~30 эндпоинтов.
Сценарии
Сотрудники
Актор: HR-менеджер / Управляющий
Жизненный цикл:
Создание → Назначение должностей → Назначение подразделений → Работа (явки) → Увольнение → Мягкое удалениеПри создании:
- ФИО, дата рождения, контакты (телефон, email)
- Табельный номер (уникальный в корпорации)
- Опционально: логин + пароль для входа в систему
- Привязка к подразделению по умолчанию
- Назначение руководителя (другой сотрудник)
Назначение должностей и подразделений:
- Сотрудник может занимать несколько должностей (бариста + кассир)
- Может работать в нескольких подразделениях
- Должности и подразделения — только из своей корпорации
Увольнение:
- Устанавливается дата увольнения (позже даты договора)
- Проверка с учётом timezone корпорации
- Автоматически удаляются все сессии пользователя (событие
employee-dismissed)
Бизнес-правила (агрегат EmployeeAggregate, instance-based):
- Возраст: 14–120 лет (
ensureValidBirthDate) - Телефон: формат +7… (
ensureValidMobilePhone) - Email: стандартная валидация (
ensureValidEmail) - Логин: латиница, цифры,
.,_(ensureValidLogin) - Пароль: ≥8 символов, буква + цифра (
ensureValidPassword) - Если указан логин — пароль обязателен (
ensureValidCredentials) - Табельный номер, телефон, email, логин — уникальны в корпорации
- Нельзя удалить сотрудника с открытыми явками
- Нельзя назначить руководителем самого себя (и циклические зависимости)
Доменные события: employee-credentials-updated, employee-dismissed
Должности
Актор: HR-менеджер
Графики работы:
- Почасовой (HOURLY) — оплата за часы. Обязательна
hourlyRate - Оклад (SALARY) — фиксированная месячная оплата. Обязателен
salary
Бизнес-правила (агрегат PositionAggregate, static):
- Название: 2–100 символов, уникальное в корпорации
- Для HOURLY:
hourlyRate> 0,salaryнедоступен - Для SALARY:
salary> 0,hourlyRateнедоступен - Мягкое удаление/восстановление
Явки (учёт рабочего времени)
Актор: Управляющий / Кассир / Сам сотрудник
Явка фиксирует: когда пришёл, когда ушёл, в каком подразделении, на какой должности.
Открытие:
- Указывается сотрудник, должность, подразделение, время прихода
- Проверки: сотрудник не уволен, нет открытых явок, нет пересечений
- Статус → OPEN
- Запись в аудит-лог
Закрытие:
- Указывается время ухода
- Расчёт длительности (минуты)
- Расчёт заработка:
- Почасовая: длительность × ставка
- Оклад: длительность × (оклад / рабочие часы в месяце)
- Статус → CLOSED
- Запись в аудит-лог
Автоматическое закрытие (cron каждый час):
- Ищет явки в OPEN >24ч → статус OVERDUE
- Время ухода = текущее время (или дата увольнения)
- Автоматический расчёт заработка
- Блокировка записи для предотвращения конфликтов
Бизнес-правила (агрегат AttendanceAggregate, static):
- Время прихода не в будущем
- Время ухода > время прихода
- Одновременно одна открытая явка на сотрудника
- Явки не пересекаются по времени
- Редактирование: только OPEN/OVERDUE
- Удаление: только CLOSED (нельзя удалить OPEN/OVERDUE)
- Опциональный тип (работа, больничный, отпуск)
- Каждая операция → аудит-лог
Ставки оплаты
Актор: HR-менеджер
Ставка привязана к паре сотрудник + должность с периодом действия. Отдельного публичного CRUD-API над PaymentRate нет (LOCALIOFFICE-867): ставки создаются и закрываются только в составе операций над карточкой сотрудника.
Создание: при POST /staffing/employees блок paymentRates: { startDate, items[] } (либо primaryPosition.paymentRate) → EmployeeService.create → PaymentRateService.applyRatesInTx создаёт PaymentRate для каждой позиции в одной транзакции. Если hourlyRate/salary не передан — берётся из Position дефолтом (LOCALIOFFICE-764 §3). Предыдущая незакрытая ставка для этой должности автоматически закрывается датой новый startDate − 1 день.
Изменение: при PATCH /staffing/employees/:id блок paymentRates обрабатывается так же (applyRatesInTx). Полная desired-state-семантика (новая позиция → создать ставку, исчезла → закрыть, изменилась ставка → закрыть текущую и открыть новую) реализуется в PUT /employees/:id (LOCALIOFFICE-864).
Закрытие: при увольнении сотрудника EmployeeService.dismiss массово закрывает все открытые PaymentRate через paymentRateRepository.closeAllOpenByEmployeeId и пишет один аудит-лог PAYMENT_RATE_CLOSED на сотрудника.
Бизнес-правила (агрегат PaymentRateAggregate, static):
- Сотрудник не уволен и не удалён (проверяется в
EmployeeServiceдо вызоваapplyRatesInTx) - Должность назначена сотруднику или назначается автоматически (
autoAssignPosition: true) сisPrimary = false - Ставка > 0
- Дата конца > дата начала (если указана)
- Тип ставки определяется графиком должности (
workScheduleType: HOURLY/SCHEDULED →hourlyRate, SALARY →salary)
Связи с другими контекстами
| Направление | Тип | Описание |
|---|---|---|
| Кадры → Аутентификация | Событие | Увольнение / смена пароля → инвалидация сессий |
| Организация → Кадры | ACL | Подразделения, корпорация — привязка сотрудников |
| POS → Кадры | Данные | Кто открыл/закрыл смену, кто пробил чек |
| Бухгалтерия → Кадры | Данные | Сотрудник в аналитике проводки |
Автоматизация
| Процесс | Расписание | Описание |
|---|---|---|
AttendanceOverdueScheduler | Каждый час | OPEN >24ч → OVERDUE, автоматический расчёт заработка |
Сущности
| Сущность | Описание |
|---|---|
| Сотрудник (Employee) | ФИО, контакты, табельный номер, логин. Привязан к корпорации, нескольким должностям и подразделениям. Мягкое удаление |
| Должность (Position) | Название, график (HOURLY/SALARY), базовая ставка. Мягкое удаление |
| Явка (Attendance) | Приход, уход, длительность, заработок, статус (OPEN/CLOSED/OVERDUE). Аудит-лог |
| Ставка оплаты (PaymentRate) | Индивидуальная ставка сотрудник+должность на период |
| Тип явки (AttendanceType) | Справочник: работа, больничный, отпуск, прогул |
| Аудит-лог явки (AttendanceAuditLog) | Все изменения явки: кто, когда, что |
| Аудит-лог сотрудника (EmployeeAuditLog) | Все изменения профиля сотрудника |
Журнал изменений (audit-log)
Записи аудит-логов содержат поля action (enum действия) и source (enum происхождения записи). Это позволяет различать записи, созданные офисом, и записи, прилетевшие из обратного канала кассы.
EmployeeAuditLogAction
| Действие | Когда пишется | Источник реализации |
|---|---|---|
CREATE | Создан сотрудник | EmployeeService.create |
UPDATE | Обновлён профиль | EmployeeService.update |
DELETE | Софт-удалён сотрудник | EmployeeService.delete |
DISMISSED | Сотрудник уволен (cascade-закрытие связей) | LOCALIOFFICE-765 (#5) |
TRANSFERRED | Перевод между подразделениями/юрлицами | LOCALIOFFICE-770 (#10) |
POSITION_ASSIGNED | Создан EmployeePosition | LOCALIOFFICE-864 |
POSITION_UNASSIGNED | Закрыт EmployeePosition (unassignedAt) | LOCALIOFFICE-864 |
SUBDIVISION_ASSIGNED | Создан EmployeeSubdivision | LOCALIOFFICE-864 |
SUBDIVISION_UNASSIGNED | Закрыт EmployeeSubdivision | LOCALIOFFICE-864 |
PAYMENT_RATE_CREATED | Создана ставка оплаты | Не пишется — значение enum зарезервировано; точку вызова (EmployeeService.create/update после PaymentRateService.applyRatesInTx) предстоит реализовать отдельной задачей |
PAYMENT_RATE_CLOSED | Установлен endDate ставки | EmployeeService.dismiss (массовое закрытие при увольнении) |
POSITION_PRIMARY_CHANGED | Сменилась primary-позиция (POST /employees/:id/positions/:positionId/set-primary) | LOCALIOFFICE-864 (BFF PUT /employees/:id) |
AttendanceAuditLogAction
| Действие | Когда пишется | Источник реализации |
|---|---|---|
CREATE | Создана явка | AttendanceService.create |
UPDATE | Изменены поля явки | AttendanceService.update |
CLOSE | Явка закрыта пользователем | AttendanceService.close |
DELETE | Софт-удалена явка | AttendanceService.delete |
AUTO_CLOSED_ON_OVERDUE | Автозакрытие просроченной явки | AttendanceOverdueScheduler |
AUTO_CLOSED_ON_DISMISSAL | Автозакрытие при увольнении сотрудника | LOCALIOFFICE-765 (#5) |
MANUALLY_EDITED | Ручная корректировка confirmed*Time (флаг isManuallyEdited) | LOCALIOFFICE-766 (#6) |
AuditLogSource
| Значение | Семантика |
|---|---|
OFFICE | Запись создана действием в офисе: REST-эндпоинт, scheduler, админ-операция. Default для всех записей, создаваемых backend’ом офиса. |
KASSA | Запись создана при обработке события из outbox кассы (core.company.kassa.attendance.*). Используется в LOCALIOFFICE-770 (#9) для различения ручной корректировки кассира vs офисного HR-менеджера. |
Денормализованное поле EmployeeAuditLog.corporationId существует ради индекса (corporationId, action, createdAt DESC) — используется при выборках журнала по корпорации с фильтром по действию.
Спецификации удаления
В src/staffing/domain/specifications/ — отдельные классы, реализующие интерфейс Specification<T> из specification.interface.ts. Метод isSatisfiedBy(candidate, tx?) возвращает SpecResult = { allowed: boolean, reason?: string, conflicts?: { entity: string, count: number }[] }.
CanDeleteEmployeeSpec— заготовка под будущий hard-delete (сейчас офис делает только soft). Минимально проверяетAttendanceиPaymentRate. Расширение проверками противCashShift/PosReceipt/PosPaymentи LegalEntity-officer добавляется при появлении самого hard-delete-эндпоинта (см. LOCALIOFFICE-765 #5). FKonDelete: Restrictстрахует БД от потери истории до этого момента.CanDeletePositionSpec— активныеEmployeePosition(безunassignedAt), любыеPaymentRateилиAttendance.CanDeleteAttendanceTypeSpec— наличиеAttendanceили попытка удалить дефолтный тип (isDefault=true).
Проверка статуса CLOSED для удаления явки осталась в AttendanceAggregate.ensureCanDelete() — отдельная спека на это не заведена, потому что внешних ссылок у Attendance нет, а agregate всё равно нужен для других сценариев.
Аналогично в src/organization/domain/specifications/:
CanDeleteLegalEntitySpec— учитывает активныеSubdivision, активныеEmployeePosition.legalEntityId(совместительство) и активныхEmployee.primaryLegalEntityId(черезOrganizationStaffingContextAcl).
Сервисный слой через хелпер specResultToHttp (#common/utils/spec-to-http.js) транслирует allowed=false в HTTP 409 Conflict с телом { message: <reason>, conflicts: [...] }.
FK onDelete: Restrict
Все ссылки на staffing-сущности используют onDelete: Restrict (вместо прежнего Cascade/SetNull) — это страхует от случайного физического удаления. Применяется к: Attendance.{employee,position,subdivision,attendanceType}, PaymentRate.{employee,position}, EmployeePosition.{employee,position,legalEntity}, EmployeeSubdivision.{employee,subdivision}, EmployeeAuditLog.{employee,user}, AttendanceAuditLog.{attendance,user}. Бизнес-проверки CanDelete*Spec выполняются до удаления, чтобы выдавать русское сообщение и список зависимостей вместо P2003 ForeignKeyConstraintViolation.