Перейти к содержимому

Кадры

Назначение

Управление персоналом: сотрудники, должности, учёт рабочего времени (явки), ставки оплаты, расчёт заработной платы. Полный кадровый цикл — от приёма до увольнения.

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 недоступен
  • Мягкое удаление/восстановление

Явки (учёт рабочего времени)

Актор: Управляющий / Кассир / Сам сотрудник

Явка фиксирует: когда пришёл, когда ушёл, в каком подразделении, на какой должности.

Открытие:

  1. Указывается сотрудник, должность, подразделение, время прихода
  2. Проверки: сотрудник не уволен, нет открытых явок, нет пересечений
  3. Статус → OPEN
  4. Запись в аудит-лог

Закрытие:

  1. Указывается время ухода
  2. Расчёт длительности (минуты)
  3. Расчёт заработка:
    • Почасовая: длительность × ставка
    • Оклад: длительность × (оклад / рабочие часы в месяце)
  4. Статус → CLOSED
  5. Запись в аудит-лог

Автоматическое закрытие (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.createPaymentRateService.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Создан EmployeePositionLOCALIOFFICE-864
POSITION_UNASSIGNEDЗакрыт EmployeePosition (unassignedAt)LOCALIOFFICE-864
SUBDIVISION_ASSIGNEDСоздан EmployeeSubdivisionLOCALIOFFICE-864
SUBDIVISION_UNASSIGNEDЗакрыт EmployeeSubdivisionLOCALIOFFICE-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). FK onDelete: 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.