Personnel Service
Назначение
personnel обслуживает кадровый домен: оргструктуру, должности, сотрудников и шаблоны смен. Сервис построен на NestJS, хранит состояние в PostgreSQL через Prisma, принимает команды и queries через RabbitMQ RPC и публикует кадровые события в RabbitMQ.
Как сервис встроен в систему
- Команды принимает через
efko.personnel.commands. - Запросы принимает через
efko.personnel.queries. - События публикует через
efko.personnel.events. - Очереди разделены на
personnel-service.commands.queueиpersonnel-service.queries.queue. - В
AppModuleподключёнRmqEventEmitterModuleсsourceService: 'personnel'; дополнительно подключён Prisma-based outbox.
Основные модули
DepartmentsModule: подразделения и иерархия оргструктуры.PositionsModule: должности внутри подразделений.EmployeesModule: сотрудники, изменение данных и увольнение.ShiftTemplatesModule: шаблоны сменных графиков.PrismaModule: доступ к БД и репозиториям.OutboxModule: публикация событий изoutbox_messagesвefko.personnel.events.RabbitModule: transport-конфигурация RabbitMQ.
RabbitMQ Commands и Queries
Все команды отправляются в exchange efko.personnel.commands, очередь personnel-service.commands.queue.
Все запросы отправляются в exchange efko.personnel.queries, очередь personnel-service.queries.queue.
Все RabbitRPC handlers работают через ValidationPipe и personnelRpcErrorInterceptor, логируют topic и request metadata (correlationId, userId, userRole).
Commands
PersonnelCreateDepartmentCommand
Создать подразделение.
Exchange: efko.personnel.commands
Request Body:
{
name: string; // Название подразделения
code: string; // Уникальный код подразделения
type: DepartmentType; // Тип: DIVISION | DEPARTMENT | SECTION | UNIT
parentId?: string | null; // UUID родительского подразделения (опционально)
headEmployeeId?: string | null; // UUID руководителя (опционально)
sourceSystemId?: string | null; // ID из внешней системы (для ETL)
}
Response:
{
id: string;
name: string;
code: string;
type: DepartmentType;
parentId: string | null;
headEmployeeId: string | null;
sourceSystemId: string | null;
}
Ошибки: DEPARTMENT_CODE_ALREADY_EXISTS -> 409
PersonnelUpdateDepartmentCommand
Обновить подразделение.
Exchange: efko.personnel.commands
Request Body:
{
id: string; // UUID подразделения
name?: string;
type?: DepartmentType;
parentId?: string | null;
headEmployeeId?: string | null;
}
Response:
{
id: string;
name: string;
code: string;
type: DepartmentType;
parentId: string | null;
headEmployeeId: string | null;
}
PersonnelCreatePositionCommand
Создать должность.
Exchange: efko.personnel.commands
Request Body:
{
title: string; // Название должности
code: string; // Уникальный код должности
departmentId: string; // UUID подразделения
sourceSystemId?: string; // ID из внешней системы
}
Response:
Ошибки:
- POSITION_CODE_ALREADY_EXISTS -> 409
- DEPARTMENT_NOT_FOUND -> 404
PersonnelUpdatePositionCommand
Обновить должность.
Exchange: efko.personnel.commands
Request Body:
Response:
PersonnelCreateEmployeeCommand
Создать сотрудника.
Exchange: efko.personnel.commands
Request Body:
{
personnelNumber: string; // Табельный номер (уникальный)
fullName: string; // Полное имя (формат: Фамилия Имя Отчество)
dateOfBirth: string; // Дата рождения (ISO date: YYYY-MM-DD)
departmentId: string; // UUID подразделения
positionId: string; // UUID должности
hireDate: string; // Дата приема (ISO date)
employmentType: EmploymentType; // MAIN | PART_TIME
sourceSystemId?: string; // ID из внешней системы
}
Response:
{
id: string;
personnelNumber: string;
fullName: string;
departmentId: string;
positionId: string;
status: EmployeeStatus; // ACTIVE | TERMINATED | ON_LEAVE
}
Ошибки:
- INVALID_FULL_NAME -> 400
- DEPARTMENT_NOT_FOUND -> 404
- POSITION_NOT_FOUND -> 404
PersonnelUpdateEmployeeCommand
Обновить данные сотрудника.
Exchange: efko.personnel.commands
Request Body:
{
id: string;
fullName?: string;
departmentId?: string;
positionId?: string;
employmentType?: EmploymentType;
dateOfBirth?: string; // ISO date
}
Response:
{
id: string;
personnelNumber: string;
fullName: string;
dateOfBirth: string;
departmentId: string;
positionId: string;
hireDate: string;
terminationDate: string | null;
employmentType: EmploymentType;
status: EmployeeStatus;
sourceSystemId: string | null;
}
PersonnelTerminateEmployeeCommand
Уволить сотрудника.
Exchange: efko.personnel.commands
Request Body:
Response:
Ошибки:
- EMPLOYEE_ALREADY_TERMINATED -> 409
- EMPLOYEE_NOT_FOUND -> 404
PersonnelCreateShiftTemplateCommand
Создать шаблон смены.
Exchange: efko.personnel.commands
Request Body:
{
name: string; // Название шаблона
shiftType: ShiftType; // DAY_SHIFT | NIGHT_SHIFT | ROTATING
startTime: string; // Время начала (HH:MM)
endTime: string; // Время окончания (HH:MM)
workDaysPattern: string; // Бинарная строка (7 символов: 1111100)
}
Response:
{
id: string;
name: string;
shiftType: ShiftType;
startTime: string;
endTime: string;
workDaysPattern: string;
}
PersonnelUpdateShiftTemplateCommand
Обновить шаблон смены.
Exchange: efko.personnel.commands
Request Body:
{
id: string;
name?: string;
shiftType?: ShiftType;
startTime?: string;
endTime?: string;
workDaysPattern?: string;
}
Response:
{
id: string;
name: string;
shiftType: ShiftType;
startTime: string;
endTime: string;
workDaysPattern: string;
}
Queries
PersonnelGetDepartmentsQuery
Получить список подразделений.
Exchange: efko.personnel.queries
Request Body:
{
type?: DepartmentType; // Фильтр по типу подразделения (опционально)
code?: string; // Фильтр по коду (опционально)
}
Response:
{
departments: Array<{
id: string;
name: string;
code: string;
type: DepartmentType;
parentId: string | null;
headEmployeeId: string | null;
sourceSystemId: string | null;
}>;
}
PersonnelGetPositionsQuery
Получить список должностей.
Exchange: efko.personnel.queries
Request Body:
Response:
PersonnelGetEmployeesQuery
Получить список сотрудников.
Exchange: efko.personnel.queries
Request Body:
{
departmentId?: string; // Фильтр по подразделению
positionId?: string; // Фильтр по должности
status?: EmployeeStatus; // ACTIVE | TERMINATED | ON_LEAVE
employmentType?: EmploymentType; // MAIN | PART_TIME
}
Response:
{
employees: Array<{
id: string;
personnelNumber: string;
fullName: string;
status: EmployeeStatus;
employmentType: EmploymentType;
departmentId: string;
positionId: string;
hireDate: string;
dateOfBirth: string;
terminationDate: string | null;
}>;
}
PersonnelGetShiftTemplatesQuery
Получить список шаблонов смен.
Exchange: efko.personnel.queries
Request Body:
Response:
{
templates: Array<{
id: string;
name: string;
shiftType: ShiftType;
startTime: string;
endTime: string;
workDaysPattern: string;
}>;
}
Основная бизнес-логика
- Подразделения поддерживают иерархию
parent -> children; при создании/обновленииparentIdиheadEmployeeIdможно передавать не только UUID, но и бизнес-идентификаторы, которые резолвятся черезresolveEntityId(...). - Должность жёстко привязана к подразделению; create flow валидирует уникальность
codeи существование подразделения. - Сотрудник связан и с подразделением, и с должностью;
CreateEmployeeUseCaseвалидирует оба reference и использует value objectsPersonnelNumberиFullName. - Увольнение реализовано отдельным use case: меняет статус сотрудника и ставит дату увольнения.
- Шаблоны смен содержат тип смены, время начала/окончания и
workDaysPattern; время и паттерн валидируются value object-ами. - Для ETL-импортов
createuse case-ы по подразделениям, должностям и сотрудникам сначала пытаются делать upsert поsourceSystemId.
Хранение данных
PostgreSQL/Prisma, основные таблицы:
departments: имя, код, тип,parent_id,head_employee_id,source_system_id.positions: title, code,department_id,source_system_id.employees: табельный номер, ФИО, дата рождения, подразделение, должность, даты приёма/увольнения, тип занятости, статус,source_system_id.shift_schedule_templates: имя шаблона, тип смены, время начала/окончания, паттерн рабочих дней.outbox_messages: события кадрового домена для асинхронной публикации.
Интеграции
- RabbitMQ RPC для всех кадровых команд и queries.
- RabbitMQ events:
- подразделения, должности и сотрудники при create/update часто пишут события через outbox;
- увольнение и часть операций используют
EventEmitterServiceнапрямую. - ETL является важным upstream:
- ZUP mapper направляет данные в
PersonnelCreateDepartmentCommand,PersonnelCreatePositionCommand,PersonnelCreateEmployeeCommand,PersonnelCreateShiftTemplateCommand; - create use case-ы умеют обновлять существующие записи по
sourceSystemId, что снижает дубли при повторном импорте.
Обработка ошибок
- Доменные ошибки наследуются от
PersonnelError. - Для RPC используется
personnelRpcErrorInterceptor, который приводит доменные и HTTP ошибки к структуре{ error: { code, message, statusCode } }. - Для HTTP-слоя подключён
AllExceptionsFilter; в HTTP-контексте он возвращает JSON сstatusCodeиmessage. - Основные маппинги:
- ошибки формата (
INVALID_*) ->400 - ошибки отсутствующих сущностей ->
404 - конфликтные состояния (
*_ALREADY_EXISTS,EMPLOYEE_ALREADY_TERMINATED,EMPLOYEE_NOT_ACTIVE) ->409
Observability и logging
- Логирование через
nestjs-pino. - В dev-режиме лог пишется в
logs/personnel.logи очищается на старте. - Bootstrap включает
enableShutdownHooks()и глобальный exception filter. - Контроллеры логируют RPC topic и request metadata.
- В
EmployeesControllerесть дополнительный debug/error лог дляPersonnelCreateEmployeeCommand, включая сериализацию payload и ошибки выполнения.
Зависимости
- NestJS
- Prisma + PostgreSQL
@golevelup/nestjs-rabbitmqnestjs-pino@efko-kernel/contracts@efko-kernel/interfaces@efko-kernel/nest-utils
Наблюдения и пробелы по коду
- Use case-ы используют смешанную схему публикации событий: outbox для части create/update операций и direct publish для части команд.
- В
DepartmentsModule,EmployeesModule,PositionsModuleявно не импортируетсяOutboxModule, хотя create use case-ы зависят отOutboxMessageRepository; корректность разрешения зависимости предполагает наличие глобального провайдера в общем composition root. - Как и в
production, внешний HTTP-сервер поднимается, но бизнес-операции экспонированы как RabbitRPC, а не REST.