Enhance employee and user management with linked user functionality

- Introduced `EmployeeUserWrite` type to manage user details associated with employees.
- Updated `EmployeesService` and `EmployeesRepository` to support user assignment and retrieval by user ID.
- Enhanced DTOs to include user information for employee creation and updates.
- Implemented validation to ensure proper handling of user data during employee operations.
- Added unit and e2e tests to validate the new functionality and ensure data integrity in user-employee relationships.
- Modified existing controllers to accommodate the new user linkage features in employee management.
This commit is contained in:
shancheas
2026-08-27 12:22:25 +07:00
parent 8a61c94078
commit 790725e227
16 changed files with 736 additions and 18 deletions
+171 -2
View File
@@ -1,12 +1,16 @@
import {
BadRequestException,
ConflictException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { DateTime } from '../../common/value-objects/date-time/date-time';
import { PhoneNumber } from '../../common/value-objects/phone-number/phone-number';
import { Status } from '../../common/value-objects/status/status';
import type { Employee } from '../configuration/employees/employee';
import { EmployeesRepository } from '../configuration/employees/employees.repository';
import { PrivilegesService } from '../privileges/privileges.service';
import type { User } from './user';
import { UsersRepository } from './users.repository';
@@ -32,6 +36,9 @@ describe('UsersService', () => {
let privilegesService: jest.Mocked<
Pick<PrivilegesService, 'findPrivilegeSummary'>
>;
let employeesRepository: jest.Mocked<
Pick<EmployeesRepository, 'findById' | 'findByUserId' | 'update'>
>;
const now = DateTime.fromUnixMs(1_700_000_000_000);
const sampleUser: User = {
@@ -51,6 +58,21 @@ describe('UsersService', () => {
updatedByUser: { id: 'user-1', username: 'alice' },
};
const sampleEmployee: Employee = {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
phone: PhoneNumber.create('+6281234567890'),
position: 'sales',
status: Status.create('draft'),
createdAt: now,
updatedAt: now,
createdBy: 'actor-1',
updatedBy: 'actor-1',
userId: null,
user: null,
};
beforeEach(async () => {
repository = {
findById: jest.fn(),
@@ -67,12 +89,18 @@ describe('UsersService', () => {
privilegesService = {
findPrivilegeSummary: jest.fn(),
};
employeesRepository = {
findById: jest.fn(),
findByUserId: jest.fn(),
update: jest.fn(),
};
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: repository },
{ provide: PrivilegesService, useValue: privilegesService },
{ provide: EmployeesRepository, useValue: employeesRepository },
{
provide: ConfigService,
useValue: { getOrThrow: () => 4 },
@@ -187,8 +215,149 @@ describe('UsersService', () => {
});
it('importCsv requires username and password headers', async () => {
await expect(service.importCsv('name\nalice', 'user-1')).rejects.toBeInstanceOf(
BadRequestException,
await expect(
service.importCsv('name\nalice', 'user-1'),
).rejects.toBeInstanceOf(BadRequestException);
});
it('createManaged links an existing employee by id', async () => {
repository.create.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue(sampleEmployee);
employeesRepository.update.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
repository.findById.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
const result = await service.createManaged({
username: 'alice',
password: 'password123',
actorUserId: 'actor-1',
employeeId: 'emp-1',
});
expect(employeesRepository.update).toHaveBeenCalledWith(
'emp-1',
expect.objectContaining({
userId: 'actor-1',
assignedUserId: 'user-1',
}),
);
expect(result.employee).toEqual({
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
});
});
it('createManaged rejects linking an employee assigned to another user', async () => {
repository.create.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue({
...sampleEmployee,
userId: 'other-user',
});
repository.delete.mockResolvedValue(undefined);
await expect(
service.createManaged({
username: 'alice',
password: 'password123',
actorUserId: 'actor-1',
employeeId: 'emp-1',
}),
).rejects.toBeInstanceOf(ConflictException);
expect(repository.delete).toHaveBeenCalledWith('user-1');
});
it('update with employeeId null unlinks the current employee', async () => {
repository.update.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
employeesRepository.findByUserId.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
employeesRepository.update.mockResolvedValue({
...sampleEmployee,
userId: null,
});
repository.findById.mockResolvedValue({ ...sampleUser, employee: null });
const result = await service.update('user-1', {
actorUserId: 'actor-1',
employeeId: null,
});
expect(employeesRepository.update).toHaveBeenCalledWith(
'emp-1',
expect.objectContaining({ assignedUserId: null, userId: 'actor-1' }),
);
expect(result.employee).toBeNull();
});
it('update with employeeId reassigns after unlinking the previous employee', async () => {
repository.update.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue({
...sampleEmployee,
id: 'emp-2',
code: 'EMP_02',
});
employeesRepository.findByUserId.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
employeesRepository.update.mockResolvedValue(sampleEmployee);
repository.findById.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-2',
code: 'EMP_02',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
await service.update('user-1', {
actorUserId: 'actor-1',
employeeId: 'emp-2',
});
expect(employeesRepository.update).toHaveBeenNthCalledWith(
1,
'emp-1',
expect.objectContaining({ assignedUserId: null }),
);
expect(employeesRepository.update).toHaveBeenNthCalledWith(
2,
'emp-2',
expect.objectContaining({ assignedUserId: 'user-1' }),
);
});
it('update with unknown employeeId throws NotFoundException', async () => {
repository.findById.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue(null);
await expect(
service.update('user-1', {
actorUserId: 'actor-1',
employeeId: 'missing',
}),
).rejects.toBeInstanceOf(NotFoundException);
expect(repository.update).not.toHaveBeenCalled();
});
});