Add user and employee management enhancements with database schema updates

- Introduced new columns `status`, `created_by`, and `updated_by` in the `users` table to track user status and ownership.
- Updated the `employees` table to include a foreign key reference to the `users` table via `user_id`.
- Created migration script `0012_users_primary.sql` to apply these changes to the database schema.
- Enhanced the `EmployeesService` and `EmployeesRepository` to support user assignments and related data retrieval.
- Updated DTOs and service methods to reflect the new user and employee relationships.
- Added unit tests to validate the new functionality and ensure data integrity.
- Modified existing controllers to accommodate the new fields and relationships in user and employee management.
This commit is contained in:
shancheas
2026-08-26 15:29:18 +07:00
parent f635ebeda0
commit 8a61c94078
50 changed files with 2175 additions and 401 deletions
+95 -3
View File
@@ -1,6 +1,12 @@
import { ConflictException } from '@nestjs/common';
import {
BadRequestException,
ConflictException,
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 { Status } from '../../common/value-objects/status/status';
import { PrivilegesService } from '../privileges/privileges.service';
import type { User } from './user';
import { UsersRepository } from './users.repository';
@@ -11,7 +17,16 @@ describe('UsersService', () => {
let repository: jest.Mocked<
Pick<
UsersRepository,
'findById' | 'findByUsername' | 'create' | 'updatePrivilegeId'
| 'findById'
| 'findByUsername'
| 'create'
| 'update'
| 'updatePrivilegeId'
| 'updateStatus'
| 'bulkUpdateStatus'
| 'delete'
| 'bulkDelete'
| 'list'
>
>;
let privilegesService: jest.Mocked<
@@ -25,8 +40,15 @@ describe('UsersService', () => {
passwordHash: 'hashed',
privilegeId: null,
isSuperadmin: false,
status: Status.create('draft'),
createdAt: now,
updatedAt: now,
createdBy: 'user-1',
updatedBy: 'user-1',
privilege: null,
employee: null,
createdByUser: { id: 'user-1', username: 'alice' },
updatedByUser: { id: 'user-1', username: 'alice' },
};
beforeEach(async () => {
@@ -34,7 +56,13 @@ describe('UsersService', () => {
findById: jest.fn(),
findByUsername: jest.fn(),
create: jest.fn(),
update: jest.fn(),
updatePrivilegeId: jest.fn(),
updateStatus: jest.fn(),
bulkUpdateStatus: jest.fn(),
delete: jest.fn(),
bulkDelete: jest.fn(),
list: jest.fn(),
};
privilegesService = {
findPrivilegeSummary: jest.fn(),
@@ -45,6 +73,10 @@ describe('UsersService', () => {
UsersService,
{ provide: UsersRepository, useValue: repository },
{ provide: PrivilegesService, useValue: privilegesService },
{
provide: ConfigService,
useValue: { getOrThrow: () => 4 },
},
],
}).compile();
@@ -94,9 +126,69 @@ describe('UsersService', () => {
repository.updatePrivilegeId.mockResolvedValue({
...sampleUser,
privilegeId: 'priv-1',
privilege: { id: 'priv-1', code: 'ADMIN', name: 'Admin' },
});
const result = await service.assignPrivilege('user-1', 'priv-1');
expect(result.privilegeId).toBe('priv-1');
expect(result.privilege).toEqual({
id: 'priv-1',
code: 'ADMIN',
name: 'Admin',
});
expect(result).not.toHaveProperty('passwordHash');
});
it('list maps nested relations and omits password', async () => {
repository.list.mockResolvedValue({ data: [sampleUser], total: 1 });
const result = await service.list({ page: 1, limit: 10 });
expect(result.total).toBe(1);
expect(result.data[0]).toMatchObject({
id: 'user-1',
username: 'alice',
status: 'draft',
privilege: null,
employee: null,
createdBy: { id: 'user-1', username: 'alice' },
});
expect(result.data[0]).not.toHaveProperty('passwordHash');
});
it('update rejects status field', async () => {
await expect(
service.update('user-1', { status: 'active', actorUserId: 'user-1' }),
).rejects.toBeInstanceOf(BadRequestException);
});
it('assertCanAuthenticate allows active users without employee', () => {
expect(() =>
service.assertCanAuthenticate({
...sampleUser,
status: Status.create('active'),
}),
).not.toThrow();
});
it('assertCanAuthenticate rejects draft users and inactive employees', () => {
expect(() => service.assertCanAuthenticate(sampleUser)).toThrow(
UnauthorizedException,
);
expect(() =>
service.assertCanAuthenticate({
...sampleUser,
status: Status.create('active'),
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada',
status: Status.create('draft'),
},
}),
).toThrow(UnauthorizedException);
});
it('importCsv requires username and password headers', async () => {
await expect(service.importCsv('name\nalice', 'user-1')).rejects.toBeInstanceOf(
BadRequestException,
);
});
});