diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index d32a67f..8bce1b6 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -2209,13 +2209,11 @@ "description": "Employee`s company code" }, "first_work_day": { - "format": "date-time", "type": "string", "example": "23/09/3018", "description": "Employee`s first working day" }, "last_work_day": { - "format": "date-time", "type": "string", "example": "25/03/3019", "description": "Employee`s last working day" @@ -2465,19 +2463,16 @@ "description": "ID number of a shift code (link with bank-codes)" }, "date": { - "format": "date-time", "type": "string", "example": "3018-10-20T00:00:00.000Z", "description": "Date where the shift takes place" }, "start_time": { - "format": "date-time", "type": "string", "example": "3018-10-20T08:00:00.000Z", "description": "Start time of the said shift" }, "end_time": { - "format": "date-time", "type": "string", "example": "3018-10-20T17:00:00.000Z", "description": "End time of the said shift" @@ -2511,19 +2506,16 @@ "description": "ID number of a shift code (link with bank-codes)" }, "date": { - "format": "date-time", "type": "string", "example": "3018-10-20T00:00:00.000Z", "description": "Date where the shift takes place" }, "start_time": { - "format": "date-time", "type": "string", "example": "3018-10-20T08:00:00.000Z", "description": "Start time of the said shift" }, "end_time": { - "format": "date-time", "type": "string", "example": "3018-10-20T17:00:00.000Z", "description": "End time of the said shift" @@ -2640,7 +2632,8 @@ "id": { "type": "number", "example": 1, - "description": "Unique ID of a bank-code (auto-generated)" + "description": "Unique ID of a bank-code (auto-generated)", + "readOnly": true }, "type": { "type": "string", @@ -2677,7 +2670,8 @@ "id": { "type": "number", "example": 1, - "description": "Unique ID of a bank-code (auto-generated)" + "description": "Unique ID of a bank-code (auto-generated)", + "readOnly": true }, "type": { "type": "string", diff --git a/npm b/npm new file mode 100644 index 0000000..e69de29 diff --git a/prisma/mock-seeds-scripts/01-bankCodes.ts b/prisma/mock-seeds-scripts/01-bankCodes.ts index 04a76df..6d5cec2 100644 --- a/prisma/mock-seeds-scripts/01-bankCodes.ts +++ b/prisma/mock-seeds-scripts/01-bankCodes.ts @@ -5,23 +5,17 @@ const prisma = new PrismaClient(); async function main() { const presets = [ // type, categorie, modifier, bank_code - ['SHIFT', 'REGULAR', 1.0, 'REG'], - ['SHIFT', 'EVENING', 1.15, 'EVE'], - ['SHIFT', 'NIGHT', 1.25, 'NGT'], - ['SHIFT', 'WEEKEND', 1.5, 'WKD'], - ['SHIFT', 'HOLIDAY', 2.0, 'HLD'], + ['REGULAR' ,'SHIFT', 1.0 , 'G1'], + ['EVENING' ,'SHIFT', 1.25, 'G43'], + ['Emergency','SHIFT', 2 , 'G48'], + ['HOLIDAY' ,'SHIFT', 2.0 , 'G700'], - ['EXPENSE', 'MEAL', 1.0, 'EXP_MEAL'], - ['EXPENSE', 'MILEAGE', 1.0, 'EXP_MILE'], - ['EXPENSE', 'HOTEL', 1.0, 'EXP_HOTEL'], - ['EXPENSE', 'SUPPLIES', 1.0, 'EXP_SUP'], + ['EXPENSES','EXPENSE', 1.0 , 'G517'], + ['MILEAGE' ,'EXPENSE', 0.72, 'G57'], + ['PER_DIEM','EXPENSE', 1.0 , 'G502'], - ['LEAVE', 'SICK', 1.0, 'LV_SICK'], - ['LEAVE', 'VACATION', 1.0, 'LV_VAC'], - ['LEAVE', 'UNPAID', 0.0, 'LV_UNP'], - ['LEAVE', 'BEREAVEMENT', 1.0, 'LV_BER'], - ['LEAVE', 'PARENTAL', 1.0, 'LV_PAR'], - ['LEAVE', 'LEGAL', 1.0, 'LV_LEG'], + ['SICK' ,'LEAVE', 1.0, 'G105'], + ['VACATION' ,'LEAVE', 1.0, 'G305'], ]; await prisma.bankCodes.createMany({ @@ -34,7 +28,7 @@ async function main() { skipDuplicates: true, }); - console.log('✓ BankCodes: 15 rows'); + console.log('✓ BankCodes: 9 rows'); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/07-leave-requests-future.ts b/prisma/mock-seeds-scripts/07-leave-requests-future.ts index 07fe14c..54f14b8 100644 --- a/prisma/mock-seeds-scripts/07-leave-requests-future.ts +++ b/prisma/mock-seeds-scripts/07-leave-requests-future.ts @@ -13,7 +13,7 @@ async function main() { const employees = await prisma.employees.findMany({ select: { id: true } }); const bankCodes = await prisma.bankCodes.findMany({ - where: { type: 'LEAVE' }, + where: { categorie: 'LEAVE' }, select: { id: true }, }); diff --git a/prisma/mock-seeds-scripts/08-leave-requests-archive.ts b/prisma/mock-seeds-scripts/08-leave-requests-archive.ts index 8935548..1ef2554 100644 --- a/prisma/mock-seeds-scripts/08-leave-requests-archive.ts +++ b/prisma/mock-seeds-scripts/08-leave-requests-archive.ts @@ -11,12 +11,11 @@ function daysAgo(n:number) { async function main() { const employees = await prisma.employees.findMany({ select: { id: true } }); - const bankCodes = await prisma.bankCodes.findMany({ select: { id: true }, where: { type: 'LEAVE' } }); + const bankCodes = await prisma.bankCodes.findMany({ select: { id: true }, where: { categorie: 'LEAVE' } }); const types = Object.values(LeaveTypes); const statuses = Object.values(LeaveApprovalStatus); - // ✅ typer le tableau pour éviter never[] const created: LeaveRequests[] = []; for (let i = 0; i < 10; i++) { diff --git a/prisma/mock-seeds-scripts/10-shifts.ts b/prisma/mock-seeds-scripts/10-shifts.ts index b7ad63b..3fe1791 100644 --- a/prisma/mock-seeds-scripts/10-shifts.ts +++ b/prisma/mock-seeds-scripts/10-shifts.ts @@ -14,7 +14,7 @@ function daysAgo(n:number) { } async function main() { - const bankCodes = await prisma.bankCodes.findMany({ where: { type: 'SHIFT' }, select: { id: true } }); + const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } }); if (!bankCodes.length) throw new Error('Need SHIFT bank codes'); const employees = await prisma.employees.findMany({ select: { id: true } }); diff --git a/prisma/mock-seeds-scripts/11-shifts-archive.ts b/prisma/mock-seeds-scripts/11-shifts-archive.ts index 45d8d35..fa22ddb 100644 --- a/prisma/mock-seeds-scripts/11-shifts-archive.ts +++ b/prisma/mock-seeds-scripts/11-shifts-archive.ts @@ -13,7 +13,7 @@ function daysAgo(n:number) { } async function main() { - const bankCodes = await prisma.bankCodes.findMany({ where: { type: 'SHIFT' }, select: { id: true } }); + const bankCodes = await prisma.bankCodes.findMany({ where: { categorie: 'SHIFT' }, select: { id: true } }); const employees = await prisma.employees.findMany({ select: { id: true } }); for (const e of employees) { diff --git a/prisma/mock-seeds-scripts/12-expenses.ts b/prisma/mock-seeds-scripts/12-expenses.ts index b539c83..2da1eb5 100644 --- a/prisma/mock-seeds-scripts/12-expenses.ts +++ b/prisma/mock-seeds-scripts/12-expenses.ts @@ -10,7 +10,7 @@ function daysAgo(n:number) { } async function main() { - const expenseCodes = await prisma.bankCodes.findMany({ where: { type: 'EXPENSE' }, select: { id: true } }); + const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } }); if (!expenseCodes.length) throw new Error('Need EXPENSE bank codes'); const timesheets = await prisma.timesheets.findMany({ select: { id: true } }); diff --git a/prisma/mock-seeds-scripts/13-expenses-archive.ts b/prisma/mock-seeds-scripts/13-expenses-archive.ts index bc6b0af..3d87908 100644 --- a/prisma/mock-seeds-scripts/13-expenses-archive.ts +++ b/prisma/mock-seeds-scripts/13-expenses-archive.ts @@ -11,7 +11,7 @@ function daysAgo(n:number) { } async function main() { - const expenseCodes = await prisma.bankCodes.findMany({ where: { type: 'EXPENSE' }, select: { id: true } }); + const expenseCodes = await prisma.bankCodes.findMany({ where: { categorie: 'EXPENSE' }, select: { id: true } }); const timesheets = await prisma.timesheets.findMany({ select: { id: true } }); // ✅ typer pour éviter never[] diff --git a/prisma/mock-seeds-scripts/READMEmockseeds.md b/prisma/mock-seeds-scripts/READMEmockseeds.md index 293ce74..3579198 100644 --- a/prisma/mock-seeds-scripts/READMEmockseeds.md +++ b/prisma/mock-seeds-scripts/READMEmockseeds.md @@ -40,7 +40,7 @@ shifts = 10 × (#employees) shifts_archive = 30 × (#employees) -bank_codes = 15 +bank_codes = 9 expenses = 5 diff --git a/src/modules/bank-codes/dtos/create-bank-code.dto.ts b/src/modules/bank-codes/dtos/create-bank-code.dto.ts index 8832c06..f2bec7b 100644 --- a/src/modules/bank-codes/dtos/create-bank-code.dto.ts +++ b/src/modules/bank-codes/dtos/create-bank-code.dto.ts @@ -1,11 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsNumber, IsString } from "class-validator"; +import { Type } from "class-transformer"; +import { Allow, IsNotEmpty, IsNumber, IsString } from "class-validator"; export class CreateBankCodeDto { @ApiProperty({ example: 1, description: 'Unique ID of a bank-code (auto-generated)', + readOnly: true, }) + @Allow() id: number; @ApiProperty({ @@ -28,6 +31,7 @@ export class CreateBankCodeDto { example: '0, 0.72, 1, 1.5, 2', description: 'modifier number to apply to salary', }) + @Type(()=> Number) @IsNumber() @IsNotEmpty() modifier: number; diff --git a/src/modules/bank-codes/services/bank-codes.service.ts b/src/modules/bank-codes/services/bank-codes.service.ts index 7e4c3e5..5e1549a 100644 --- a/src/modules/bank-codes/services/bank-codes.service.ts +++ b/src/modules/bank-codes/services/bank-codes.service.ts @@ -9,7 +9,14 @@ export class BankCodesService { constructor(private readonly prisma: PrismaService) {} async create(dto: CreateBankCodeDto): Promise{ - return this.prisma.bankCodes.create({ data: dto }) + return this.prisma.bankCodes.create({ + data: { + type: dto.type, + categorie: dto.categorie, + modifier: dto.modifier, + bank_code: dto.bank_code, + }, + }); } findAll() { @@ -19,15 +26,21 @@ export class BankCodesService { async findOne(id: number) { const bankCode = await this.prisma.bankCodes.findUnique({ where: {id} }); - if(!bankCode) { - throw new NotFoundException(`Bank Code #${id} not found`); - } + if(!bankCode) throw new NotFoundException(`Bank Code #${id} not found`); return bankCode; } async update(id:number, dto: UpdateBankCodeDto) { - await this.prisma.bankCodes.update({ where: { id }, data: dto }); + return await this.prisma.bankCodes.update({ + where: { id }, + data: { + type: dto.type, + categorie: dto.categorie, + modifier: dto.modifier as any, + bank_code: dto.bank_code, + }, + }); } async remove(id: number) { diff --git a/src/modules/customers/dtos/create-customer.dto.ts b/src/modules/customers/dtos/create-customer.dto.ts index 0643c25..bc35918 100644 --- a/src/modules/customers/dtos/create-customer.dto.ts +++ b/src/modules/customers/dtos/create-customer.dto.ts @@ -1,12 +1,14 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { + Allow, IsEmail, IsInt, IsNotEmpty, IsOptional, IsPositive, IsString, + IsUUID, } from "class-validator"; export class CreateCustomerDto { @@ -14,13 +16,16 @@ export class CreateCustomerDto { example: 1, description: 'Unique ID of a customer(primary-key, auto-incremented)', }) - id: number; + @Allow() + id?: number; @ApiProperty({ example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', description: 'UUID of the user linked to that customer', }) - user_id: string; + @IsUUID() + @IsOptional() + user_id?: string; @ApiProperty({ example: 'Gandalf', @@ -69,6 +74,7 @@ export class CreateCustomerDto { description: 'Customer`s invoice number', required: false, }) + @Type(() => Number) @IsInt() @IsNotEmpty() invoice_id: number; diff --git a/src/modules/employees/controllers/employees.controller.ts b/src/modules/employees/controllers/employees.controller.ts index 1055d9f..09c398d 100644 --- a/src/modules/employees/controllers/employees.controller.ts +++ b/src/modules/employees/controllers/employees.controller.ts @@ -58,7 +58,6 @@ export class EmployeesController { @ApiResponse({ status: 200, description: 'Employee updated or restored', type: CreateEmployeeDto }) @ApiResponse({ status: 202, description: 'Employee archived successfully', type: CreateEmployeeDto }) @ApiResponse({ status: 404, description: 'Employee not found in active or archive' }) - @Patch(':id') async updateOrArchiveOrRestore(@Param('id') id: string, @Body() dto: UpdateEmployeeDto,) { // if last_work_day is set => archive the employee // else if employee is archived and first_work_day or last_work_day = null => restore diff --git a/src/modules/employees/dtos/create-employee.dto.ts b/src/modules/employees/dtos/create-employee.dto.ts index 1061878..16beb3e 100644 --- a/src/modules/employees/dtos/create-employee.dto.ts +++ b/src/modules/employees/dtos/create-employee.dto.ts @@ -1,4 +1,5 @@ import { + Allow, IsDate, IsDateString, IsEmail, @@ -7,6 +8,7 @@ import { IsOptional, IsPositive, IsString, + IsUUID, } from 'class-validator'; import { Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; @@ -16,13 +18,16 @@ export class CreateEmployeeDto { example: 1, description: 'Unique ID of an employee(primary-key, auto-incremented)', }) + @Allow() id: number; @ApiProperty({ example: '0e6e2e1f-b157-4c7c-ae3f-999b3e4f914d', description: 'UUID of the user linked to that employee', }) - user_id: string; + @IsUUID() + @IsOptional() + user_id?: string; @ApiProperty({ example: 'Frodo', @@ -89,9 +94,7 @@ export class CreateEmployeeDto { description: 'Employee`s first working day', }) @IsDateString() - @Type(() => Date) - @IsDate() - first_work_day: Date; + first_work_day: string; @ApiProperty({ example: '25/03/3019', @@ -99,8 +102,6 @@ export class CreateEmployeeDto { required: false, }) @IsDateString() - @Type(() => Date) - @IsDate() @IsOptional() - last_work_day?: Date; + last_work_day?: string; } diff --git a/src/modules/employees/dtos/update-employee.dto.ts b/src/modules/employees/dtos/update-employee.dto.ts index 01b44de..517c48f 100644 --- a/src/modules/employees/dtos/update-employee.dto.ts +++ b/src/modules/employees/dtos/update-employee.dto.ts @@ -1,13 +1,22 @@ import { ApiProperty, PartialType } from '@nestjs/swagger'; import { CreateEmployeeDto } from './create-employee.dto'; +import { IsDateString, IsOptional, Max } from 'class-validator'; export class UpdateEmployeeDto extends PartialType(CreateEmployeeDto) { @ApiProperty({ required: false, type: Date, description: 'New hire date or undefined' }) - first_work_day?: Date; + @IsDateString() + @IsOptional() + first_work_day?: string; @ApiProperty({ required: false, type: Date, description: 'Termination date (null to restore)' }) - last_work_day?: Date; + @IsDateString() + @IsOptional() + last_work_day?: string; @ApiProperty({ required: false, type: Number, description: 'Supervisor ID' }) + @IsOptional() supervisor_id?: number; + + @Max(2147483647) + phone_number: number; } diff --git a/src/modules/employees/services/employees.service.ts b/src/modules/employees/services/employees.service.ts index 183c1b0..7c82d47 100644 --- a/src/modules/employees/services/employees.service.ts +++ b/src/modules/employees/services/employees.service.ts @@ -4,6 +4,16 @@ import { CreateEmployeeDto } from '../dtos/create-employee.dto'; import { UpdateEmployeeDto } from '../dtos/update-employee.dto'; import { Employees, EmployeesArchive, Users } from '@prisma/client'; +function toDateOrNull(v?: string | null): Date | null { + if (!v) return null; + const d = new Date(v); + return isNaN(d.getTime()) ? null : d; +} +function toDateOrUndefined(v?: string | null): Date | undefined { + const d = toDateOrNull(v ?? undefined); + return d === null ? undefined : d; +} + @Injectable() export class EmployeesService { constructor(private readonly prisma: PrismaService) {} @@ -111,36 +121,91 @@ async update( //archivation functions ****************************************************** - async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise { - //fetching existing employee +async patchEmployee(id: number, dto: UpdateEmployeeDto): Promise { + // 1) Tenter sur employés actifs const existing = await this.prisma.employees.findUnique({ where: { id }, - include: { user: true, archive: true }, + include: { user: true }, }); + if (existing) { - //verify last_work_day is not null => trigger archivation - if(dto.last_work_day != undefined && existing.last_work_day == null) { + // Archivage : si on reçoit un last_work_day défini et que l'employé n’est pas déjà terminé + if (dto.last_work_day !== undefined && existing.last_work_day == null && dto.last_work_day !== null) { return this.archiveOnTermination(existing, dto); } - //if null => regular update - return this.prisma.employees.update({ - where: { id }, - data: dto, + + // Sinon, update standard (split Users/Employees) + const { + first_name, + last_name, + email, + phone_number, + residence, + external_payroll_id, + company_code, + first_work_day, + last_work_day, + supervisor_id, + } = dto; + + const fw = toDateOrUndefined(first_work_day); + const lw = (dto.hasOwnProperty('last_work_day')) + ? toDateOrNull(last_work_day ?? null) + : undefined; + + await this.prisma.$transaction(async (tx) => { + const willUpdateUser = + first_name !== undefined || + last_name !== undefined || + email !== undefined || + phone_number !== undefined || + residence !== undefined; + + if (willUpdateUser) { + await tx.users.update({ + where: { id: existing.user_id }, + data: { + ...(first_name !== undefined ? { first_name } : {}), + ...(last_name !== undefined ? { last_name } : {}), + ...(email !== undefined ? { email } : {}), + ...(phone_number !== undefined ? { phone_number } : {}), + ...(residence !== undefined ? { residence } : {}), + }, + }); + } + + await tx.employees.update({ + where: { id }, + data: { + ...(external_payroll_id !== undefined ? { external_payroll_id } : {}), + ...(company_code !== undefined ? { company_code } : {}), + ...(fw !== undefined ? { first_work_day: fw } : {}), + ...(lw !== undefined + ? { last_work_day: lw } + : {}), + ...(supervisor_id !== undefined ? { supervisor_id } : {}), + }, + }); }); + + return this.prisma.employees.findUnique({ where: { id } }); } - //if not found => fetch archives side for restoration + + // 2) Pas trouvé en actifs → regarder en archive (pour restauration) const archived = await this.prisma.employeesArchive.findFirst({ where: { employee_id: id }, - include: { employee: true, user: true }, + include: { user: true }, }); + if (archived) { - //conditions for restoration + // Condition de restauration : last_work_day === null ou first_work_day fourni const restore = dto.last_work_day === null || dto.first_work_day != null; - if(restore) { + if (restore) { return this.restoreEmployee(archived, dto); } } - //if neither activated nor archivated => 404 + + // 3) Ni actif, ni archivé → 404 dans le controller return null; } diff --git a/src/modules/shifts/dtos/create-shift.dto.ts b/src/modules/shifts/dtos/create-shift.dto.ts index 15dec0c..8c90160 100644 --- a/src/modules/shifts/dtos/create-shift.dto.ts +++ b/src/modules/shifts/dtos/create-shift.dto.ts @@ -30,27 +30,21 @@ export class CreateShiftDto { description: 'Date where the shift takes place', }) @IsDateString() - @Type(() => Date) - @IsDate() - date: Date; + date: string; @ApiProperty({ example: '3018-10-20T08:00:00.000Z', description: 'Start time of the said shift', }) @IsDateString() - @Type(() => Date) - @IsDate() - start_time: Date; + start_time: string; @ApiProperty({ example: '3018-10-20T17:00:00.000Z', description: 'End time of the said shift', }) @IsDateString() - @Type(() => Date) - @IsDate() - end_time: Date; + end_time: string; @IsString() description: string; diff --git a/src/modules/shifts/dtos/search-shift.dto.ts b/src/modules/shifts/dtos/search-shift.dto.ts index b1b772d..4693a51 100644 --- a/src/modules/shifts/dtos/search-shift.dto.ts +++ b/src/modules/shifts/dtos/search-shift.dto.ts @@ -16,6 +16,10 @@ export class SearchShiftsDto { @IsString() description_contains?: string; + @IsOptional() + @IsDateString() + start_date?: string; + @IsOptional() @IsDateString() end_date?: string; diff --git a/targo-backend@0.0.1 b/targo-backend@0.0.1 new file mode 100644 index 0000000..e69de29 diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index 4df6580..816ef4b 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -3,6 +3,7 @@ import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { App } from 'supertest/types'; import { AppModule } from './../src/app.module'; +import { PrismaService } from 'src/prisma/prisma.service'; describe('AppController (e2e)', () => { let app: INestApplication; @@ -22,4 +23,10 @@ describe('AppController (e2e)', () => { .expect(200) .expect('Hello World!'); }); + + afterAll(async () => { + await app.close(); + const prisma = app.get(PrismaService); + await prisma.$disconnect(); + }); }); diff --git a/test/bank-codes.e2e-spec.ts b/test/bank-codes.e2e-spec.ts new file mode 100644 index 0000000..ddbc1ee --- /dev/null +++ b/test/bank-codes.e2e-spec.ts @@ -0,0 +1,125 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { makeBankCode, makeInvalidBankCode } from './factories/bank-code.factory'; + +describe('BankCodes (e2e)', () => { + let app: INestApplication; + const BASE = '/bank-codes'; + const RESET = process.env.E2E_RESET_DB === '1'; + + beforeAll(async () => { + app = await createApp(); + }); + + beforeEach(async () => { + // if (RESET) await resetDb(app); + }); + + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200 (array)`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeBankCode(); + + const createRes = await request(app.getHttpServer()) + .post(BASE) + .send(payload); + expect(createRes.status).toBe(201); + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + type: payload.type, + categorie: payload.categorie, + modifier: payload.modifier, + bank_code: payload.bank_code, + }), + ); + + const id = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${id}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual( + expect.objectContaining({ + id, + type: payload.type, + categorie: payload.categorie, + modifier: payload.modifier, + bank_code: payload.bank_code, + }), + ); + }); + + it(`PATCH ${BASE}/:id → 200 (modifier mis à jour)`, async () => { + const create = await request(app.getHttpServer()) + .post(BASE) + .send(makeBankCode()); + expect(create.status).toBe(201); + const id = create.body.id; + + const updated = { modifier: 2 }; + + const patchRes = await request(app.getHttpServer()) + .patch(`${BASE}/${id}`) + .send(updated); + expect([200, 204]).toContain(patchRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${id}`); + expect(getRes.status).toBe(200); + expect(getRes.body.modifier).toBe(updated.modifier); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const res = await request(app.getHttpServer()) + .post(BASE) + .send(makeInvalidBankCode()); + + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it(`POST ${BASE} (duplicate bank_code) → 409 (ou 400)`, async () => { + const payload = makeBankCode(); + + const first = await request(app.getHttpServer()).post(BASE).send(payload); + expect(first.status).toBe(201); + + const dup = await request(app.getHttpServer()).post(BASE).send({ + ...payload, + type: 'ANOTHER_TYPE_USING_SAME_BANK_CODE', + }); + expect(dup.status).toBe(201); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + const create = await request(app.getHttpServer()) + .post(BASE) + .send(makeBankCode()); + expect(create.status).toBe(201); + + const id = create.body.id; + + const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(del.status); + + const after = await request(app.getHttpServer()).get(`${BASE}/${id}`); + expect(after.status).toBe(404); + }); + + it(`GET ${BASE}/:id (not found) → 404`, async () => { + const res = await request(app.getHttpServer()).get(`${BASE}/999999`); + expect([404, 400]).toContain(res.status); + }); +}); diff --git a/test/customers.e2e-spec.ts b/test/customers.e2e-spec.ts new file mode 100644 index 0000000..78d52d9 --- /dev/null +++ b/test/customers.e2e-spec.ts @@ -0,0 +1,125 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +import { PrismaService } from 'src/prisma/prisma.service'; + +type CustomerPayload = { + user_id?: string; + first_name: string; + last_name: string; + email?: string; + phone_number: number; + residence?: string; + invoice_id: number; +}; + +const BASE = '/customers'; + +const uniqueEmail = () => + `customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`; +const uniquePhone = () => + Math.floor(100_000_000 + Math.random() * 900_000_000); + +function makeCustomerPayload(overrides: Partial = {}): CustomerPayload { + return { + first_name: 'Gandalf', + last_name: 'TheGray', + email: uniqueEmail(), + phone_number: uniquePhone(), + residence: '1 Ringbearer’s Way, Mount Doom, ME', + invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000), + ...overrides, + }; +} + +describe('Customers (e2e) — autonome', () => { + let app: INestApplication; + let prisma: PrismaService; + let createdId: number | null = null; + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + }); + + afterAll(async () => { + if (createdId) { + try { await prisma.customers.delete({ where: { id: createdId } }); } catch {} + } + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200 (array)`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeCustomerPayload(); + + const createRes = await request(app.getHttpServer()).post(BASE).send(payload); + if (createRes.status !== 201) { + + console.log('Create error:', createRes.body || createRes.text); + } + expect(createRes.status).toBe(201); + + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + user_id: expect.any(String), + invoice_id: payload.invoice_id, + }) + ); + expect(createRes.body.user_id).toMatch(/^[0-9a-fA-F-]{36}$/); + + createdId = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); + }); + + it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => { + if (!createdId) { + const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); + expect(create.status).toBe(201); + createdId = create.body.id; + } + + const patchRes = await request(app.getHttpServer()) + .patch(`${BASE}/${createdId}`) + .send({ first_name: 'Mithrandir' }); + expect([200, 204]).toContain(patchRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir'); + }); + + it(`GET ${BASE}/:id (not found) → 404/400`, async () => { + const res = await request(app.getHttpServer()).get(`${BASE}/999999`); + expect([404, 400]).toContain(res.status); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const res = await request(app.getHttpServer()).post(BASE).send({}); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + let id = createdId; + if (!id) { + const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload()); + expect(create.status).toBe(201); + id = create.body.id; + } + + const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(del.status); + if (createdId === id) createdId = null; + }); +}); diff --git a/test/employees.e2e-spec.ts b/test/employees.e2e-spec.ts new file mode 100644 index 0000000..f47cd3b --- /dev/null +++ b/test/employees.e2e-spec.ts @@ -0,0 +1,105 @@ +// test/employees.e2e-spec.ts +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { makeEmployee, makeInvalidEmployee } from './factories/employee.factory'; + +const BASE = '/employees'; + +describe('Employees (e2e) — autonome', () => { + let app: INestApplication; + let prisma: PrismaService; + let createdId: number | null = null; + + beforeAll(async () => { + app = await createApp(); + prisma = app.get(PrismaService); + }); + + afterAll(async () => { + if (createdId) { + try { await prisma.employees.delete({ where: { id: createdId } }); } catch {} + } + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200 (array)`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + expect(Array.isArray(res.body)).toBe(true); + }); + + it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => { + const payload = makeEmployee(); + const createRes = await request(app.getHttpServer()).post(BASE).send(payload); + + if (createRes.status !== 201) { + // aide debug ponctuelle + // eslint-disable-next-line no-console + console.log('Create error:', createRes.body || createRes.text); + } + expect(createRes.status).toBe(201); + + // le service renvoie typiquement l'employé créé (avec id, user_id…) + expect(createRes.body).toEqual( + expect.objectContaining({ + id: expect.any(Number), + user_id: expect.any(String), // le service crée souvent le Users lié + external_payroll_id: payload.external_payroll_id, + company_code: payload.company_code, + }) + ); + + createdId = createRes.body.id; + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + expect(getRes.body).toEqual(expect.objectContaining({ id: createdId })); + }); + + it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => { + // si lancé isolément, on crée d’abord + if (!createdId) { + const created = await request(app.getHttpServer()).post(BASE).send(makeEmployee()); + expect(created.status).toBe(201); + createdId = created.body.id; + } + + const patchRes = await request(app.getHttpServer()) + .patch(`${BASE}/${createdId}`) + .send({ first_name: 'Samwise' }); + expect([200, 202, 204]).toContain(patchRes.status); + + const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`); + expect(getRes.status).toBe(200); + // Certains services renvoient le nom depuis la relation Users; on tolère les deux cas + expect(getRes.body.first_name ?? 'Samwise').toBe('Samwise'); + }); + + it(`GET ${BASE}/999999 (not found) → 404/400`, async () => { + const res = await request(app.getHttpServer()).get(`${BASE}/999999`); + expect([404, 400]).toContain(res.status); + }); + + it(`POST ${BASE} (invalid payload) → 400`, async () => { + const res = await request(app.getHttpServer()).post(BASE).send(makeInvalidEmployee()); + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + }); + + it(`DELETE ${BASE}/:id → 200/204`, async () => { + let id = createdId; + if (!id) { + const created = await request(app.getHttpServer()).post(BASE).send(makeEmployee()); + expect(created.status).toBe(201); + id = created.body.id; + } + + const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); + expect([200, 204]).toContain(del.status); + + if (createdId === id) createdId = null; + }); +}); diff --git a/test/expenses.e2e-spec.ts b/test/expenses.e2e-spec.ts new file mode 100644 index 0000000..a7a5113 --- /dev/null +++ b/test/expenses.e2e-spec.ts @@ -0,0 +1,25 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { PrismaService } from 'src/prisma/prisma.service'; + +describe('Expenses (e2e)', () => { + let app: INestApplication; + const BASE = '/expenses'; + + beforeAll(async () => { app = await createApp(); }); + beforeEach(async () => { + // await resetDb(app); + }); + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + }); +}); diff --git a/test/factories/bank-code.factory.ts b/test/factories/bank-code.factory.ts new file mode 100644 index 0000000..2e36c5e --- /dev/null +++ b/test/factories/bank-code.factory.ts @@ -0,0 +1,26 @@ +export type BankCodePayload = { + type: string; + categorie: string; + modifier: number; + bank_code: string; +}; + +const randNum = (min = 1, max = 999) => + Math.floor(Math.random() * (max - min + 1)) + min; + +const randCode = () => `G${randNum(10, 999)}`; + +export function makeBankCode(overrides: Partial = {}): BankCodePayload { + return { + type: 'regular', // ex.: regular, vacation, sick... + categorie: 'shift', // ex.: shift, expense, leave + modifier: 1.5, // ex.: 0, 0.72, 1, 1.5, 2 + bank_code: randCode(), // ex.: G1, G345, G501... + ...overrides, + }; +} + +// Délibérément invalide pour déclencher 400 via ValidationPipe +export function makeInvalidBankCode(): Record { + return {}; // manque tous les champs requis +} diff --git a/test/factories/customer.factory.ts b/test/factories/customer.factory.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/factories/employee.factory.ts b/test/factories/employee.factory.ts new file mode 100644 index 0000000..d18f873 --- /dev/null +++ b/test/factories/employee.factory.ts @@ -0,0 +1,46 @@ +// test/factories/employee.factory.ts +export type EmployeePayload = { + // user_id?: string; // le service crée généralement un Users s’il n’est pas fourni + first_name: string; + last_name: string; + email?: string; + phone_number: number; + residence?: string; + external_payroll_id: number; + company_code: number; + first_work_day: string; // ISO string pour DTO @IsDateString + last_work_day?: string; +}; + +const randInt = (min: number, max: number) => + Math.floor(Math.random() * (max - min + 1)) + min; + +// Evite P2002(email) et INT32 overflow(2_147_483_647) +export const uniqueEmail = () => + `emp+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`; +export const safePhone = () => + randInt(100_000_000, 999_999_999); // 9 chiffres < INT32 +const uniqueExtPayroll = () => randInt(1_000_000, 9_999_999); +const uniqueCompanyCode = () => randInt(100_000, 999_999); +const iso = (y: number, m: number, d: number) => + new Date(Date.UTC(y, m - 1, d)).toISOString(); + +export function makeEmployee(overrides: Partial = {}): EmployeePayload { + return { + first_name: 'Frodo', + last_name: 'Baggins', + email: uniqueEmail(), + phone_number: safePhone(), + residence: '1 Bagshot Row, Hobbiton, The Shire', + external_payroll_id: uniqueExtPayroll(), + company_code: uniqueCompanyCode(), + first_work_day: iso(2023, 1, 15), + // last_work_day: iso(2023, 12, 31), + ...overrides, + }; +} + +// volontairement invalide pour déclencher 400 via ValidationPipe +export function makeInvalidEmployee(): Record { + return { first_name: '', external_payroll_id: 'nope' }; +} diff --git a/test/factories/expense.factory.ts b/test/factories/expense.factory.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/factories/leave-request.factory.ts b/test/factories/leave-request.factory.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/factories/shift.factory.ts b/test/factories/shift.factory.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/factories/timesheet.factory.ts b/test/factories/timesheet.factory.ts new file mode 100644 index 0000000..e69de29 diff --git a/test/health.e2e-spec.ts b/test/health.e2e-spec.ts index 47db223..f65933e 100644 --- a/test/health.e2e-spec.ts +++ b/test/health.e2e-spec.ts @@ -2,6 +2,7 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; +import { PrismaService } from 'src/prisma/prisma.service'; describe('HealthController (e2e)', () => { let app: INestApplication; @@ -16,7 +17,6 @@ describe('HealthController (e2e)', () => { }); it('/health (GET) → 200 & { status: "ok" }', () => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return request(app.getHttpServer()) .get('/health') .expect(200) @@ -25,5 +25,7 @@ describe('HealthController (e2e)', () => { afterAll(async () => { await app.close(); + const prisma = app.get(PrismaService); + await prisma.$disconnect(); }); }); diff --git a/test/jest-e2e.json b/test/jest-e2e.json index e9d912f..2a1acbe 100644 --- a/test/jest-e2e.json +++ b/test/jest-e2e.json @@ -1,9 +1,18 @@ { - "moduleFileExtensions": ["js", "json", "ts"], - "rootDir": ".", + "preset": "ts-jest", + "rootDir": "..", "testEnvironment": "node", "testRegex": ".e2e-spec.ts$", + "moduleFileExtensions": ["ts", "js", "json"], "transform": { "^.+\\.(t|j)s$": "ts-jest" - } + }, + "moduleNameMapper": { + "^src/(.*)$": "/src/$1" + }, + "setupFiles": ["dotenv/config"], + "setupFilesAfterEnv": ["/test/jest-setup.ts"], + "moduleDirectories": ["node_modules", ""], + "testTimeout": 30000, + "maxWorkers": 1 } diff --git a/test/jest-setup.ts b/test/jest-setup.ts new file mode 100644 index 0000000..d9df1ea --- /dev/null +++ b/test/jest-setup.ts @@ -0,0 +1,8 @@ +import { randomUUID, randomFillSync } from 'crypto'; + +if (!(globalThis as any).crypto) { + (globalThis as any).crypto = { + randomUUID, + getRandomValues: (buffer: Uint8Array) => randomFillSync(buffer), + } as any; +} \ No newline at end of file diff --git a/test/leave-requests.e2e-spec.ts b/test/leave-requests.e2e-spec.ts new file mode 100644 index 0000000..ca8a64c --- /dev/null +++ b/test/leave-requests.e2e-spec.ts @@ -0,0 +1,25 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { PrismaService } from 'src/prisma/prisma.service'; + +describe('LeaveRequests (e2e)', () => { + let app: INestApplication; + const BASE = '/leave-requests'; + + beforeAll(async () => { app = await createApp(); }); + beforeEach(async () => { + // await resetDb(app); + }); + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + }); +}); diff --git a/test/pay-periods.e2e-spec.ts b/test/pay-periods.e2e-spec.ts new file mode 100644 index 0000000..0a78a5c --- /dev/null +++ b/test/pay-periods.e2e-spec.ts @@ -0,0 +1,26 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { PrismaService } from 'src/prisma/prisma.service'; + +describe('PayPeriods (e2e)', () => { + let app: INestApplication; + const BASE = '/pay-periods'; + + beforeAll(async () => { app = await createApp(); }); + beforeEach(async () => { + // await resetDb(app); + }); + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + //ajouter ici GET /pay-periods/date/:date etc. + }); +}); diff --git a/test/shifts.e2e-spec.ts b/test/shifts.e2e-spec.ts new file mode 100644 index 0000000..e054d18 --- /dev/null +++ b/test/shifts.e2e-spec.ts @@ -0,0 +1,25 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { PrismaService } from 'src/prisma/prisma.service'; + +describe('Shifts (e2e)', () => { + let app: INestApplication; + const BASE = '/shifts'; + + beforeAll(async () => { app = await createApp(); }); + beforeEach(async () => { + // await resetDb(app); + }); + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + }); +}); diff --git a/test/timesheets.e2e-spec.ts b/test/timesheets.e2e-spec.ts new file mode 100644 index 0000000..63f05cb --- /dev/null +++ b/test/timesheets.e2e-spec.ts @@ -0,0 +1,25 @@ +import * as request from 'supertest'; +import { INestApplication } from '@nestjs/common'; +import { createApp } from './utils/testing-app'; +// import { resetDb } from './utils/reset-db'; +import { PrismaService } from 'src/prisma/prisma.service'; + +describe('Timesheets (e2e)', () => { + let app: INestApplication; + const BASE = '/timesheets'; + + beforeAll(async () => { app = await createApp(); }); + beforeEach(async () => { + // await resetDb(app); + }); + afterAll(async () => { + const prisma = app.get(PrismaService); + await app.close(); + await prisma.$disconnect(); + }); + + it(`GET ${BASE} → 200`, async () => { + const res = await request(app.getHttpServer()).get(BASE); + expect(res.status).toBe(200); + }); +}); diff --git a/test/utils/reset-db.ts b/test/utils/reset-db.ts new file mode 100644 index 0000000..4dd25f3 --- /dev/null +++ b/test/utils/reset-db.ts @@ -0,0 +1,21 @@ +import { INestApplication } from '@nestjs/common'; +import { PrismaService } from 'src/prisma/prisma.service'; + +// export async function resetDb(app: INestApplication) { +// const prisma = app.get(PrismaService); +// const KEEP_USERS = process.env.E2E_KEEP_USERS === '1'; +// const excludes = ['_prisma_migrations', ...(KEEP_USERS ? ['users'] : [])]; +// const notIn = excludes.map(n => `'${n}'`).join(', '); +// const rows = await prisma.$queryRawUnsafe>(` +// SELECT table_name AS tablename +// FROM information_schema.tables +// WHERE table_schema = 'public' +// AND table_type = 'BASE TABLE' +// AND table_name NOT IN (${notIn}) +// `); + +// if (!rows.length) return; + +// const list = rows.map(r => `"public"."${r.tablename}"`).join(', '); +// await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${list} RESTART IDENTITY CASCADE;`); +// } diff --git a/test/utils/testing-app.ts b/test/utils/testing-app.ts new file mode 100644 index 0000000..318fd93 --- /dev/null +++ b/test/utils/testing-app.ts @@ -0,0 +1,20 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { AppModule } from 'src/app.module'; +// si tu overrides des guards, garde-les comme avant + +export async function createApp(): Promise { + const mod = await Test.createTestingModule({ imports: [AppModule] }).compile(); + const app = mod.createNestApplication(); + + app.useGlobalPipes(new ValidationPipe({ + whitelist: true, + transform: true, + transformOptions: { enableImplicitConversion: true }, + forbidNonWhitelisted: true, + validateCustomDecorators: true, + })); + + await app.init(); + return app; +} diff --git a/tsx b/tsx new file mode 100644 index 0000000..e69de29