Add employees management module with database schema and validation

- Introduced `EmployeesModule` to manage employee data, including read and write controllers.
- Created database migrations for the `employees` table, including constraints and unique indexes.
- Implemented validation for employee fields such as name, code, and position with corresponding utility functions.
- Developed service and repository layers for handling employee data operations.
- Added unit tests for the employees service, repository, and controllers to ensure functionality and correctness.
- Updated application module to include the new `EmployeesModule` for better organization.
This commit is contained in:
shancheas
2026-08-24 15:15:52 +07:00
parent cdcc508947
commit ddbe8a9ef8
20 changed files with 3355 additions and 2 deletions
@@ -0,0 +1,108 @@
import {
EMPLOYEE_CODE_MAX_LENGTH,
EMPLOYEE_NAME_MAX_LENGTH,
isAllowedCsvUpload,
isValidEmployeeCode,
isValidEmployeeName,
isValidEmployeePosition,
parseCsvRecord,
} from './employee-fields';
describe('employee fields', () => {
describe('isValidEmployeeName', () => {
it.each(['Ada', 'Jean Luc', 'A', 'North West Crew'])(
'accepts %s',
(name) => {
expect(isValidEmployeeName(name)).toBe(true);
},
);
it.each(['', 'Ada1', 'Jean-Luc', 'EMP_01', ' Ada', 'Ada ', 'Jean Luc'])(
'rejects %s',
(name) => {
expect(isValidEmployeeName(name)).toBe(false);
},
);
it('rejects names longer than 64 characters', () => {
expect(
isValidEmployeeName('A'.repeat(EMPLOYEE_NAME_MAX_LENGTH + 1)),
).toBe(false);
expect(isValidEmployeeName('A'.repeat(EMPLOYEE_NAME_MAX_LENGTH))).toBe(
true,
);
});
});
describe('isValidEmployeeCode', () => {
it.each(['EMP', 'EMP_01', 'A', 'ops2', 'A_b_1'])('accepts %s', (code) => {
expect(isValidEmployeeCode(code)).toBe(true);
});
it.each(['', 'EMP 01', 'EMP-01', 'EMP.01', ' EMP', 'EMP '])(
'rejects %s',
(code) => {
expect(isValidEmployeeCode(code)).toBe(false);
},
);
it('rejects codes longer than 16 characters', () => {
expect(
isValidEmployeeCode('A'.repeat(EMPLOYEE_CODE_MAX_LENGTH + 1)),
).toBe(false);
expect(isValidEmployeeCode('A'.repeat(EMPLOYEE_CODE_MAX_LENGTH))).toBe(
true,
);
});
});
describe('isValidEmployeePosition', () => {
it.each(['sales', 'driver', 'crew'])('accepts %s', (position) => {
expect(isValidEmployeePosition(position)).toBe(true);
});
it.each(['', 'Sales', 'pilot', 'crew '])('rejects %s', (position) => {
expect(isValidEmployeePosition(position)).toBe(false);
});
});
describe('parseCsvRecord', () => {
it('keeps commas inside quoted fields', () => {
expect(parseCsvRecord('EMP_01,Ada Lovelace,"sales, lead"')).toEqual([
'EMP_01',
'Ada Lovelace',
'sales, lead',
]);
});
it('unescapes doubled quotes', () => {
expect(parseCsvRecord('"Say ""hello""",x')).toEqual(['Say "hello"', 'x']);
});
});
describe('isAllowedCsvUpload', () => {
it('accepts csv mime or .csv names', () => {
expect(
isAllowedCsvUpload({
mimetype: 'text/csv',
originalname: 'x.txt',
}),
).toBe(true);
expect(
isAllowedCsvUpload({
mimetype: 'application/octet-stream',
originalname: 'employees.csv',
}),
).toBe(true);
});
it('rejects non-csv files', () => {
expect(
isAllowedCsvUpload({
mimetype: 'application/pdf',
originalname: 'x.pdf',
}),
).toBe(false);
});
});
});