From 5452641f19606e174ead59bd7c5f7bff922f6fcf Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Mon, 24 Nov 2025 14:59:16 -0500 Subject: [PATCH] feat(migration): added a split prismaModule to manage legacy DB, created scripts to fetch old data, transform to match the new structure and creates new items in the new DB --- prisma-legacy/schema.prisma | 197 +++++++++++++ .../migration.sql | 67 +++++ .../migration.sql | 2 + .../migration.sql | 12 + prisma/schema.prisma | 8 +- scripts/done/import-employees-from-csv.ts | 270 ++++++++++++++++++ scripts/done/import-users-from-csv.ts | 106 +++++++ scripts/done/init-preferences.ts | 67 +++++ scripts/migrate-expenses.ts | 164 +++++++++++ scripts/migrate-shifts.ts | 210 ++++++++++++++ scripts/migrate-timesheets.ts | 118 ++++++++ scripts/migration.service.ts | 21 ++ src/app.module.ts | 2 + src/common/utils/date-utils.ts | 2 +- src/main.ts | 10 + src/prisma-legacy/prisma.module.ts | 9 + src/prisma-legacy/prisma.service.ts | 18 ++ .../shifts/services/shifts-create.service.ts | 6 +- .../services/shifts-update-delete.service.ts | 10 +- .../expenses-query.service.ts | 2 +- test/customers.e2e-spec.ts | 208 +++++++------- test/pay-periods-approval.e2e-spec.ts | 248 ++++++++-------- 22 files changed, 1515 insertions(+), 242 deletions(-) create mode 100644 prisma-legacy/schema.prisma create mode 100644 prisma/migrations/20251119155820_init_staging_db/migration.sql create mode 100644 prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql create mode 100644 prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql create mode 100644 scripts/done/import-employees-from-csv.ts create mode 100644 scripts/done/import-users-from-csv.ts create mode 100644 scripts/done/init-preferences.ts create mode 100644 scripts/migrate-expenses.ts create mode 100644 scripts/migrate-shifts.ts create mode 100644 scripts/migrate-timesheets.ts create mode 100644 scripts/migration.service.ts create mode 100644 src/prisma-legacy/prisma.module.ts create mode 100644 src/prisma-legacy/prisma.service.ts diff --git a/prisma-legacy/schema.prisma b/prisma-legacy/schema.prisma new file mode 100644 index 0000000..e12d8be --- /dev/null +++ b/prisma-legacy/schema.prisma @@ -0,0 +1,197 @@ +generator client { + provider = "prisma-client-js" + output = "../node_modules/@prisma/client-legacy" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL_LEGACY") +} + +model codeDesjardins { + id String @id @map("_id") @db.VarChar(50) + code String @db.VarChar(50) + label String @db.VarChar(50) + description String @db.VarChar(250) +} + +model customers { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + address String? @db.VarChar(255) + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar(50) +} + +model dealers { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar(50) +} + +model employee_shift_template { + id String @id @map("_id") @db.Uuid + employee_id String @db.VarChar + day_of_the_week String @db.VarChar + start_time BigInt + end_time BigInt + created_at BigInt + updated_at BigInt +} + +model employees { + id String @id @map("_id") @db.Uuid + user_id String? @db.VarChar(50) + employee_number String? @db.VarChar(50) + email String? @db.VarChar(50) + first_name String? @db.VarChar(50) + last_name String? @db.VarChar(50) + phone_number String? @db.VarChar(50) + job_title String? @db.VarChar(50) + company Int? + supervisor String? @db.VarChar(50) + is_supervisor Boolean? + onboarding BigInt? + offboarding BigInt? + regular_hours_day Float? @db.Real + hours_bank_max Int? + created_at BigInt? + updated_at BigInt? + created_by String? @db.VarChar +} + +model expenses { + id String @id @map("_id") @db.Uuid + time_sheet_id String? @db.VarChar(50) + date String? @db.VarChar(50) + code String? @db.VarChar(50) + value Float? @db.Real + description String? @db.VarChar + evidence_id String? @db.VarChar + status Boolean? + created_at BigInt? + updated_at BigInt? + supervisor_note String? @db.VarChar(255) +} + +model hours_bank { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + hours Float? @db.Real + created_at BigInt? + updated_at BigInt? +} + +model mileage_bank { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + mileage Int? + year Int? +} + +model shifts { + id String @id @map("_id") @db.Uuid + time_sheet_id String? @db.VarChar(50) + code String? @db.VarChar(50) + type String? @db.VarChar(50) + date DateTime? @db.Date + start_time BigInt? + end_time BigInt? + comment String? @db.VarChar(255) + status Boolean? + created_at BigInt? + updated_at BigInt? + supervisor_note String? @db.VarChar(255) +} + +model shifts_of_template { + id String @id @map("_id") @db.Uuid + model_id String @db.Uuid + day_of_the_week String @db.VarChar(50) + start_time BigInt + end_time BigInt + created_at BigInt + updated_at BigInt +} + +model sick_leave { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + accumulated Float? @db.Real + consumed Float? @db.Real + year Int? + created_at BigInt? + updated_at BigInt? +} + +model time_sheet_periods { + id String @id @map("_id") @db.Uuid + start_date DateTime? @db.Date + end_date DateTime? @db.Date + payment_date DateTime? @db.Date + period_number Int? + year Int? +} + +model time_sheet_template { + id String @id @map("_id") @db.Uuid + title String @db.VarChar + description String? @db.VarChar + created_at BigInt + updated_at BigInt +} + +model time_sheets { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar + start_date DateTime? @db.Date + end_date DateTime? @db.Date + status Boolean? + banked_hours Float? @db.Real + consumed_vacation Float? @db.Real + consumed_sick Float? @db.Real + period_id String? @db.VarChar(50) + period_number Int? + created_at BigInt? + updated_at BigInt? + blocked_week Boolean? @default(false) +} + +model users { + id String @id @map("_id") @db.VarChar(50) + email String @unique @db.VarChar(50) + password String @db.VarChar(255) + type String @db.VarChar(50) + role String @db.VarChar(50) + is_verified Boolean? + verification_token String? + otp_token String? + refresh_token String? + created_at BigInt? + updated_at BigInt? + created_by String @db.VarChar(255) + last_login BigInt +} + +model vacation_leave { + id String @id @map("_id") @db.Uuid + employee_id String? @db.VarChar(50) + accumulated Float? @db.Real + consumed Float? @db.Real + created_at BigInt? + updated_at BigInt? + start_year Int? + end_year Int? + max_hours_per_year Float? @db.Real +} diff --git a/prisma/migrations/20251119155820_init_staging_db/migration.sql b/prisma/migrations/20251119155820_init_staging_db/migration.sql new file mode 100644 index 0000000..d5ef8f6 --- /dev/null +++ b/prisma/migrations/20251119155820_init_staging_db/migration.sql @@ -0,0 +1,67 @@ +/* + Warnings: + + - You are about to drop the column `patch` on the `attachment_variants` table. All the data in the column will be lost. + - You are about to alter the column `amount` on the `expenses` table. The data in that column could be lost. The data in that column will be cast from `Money` to `Decimal(12,2)`. + - You are about to alter the column `amount` on the `expenses_archive` table. The data in that column could be lost. The data in that column will be cast from `Money` to `Decimal(12,2)`. + - You are about to drop the column `date` on the `leave_requests` table. All the data in the column will be lost. + - You are about to drop the `customers` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `customers_archive` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `employees_archive` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[employee_id,leave_type,dates]` on the table `leave_requests` will be added. If there are existing duplicate values, this will fail. + - Added the required column `path` to the `attachment_variants` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "public"."customers" DROP CONSTRAINT "customers_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."customers_archive" DROP CONSTRAINT "customers_archive_customer_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."customers_archive" DROP CONSTRAINT "customers_archive_user_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_employee_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_supervisor_id_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."employees_archive" DROP CONSTRAINT "employees_archive_user_id_fkey"; + +-- DropIndex +DROP INDEX "public"."leave_requests_employee_id_date_idx"; + +-- DropIndex +DROP INDEX "public"."leave_requests_employee_id_leave_type_date_key"; + +-- AlterTable +ALTER TABLE "attachment_variants" DROP COLUMN "patch", +ADD COLUMN "path" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "expenses" ALTER COLUMN "amount" DROP NOT NULL, +ALTER COLUMN "amount" SET DATA TYPE DECIMAL(12,2); + +-- AlterTable +ALTER TABLE "expenses_archive" ALTER COLUMN "amount" SET DATA TYPE DECIMAL(12,2); + +-- AlterTable +ALTER TABLE "leave_requests" DROP COLUMN "date", +ADD COLUMN "dates" DATE[]; + +-- DropTable +DROP TABLE "public"."customers"; + +-- DropTable +DROP TABLE "public"."customers_archive"; + +-- DropTable +DROP TABLE "public"."employees_archive"; + +-- CreateIndex +CREATE INDEX "leave_requests_employee_id_dates_idx" ON "leave_requests"("employee_id", "dates"); + +-- CreateIndex +CREATE UNIQUE INDEX "leave_requests_employee_id_leave_type_dates_key" ON "leave_requests"("employee_id", "leave_type", "dates"); diff --git a/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql b/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql new file mode 100644 index 0000000..d4d9805 --- /dev/null +++ b/prisma/migrations/20251119185059_preparation_to_get_the_staging_db_ready/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "public"."users_phone_number_key"; diff --git a/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql b/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql new file mode 100644 index 0000000..bcb1752 --- /dev/null +++ b/prisma/migrations/20251121194224_unique_keys_added_to_shifts_and_expenses_for_migration_from_old_db/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[timesheet_id,date,amount,mileage]` on the table `expenses` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[timesheet_id,date,start_time]` on the table `shifts` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "expenses_timesheet_id_date_amount_mileage_key" ON "expenses"("timesheet_id", "date", "amount", "mileage"); + +-- CreateIndex +CREATE UNIQUE INDEX "shifts_timesheet_id_date_start_time_key" ON "shifts"("timesheet_id", "date", "start_time"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b2171ad..d0d2c87 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,7 +11,7 @@ generator client { datasource db { provider = "postgresql" - url = env("DATABASE_URL_DEV") + url = env("DATABASE_URL_STAGING") } model Users { @@ -19,7 +19,7 @@ model Users { first_name String last_name String email String @unique - phone_number String @unique + phone_number String residence String? role Roles @default(GUEST) @@ -180,7 +180,7 @@ model Shifts { comment String? archive ShiftsArchive[] @relation("ShiftsToArchive") - + @@unique([timesheet_id, date, start_time], name: "unique_ts_id_date_start_time") @@map("shifts") } @@ -232,7 +232,7 @@ model Expenses { is_approved Boolean @default(false) archive ExpensesArchive[] @relation("ExpensesToArchive") - + @@unique([timesheet_id, date, amount, mileage], name: "unique_ts_id_date_amount_mileage") @@map("expenses") } diff --git a/scripts/done/import-employees-from-csv.ts b/scripts/done/import-employees-from-csv.ts new file mode 100644 index 0000000..d36cf8a --- /dev/null +++ b/scripts/done/import-employees-from-csv.ts @@ -0,0 +1,270 @@ +// src/scripts/import-employees-from-csv.ts +import { PrismaClient, Roles } from '@prisma/client'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); + +// ⚙️ Chemin vers ton CSV employees +const CSV_PATH = path.resolve(__dirname, '../../data/export_new_employee_table.csv'); + +// Rôles éligibles pour la table Employees +const ELIGIBLE_ROLES: Roles[] = [ + Roles.EMPLOYEE, + Roles.SUPERVISOR, + Roles.HR, + Roles.ACCOUNTING, + Roles.ADMIN, +]; + +// Type correspondant EXACT aux colonnes de ton CSV +type EmployeeCsvRow = { + employee_number: string; + email: string; + job_title: string; + company: string; // sera converti en number + is_supervisor: string; // "True"/"False" (ou variantes) + onboarding: string; // millis + offboarding: string; // millis ou "NULL" +}; + +// Représentation minimale d'un user +type UserSummary = { + id: string; // UUID + email: string; + role: Roles; +}; + +// ============ Helpers CSV ============ + +function splitCsvLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + // guillemet échappé "" + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + + result.push(current); + return result.map((v) => v.trim()); +} + +// ============ Helpers de parsing ============ + +function parseBoolean(value: string): boolean { + const v = value.trim().toLowerCase(); + return v === 'true' || v === '1' || v === 'yes' || v === 'y' || v === 'oui'; +} + +function parseIntSafe(value: string, fieldName: string): number | null { + const trimmed = value.trim(); + if (!trimmed || trimmed.toUpperCase() === 'NULL') return null; + + const n = Number.parseInt(trimmed, 10); + if (Number.isNaN(n)) { + console.warn(`⚠️ Impossible de parser "${value}" en entier pour le champ ${fieldName}`); + return null; + } + return n; +} + +function millisToDate(value: string): Date | null { + const trimmed = value.trim().toUpperCase(); + if (!trimmed || trimmed === 'NULL') return null; + + const ms = Number(trimmed); + if (!Number.isFinite(ms)) { + console.warn(`⚠️ Impossible de parser "${value}" en millis pour une Date`); + return null; + } + + const d = new Date(ms); + // On normalise au jour (minuit UTC) + const normalized = new Date(Date.UTC( + d.getUTCFullYear(), + d.getUTCMonth(), + d.getUTCDate(), + )); + + return normalized; +} + +// ============ MAIN ============ + +async function main() { + // 1. Lecture du CSV + const fileContent = fs.readFileSync(CSV_PATH, 'utf-8'); + + const lines = fileContent + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.length > 0); + + if (lines.length <= 1) { + console.error('CSV vide ou seulement un header'); + return; + } + + const header = splitCsvLine(lines[0]); // ["employee_number","email",...] + const dataLines = lines.slice(1); + + const csvRows: EmployeeCsvRow[] = dataLines.map((line) => { + const values = splitCsvLine(line); + const row: any = {}; + + header.forEach((col, idx) => { + row[col] = values[idx] ?? ''; + }); + + return row as EmployeeCsvRow; + }); + + console.log(`➡️ ${csvRows.length} lignes trouvées dans le CSV employees`); + + // 2. Récupérer tous les emails du CSV + const emails = Array.from( + new Set( + csvRows + .map((r) => r.email.trim()) + .filter((e) => e.length > 0), + ), + ); + + console.log(`➡️ ${emails.length} emails uniques trouvés dans le CSV`); + + // 3. Charger les users correspondants avec les bons rôles + const users = (await prisma.users.findMany({ + where: { + email: { in: emails }, + role: { in: ELIGIBLE_ROLES }, + }, + select: { + id: true, + email: true, + role: true, + }, + })) as UserSummary[]; + + console.log(`➡️ ${users.length} users éligibles trouvés dans la DB`); + + // Map email → user + const userByEmail = new Map(); + for (const user of users) { + const key = user.email.trim().toLowerCase(); + userByEmail.set(key, user); + } + + // 4. Construire les données pour employees.createMany + const employeesToCreate: { + user_id: string; + external_payroll_id: number; + company_code: number; + first_work_day: Date; + last_work_day: Date | null; + job_title: string | null; + is_supervisor: boolean; + supervisor_id?: number | null; + }[] = []; + + const rowsWithoutUser: EmployeeCsvRow[] = []; + const rowsWithInvalidNumbers: EmployeeCsvRow[] = []; + + for (const row of csvRows) { + const emailKey = row.email.trim().toLowerCase(); + const user = userByEmail.get(emailKey); + + if (!user) { + rowsWithoutUser.push(row); + continue; + } + + const external_payroll_id = parseIntSafe(row.employee_number, 'external_payroll_id'); + const company_code = parseIntSafe(String(row.company), 'company_code'); + + if (external_payroll_id === null || company_code === null) { + rowsWithInvalidNumbers.push(row); + continue; + } + + const first_work_day = millisToDate(row.onboarding); + const last_work_day = millisToDate(row.offboarding); + const is_supervisor = parseBoolean(row.is_supervisor); + const job_title = row.job_title?.trim() || null; + + if (!first_work_day) { + console.warn( + `⚠️ Date d'onboarding invalide pour ${row.email} (employee_number=${row.employee_number})`, + ); + continue; + } + + employeesToCreate.push({ + user_id: user.id, + external_payroll_id, + company_code, + first_work_day, + last_work_day, + job_title, + is_supervisor, + supervisor_id: null, // on pourra gérer ça plus tard si tu as les infos + }); + } + + console.log(`➡️ ${employeesToCreate.length} entrées Employees prêtes à être insérées`); + + if (rowsWithoutUser.length > 0) { + console.warn(`⚠️ ${rowsWithoutUser.length} lignes CSV sans user correspondant (email / rôle) :`); + for (const row of rowsWithoutUser) { + console.warn( + ` - email=${row.email}, employee_number=${row.employee_number}, company=${row.company}`, + ); + } + } + + if (rowsWithInvalidNumbers.length > 0) { + console.warn(`⚠️ ${rowsWithInvalidNumbers.length} lignes CSV avec ids/compagnies invalides :`); + for (const row of rowsWithInvalidNumbers) { + console.warn( + ` - email=${row.email}, employee_number="${row.employee_number}", company="${row.company}"`, + ); + } + } + + if (employeesToCreate.length === 0) { + console.warn('⚠️ Aucun Employees à créer, arrêt.'); + return; + } + + // 5. Insert en batch + const result = await prisma.employees.createMany({ + data: employeesToCreate, + skipDuplicates: true, // évite les erreurs si tu relances le script + }); + + console.log(`✅ ${result.count} employees insérés dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’import CSV → Employees', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/done/import-users-from-csv.ts b/scripts/done/import-users-from-csv.ts new file mode 100644 index 0000000..2c0dad2 --- /dev/null +++ b/scripts/done/import-users-from-csv.ts @@ -0,0 +1,106 @@ +// src/scripts/import-users-from-csv.ts +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); + +// ⚙️ Chemin vers ton CSV (à adapter selon où tu le mets) +const CSV_PATH = path.resolve(__dirname, '../../data/export_employee_table.csv'); + +// Type aligné sur les colonnes du CSV +type CsvUserRow = { + email: string; + first_name: string; + last_name: string; + phone_number: string; +}; + +// Petit parseur de ligne CSV sans dépendance +function splitCsvLine(line: string): string[] { + const result: string[] = []; + let current = ''; + let inQuotes = false; + + for (let i = 0; i < line.length; i++) { + const char = line[i]; + + if (char === '"') { + // guillemet échappé "" + if (inQuotes && line[i + 1] === '"') { + current += '"'; + i++; // on saute le deuxième + } else { + inQuotes = !inQuotes; + } + } else if (char === ',' && !inQuotes) { + result.push(current); + current = ''; + } else { + current += char; + } + } + + result.push(current); + return result.map((v) => v.trim()); +} + +async function main() { + // 1. Lecture du fichier CSV + const fileContent = fs.readFileSync(CSV_PATH, 'utf-8'); + + const lines = fileContent + .split(/\r?\n/) + .map((l) => l.trim()) + .filter((l) => l.length > 0); + + if (lines.length <= 1) { + console.error('CSV vide ou seulement un header'); + return; + } + + // 2. Header (noms de colonnes) -> ["email", "first_name", "last_name", "phone_number"] + const header = splitCsvLine(lines[0]); + const dataLines = lines.slice(1); + + // 3. Conversion de chaque ligne en objet { email, first_name, last_name, phone_number } + const records: CsvUserRow[] = dataLines.map((line) => { + const values = splitCsvLine(line); + const row: any = {}; + + header.forEach((col, idx) => { + row[col] = values[idx] ?? ''; + }); + + return row as CsvUserRow; + }); + + // 4. Mapping vers le format attendu par Prisma (model Users) + const data = records.map((row) => ({ + email: row.email.trim(), + first_name: row.first_name.trim(), + last_name: row.last_name.trim(), + phone_number: row.phone_number.trim(), + // residence: null, // si tu veux la forcer à null + // role: 'GUEST', // sinon Prisma va appliquer la valeur par défaut + })); + + console.log(`➡️ ${data.length} lignes trouvées dans le CSV`); + console.log('Exemple importé :', data[0]); + + + const result = await prisma.users.createMany({ + data, + }); + + console.log(`✅ ${result.count} utilisateurs insérés dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’import CSV → DB', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/done/init-preferences.ts b/scripts/done/init-preferences.ts new file mode 100644 index 0000000..f520857 --- /dev/null +++ b/scripts/done/init-preferences.ts @@ -0,0 +1,67 @@ +// src/scripts/init-preferences.ts +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +type UserSummary = { + id: string; // UUID + email: string; +}; + +async function main() { + console.log('➡️ Initialisation des préférences utilisateurs…'); + + // 1. Récupérer tous les users + const users = (await prisma.users.findMany({ + select: { + id: true, + email: true, + }, + })) as UserSummary[]; + + console.log(`➡️ ${users.length} users trouvés dans la DB`); + + // 2. Récupérer toutes les préférences existantes + const existingPrefs = await prisma.preferences.findMany({ + select: { + user_id: true, + }, + }); + + const userIdsWithPrefs = new Set(existingPrefs.map((p) => p.user_id)); + + console.log(`➡️ ${existingPrefs.length} users ont déjà des préférences`); + + // 3. Filtrer les users qui n'ont pas encore de preferences + const usersWithoutPrefs = users.filter((u) => !userIdsWithPrefs.has(u.id)); + + console.log(`➡️ ${usersWithoutPrefs.length} users n'ont pas encore de préférences`); + + if (usersWithoutPrefs.length === 0) { + console.log('✅ Rien à faire, toutes les préférences sont déjà créées.'); + return; + } + + // 4. Préparer les entrées pour createMany + const prefsToCreate = usersWithoutPrefs.map((u) => ({ + user_id: u.id, + // tous les autres champs prendront leurs valeurs par défaut (0) + })); + + // 5. Insertion en batch + const result = await prisma.preferences.createMany({ + data: prefsToCreate, + skipDuplicates: true, // sécurité si jamais le script est relancé + }); + + console.log(`✅ ${result.count} préférences créées dans la DB`); +} + +main() + .catch((err) => { + console.error('❌ Erreur pendant l’initialisation des préférences', err); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/scripts/migrate-expenses.ts b/scripts/migrate-expenses.ts new file mode 100644 index 0000000..6c20eb6 --- /dev/null +++ b/scripts/migrate-expenses.ts @@ -0,0 +1,164 @@ +import { PrismaClient as Prisma } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" +import { toDateFromString, toHHmmFromDate, toStringFromDate } from "src/common/utils/date-utils"; + +const prisma_legacy = new PrismaLegacy({}); +const prisma = new Prisma({}); + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldExpense = { + time_sheet_id: string | null; + date: string | null; + code: string | null; + description: string | null; + value: number | null; + status: boolean | null; +} + +export const extractOldExpenses = async () => { + for (let id = 1; id <= 61; id++) { + + console.log(`Start of Expense migration ***************************************************************`); + + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB`); + + const new_timesheets = await findManyNewTimesheets(new_employee.id); + console.log(`New Timesheets found for employee ${id}`); + + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(`Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + console.log('Start of Expense creation*****************************************************************'); + for (const old_timesheet of old_timesheets) { + if (!old_timesheet.start_date) continue; + const new_timesheet = new_timesheets.find((ts) => ts.start_date.getTime() === old_timesheet.start_date!.getTime()); + if (!new_timesheet) { + console.warn(`No new timesheet matching legacy timesheet ${old_timesheet.id}`); + continue; + } + const old_expenses = await prisma_legacy.expenses.findMany({ + where: { time_sheet_id: old_timesheet.id }, + select: { + time_sheet_id: true, + date: true, + code: true, + description: true, + value: true, + status: true, + + }, + }); + await createManyNewExpenses(new_timesheet.id, old_expenses); + } + + } + await prisma_legacy.$disconnect(); + await prisma.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: new_employee.external_payroll_id.toString(), + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + return old_timesheets; +} + +const findManyNewTimesheets = async (employee_id: number) => { + const timesheets = await prisma.timesheets.findMany({ + where: { employee_id: employee_id }, + select: { id: true, start_date: true } + }) + return timesheets; +} + +const createManyNewExpenses = async (timesheet_id: number, old_expenses: OldExpense[]) => { + for (const old_expense of old_expenses) { + let mileage: number = 0; + let amount: number = old_expense.value ?? 0; + if (old_expense.code === 'G503') { + mileage = old_expense.value!; + amount = mileage * 0.72; + } + if (mileage < 0) { + console.warn(`expense of value less than '0' found`) + } + + if (old_expense.date == null) { + console.warn(`Expense date invalid ${old_expense.date}`); + continue; + } + const date = toDateFromString(old_expense.date); + + if (old_expense.status == null) { + console.warn(`status null for legacy expense ${old_expense}`); + continue; + } + + if (old_expense.code == null) { + console.warn(`Code null for legacy expense ${old_expense.code}`); + continue; + } + const bank_code_id = await findBankCodeIdUsingOldCode(old_expense.code); + + await prisma.expenses.create({ + // where: { unique_ts_id_date_amount_mileage: { timesheet_id: timesheet_id, date, amount, mileage } }, + // update: { + // is_approved: old_expense.status, + // }, + data: { + date: date, + comment: old_expense.description ?? '', + timesheet_id: timesheet_id, + bank_code_id: bank_code_id, + amount: amount, + mileage: mileage, + } + }); + } +} + +const findBankCodeIdUsingOldCode = async (code: string): Promise => { + const bank_code = await prisma.bankCodes.findFirst({ + where: { bank_code: code }, + select: { id: true }, + }); + if (!bank_code) throw new Error(`Bank_code_id not found for Code ${code}`) + return bank_code.id; +} \ No newline at end of file diff --git a/scripts/migrate-shifts.ts b/scripts/migrate-shifts.ts new file mode 100644 index 0000000..5b966c4 --- /dev/null +++ b/scripts/migrate-shifts.ts @@ -0,0 +1,210 @@ +import { PrismaClient as PrismaNew } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" + +const prisma_legacy = new PrismaLegacy({}); +const prisma = new PrismaNew({}); + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldShifts = { + time_sheet_id: string | null; + code: string | null; + type: string | null; + date: Date | null; + start_time: bigint | null; + end_time: bigint | null; + comment: string | null; + status: boolean | null; +} + +export const extractOldShifts = async () => { + for (let id = 1; id <= 61; id++) { + console.log(`Start of shift migration ***************************************************************`); + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB`); + + const new_timesheets = await findManyNewTimesheets(new_employee.id); + console.log(`New Timesheets found for employee ${id}`); + for (const ts of new_timesheets) { + console.log(`start_date = ${ts.start_date} timesheet_id = ${ts.id}`) + + } + console.log('***************************************************************'); + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(`Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + for (const old_timesheet of old_timesheets) { + if (!old_timesheet.start_date) continue; + const new_timesheet = new_timesheets.find((ts) => ts.start_date.getTime() === old_timesheet.start_date!.getTime()); + if (!new_timesheet) { + console.warn(`No new timesheet ${new_timesheet} matching legacy timesheet ${old_timesheet.id}`); + continue; + } + const old_shifts = await prisma_legacy.shifts.findMany({ + where: { time_sheet_id: old_timesheet.id }, + select: { + time_sheet_id: true, + code: true, + type: true, + date: true, + start_time: true, + end_time: true, + comment: true, + status: true, + }, + }); + await createManyNewShifts(new_timesheet.id, old_shifts); + } + } + await prisma_legacy.$disconnect(); + await prisma.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: new_employee.external_payroll_id.toString(), + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + return old_timesheets; +} + +const findManyNewTimesheets = async (employee_id: number) => { + const timesheets = await prisma.timesheets.findMany({ + where: { employee_id: employee_id }, + select: { id: true, start_date: true } + }) + return timesheets; +} + +const createManyNewShifts = async (timesheet_id: number, old_shifts: OldShifts[]) => { + for (const old_shift of old_shifts) { + let is_remote = true; + + const start = toHHmmfromLegacyTimestamp(old_shift.start_time); + if (old_shift.start_time == null || !start) { + console.warn(`Shift start invalid ${old_shift.start_time}`); + continue; + } + + const end = toHHmmfromLegacyTimestamp(old_shift.end_time); + if (old_shift.end_time == null || !end) { + console.warn(`Shift end invalid ${old_shift.end_time}`); + continue; + } + + if (old_shift.date == null) { + console.warn(`Shift date invalid ${old_shift.date}`); + continue; + } + + if (old_shift.status == null) { + console.warn(`status null for legacy shift ${old_shift}`); + continue; + } + if (old_shift.type == null) { + console.warn(`type null for legacy shift ${old_shift.type}`); + continue; + } + + if (old_shift.type === 'office') { + is_remote = false; + } + + if (old_shift.code == null) { + console.warn(`Code null for legacy shift ${old_shift.code}`); + continue; + } + const bank_code_id = await findBankCodeIdUsingOldCode(old_shift.code); + try { + await prisma.shifts.create({ + // where: { unique_ts_id_date_start_time: { + // timesheet_id, + // date: old_shift.date, + // start_time: toDateFromHHmm(start) }}, + // update: { + // start_time: toDateFromHHmm(start), + // end_time: toDateFromHHmm(end), + // comment: old_shift.comment, + // is_approved: old_shift.status, + // is_remote: is_remote, + // bank_code_id: bank_code_id, + // }, + data: { + date: old_shift.date, + start_time: toDateFromHHmm(start), + end_time: toDateFromHHmm(end), + comment: old_shift.comment, + is_approved: old_shift.status, + is_remote: is_remote, + timesheet_id: timesheet_id, + bank_code_id: bank_code_id, + }, + }); + } catch (error) { + console.log('An error occured during shifts creation'); + } + } +} + +const toHHmmfromLegacyTimestamp = (value: bigint | null): string | null => { + if (value == null) return null; + const date = new Date(Number(value)); + const hh = String(date.getHours()).padStart(2, '0'); + const mm = String(date.getMinutes()).padStart(2, '0'); + return `${hh}:${mm}`; +} + +const toDateFromHHmm = (hhmm: string): Date => { + const [hh, mm] = hhmm.split(':'); + const hours = Number(hh); + const minutes = Number(mm); + return new Date(Date.UTC(1970, 0, 1, hours, minutes, 0, 0)); +} + +const findBankCodeIdUsingOldCode = async (code: string): Promise => { + if (code === 'G700') { + code = 'G104'; + } else if (code === 'G140') { + code = 'G56' + } + const bank_code = await prisma.bankCodes.findFirst({ + where: { bank_code: code }, + select: { id: true, bank_code: true }, + }); + if (!bank_code) throw new Error(`Bank_code_id not found for Code ${code}`) + return bank_code.id; +} diff --git a/scripts/migrate-timesheets.ts b/scripts/migrate-timesheets.ts new file mode 100644 index 0000000..da25839 --- /dev/null +++ b/scripts/migrate-timesheets.ts @@ -0,0 +1,118 @@ +import { PrismaClient as Prisma } from "@prisma/client"; +import { PrismaClient as PrismaLegacy } from "@prisma/client-legacy" +import { toStringFromDate } from "src/common/utils/date-utils"; + + +type NewEmployee = { + id: number; + company_code: number; + external_payroll_id: number; +} + +type OldTimesheets = { + id: string; + start_date: Date | null; + status: boolean | null; +} + +const prisma_legacy = new PrismaLegacy({}); +const prisma_new = new Prisma({}); + +export const extractOldTimesheets = async () => { + for (let id = 1; id <= 61; id++) { + const new_employee = await findOneNewEmployee(id); + console.log(`Employee ${id} found in new DB ${new_employee.external_payroll_id}`); + + const old_employee_id = await findOneOldEmployee(new_employee); + console.log(`Employee ${new_employee.id} found in old DB`); + + const old_timesheets = await findManyOldTimesheets(old_employee_id); + console.log(` ${old_timesheets.length} Timesheets for employee ${old_employee_id}/${new_employee.id} found in old DB`); + + await createManyNewTimesheets(old_timesheets, new_employee); + console.log(`${old_timesheets.length} New Timesheets created in new DB for employee ${new_employee.id}`); + } + await prisma_legacy.$disconnect(); + await prisma_new.$disconnect(); +} + +const findOneNewEmployee = async (id: number): Promise => { + const new_employee = await prisma_new.employees.findUnique({ + where: { id: id }, + select: { + id: true, + company_code: true, + external_payroll_id: true, + }, + }); + if (!new_employee) throw new Error(`New Employee with id ${id} not found`) + return new_employee; +} + +const findOneOldEmployee = async (new_employee: NewEmployee): Promise => { + const employee_number = new_employee.external_payroll_id.toString() + const old_employee = await prisma_legacy.employees.findFirst({ + where: { + company: new_employee.company_code, + employee_number: employee_number, + }, + select: { + id: true, + }, + }); + if (!old_employee) throw new Error(`Old Employee not found`); + return old_employee.id; +} + +const findManyOldTimesheets = async (old_employee_id: string) => { + const old_timesheets = await prisma_legacy.time_sheets.findMany({ + where: { employee_id: old_employee_id }, + select: { id: true, start_date: true, status: true } + }); + if (!old_timesheets) throw new Error(`old Timesheets not found for employee_id ${old_employee_id}`) + return old_timesheets; +} + +const createManyNewTimesheets = async (old_timesheets: OldTimesheets[], new_employee: NewEmployee) => { + for (const timesheet of old_timesheets) { + if (timesheet.start_date == null) { + console.warn(`start_date invalid for legacy timesheet ${timesheet.id}`); + continue; + } + if (timesheet.status == null) { + console.warn(`status null for legacy timesheet ${timesheet.id}`); + continue; + } + + try { + const new_timesheet = await prisma_new.timesheets.upsert({ + where: { employee_id_start_date: { employee_id: new_employee.id, start_date: timesheet.start_date } }, + update: { + is_approved: timesheet.status, + }, + create: { + employee_id: new_employee.id, + start_date: timesheet.start_date, + is_approved: timesheet.status, + }, + }); + if (!new_timesheet) throw new Error( + `Timesheet with start_date: ${toStringFromDate(timesheet.start_date!)} for employee ${new_employee.id} not created` + ); + } catch (error) { + throw new Error('An error occured during timesheets creation'); + } + } +} + +extractOldTimesheets() + .then(() => { + console.log("Migration completed"); + }) + .catch((error) => { + console.error("Migration failed:", error); + }) + .finally(async () => { + await prisma_legacy.$disconnect(); + await prisma_new.$disconnect(); + }); \ No newline at end of file diff --git a/scripts/migration.service.ts b/scripts/migration.service.ts new file mode 100644 index 0000000..88dcb82 --- /dev/null +++ b/scripts/migration.service.ts @@ -0,0 +1,21 @@ +import { extractOldTimesheets } from "scripts/migrate-timesheets"; +import { extractOldExpenses } from "scripts/migrate-expenses"; +import { extractOldShifts } from "scripts/migrate-shifts"; +import { Injectable } from "@nestjs/common"; + +@Injectable() +export class MigrationService { + constructor() {} + + async migrateTimesheets() { + extractOldTimesheets(); + }; + + async migrateShifts() { + extractOldShifts(); + } + + async migrateExpenses() { + extractOldExpenses(); + } +} \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index e03505c..f2b98e5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { ValidationError } from 'class-validator'; import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module'; import { AuthenticationModule } from 'src/identity-and-account/authentication/auth.module'; import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-account.module'; +import { PrismaLegacyModule } from 'src/prisma-legacy/prisma.module'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and- ScheduleModule.forRoot(), //cronjobs NotificationsModule, PrismaModule, + PrismaLegacyModule, TimeAndAttendanceModule, IdentityAndAccountModule, ], diff --git a/src/common/utils/date-utils.ts b/src/common/utils/date-utils.ts index da63283..604da75 100644 --- a/src/common/utils/date-utils.ts +++ b/src/common/utils/date-utils.ts @@ -75,7 +75,7 @@ export const toStringFromDate = (date: Date) => //converts HHmm format to string -export const toHHmmFromString = (hhmm: string): Date => { +export const toDateFromHHmm = (hhmm: string): Date => { const [hh, mm] = hhmm.split(':').map(Number); const date = new Date('1970-01-01T00:00:00.000Z'); date.setUTCHours(hh, mm, 0, 0); diff --git a/src/main.ts b/src/main.ts index fbe5afd..5b9134b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,10 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { writeFileSync } from 'fs'; import * as session from 'express-session'; import * as passport from 'passport'; +// import { extractOldTimesheets } from 'scripts/migrate-timesheets'; +import { extractOldShifts } from 'scripts/migrate-shifts'; +import { extractOldTimesheets } from 'scripts/migrate-timesheets'; +import { extractOldExpenses } from 'scripts/migrate-expenses'; const SESSION_TOKEN_DURATION_MINUTES = 180 @@ -89,5 +93,11 @@ async function bootstrap() { await ensureAttachmentsTmpDir(); await app.listen(process.env.PORT ?? 3000); + + + // migration function calls + // await extractOldTimesheets(); + // await extractOldShifts(); + // await extractOldExpenses(); } bootstrap(); diff --git a/src/prisma-legacy/prisma.module.ts b/src/prisma-legacy/prisma.module.ts new file mode 100644 index 0000000..add3840 --- /dev/null +++ b/src/prisma-legacy/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaLegacyService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaLegacyService], + exports: [PrismaLegacyService], +}) +export class PrismaLegacyModule {} diff --git a/src/prisma-legacy/prisma.service.ts b/src/prisma-legacy/prisma.service.ts new file mode 100644 index 0000000..2e8fe8e --- /dev/null +++ b/src/prisma-legacy/prisma.service.ts @@ -0,0 +1,18 @@ +import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; +import { PrismaClient as PrismaLegacyClient } from '@prisma/client'; + +//Gestion des connections à la DB + +@Injectable() +export class PrismaLegacyService + extends PrismaLegacyClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + } + + async onModuleDestroy() { + await this.$disconnect(); + } +} diff --git a/src/time-and-attendance/shifts/services/shifts-create.service.ts b/src/time-and-attendance/shifts/services/shifts-create.service.ts index 060a761..7c8ec2c 100644 --- a/src/time-and-attendance/shifts/services/shifts-create.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-create.service.ts @@ -5,7 +5,7 @@ import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper"; import { EmailToIdResolver } from "src/common/mappers/email-id.mapper"; import { PrismaService } from "src/prisma/prisma.service"; import { Result } from "src/common/errors/result-error.factory"; -import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toHHmmFromString } from "src/common/utils/date-utils"; +import { toStringFromHHmm, toStringFromDate, toDateFromString, overlaps, toDateFromHHmm } from "src/common/utils/date-utils"; import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() @@ -135,8 +135,8 @@ export class ShiftsCreateService { //TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX const date = toDateFromString(dto.date); - const start_time = toHHmmFromString(dto.start_time); - const end_time = toHHmmFromString(dto.end_time); + const start_time = toDateFromHHmm(dto.start_time); + const end_time = toDateFromHHmm(dto.end_time); return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } }; } diff --git a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts index 85f4d87..ed602b0 100644 --- a/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts +++ b/src/time-and-attendance/shifts/services/shifts-update-delete.service.ts @@ -5,7 +5,7 @@ import { Injectable } from "@nestjs/common"; import { Normalized } from "src/time-and-attendance/utils/type.utils"; import { Result } from "src/common/errors/result-error.factory"; import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper"; -import { toDateFromString, toStringFromHHmm, toStringFromDate, toHHmmFromString, overlaps } from "src/common/utils/date-utils"; +import { toDateFromString, toStringFromHHmm, toStringFromDate, toDateFromHHmm, overlaps } from "src/common/utils/date-utils"; import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto"; @Injectable() @@ -145,8 +145,8 @@ export class ShiftsUpdateDeleteService { success: true, data: { date: toDateFromString(dto.date), - start_time: toHHmmFromString(dto.start_time), - end_time: toHHmmFromString(dto.end_time), + start_time: toDateFromHHmm(dto.start_time), + end_time: toDateFromHHmm(dto.end_time), bank_code_id: bank_code_id.data } }; @@ -160,8 +160,8 @@ export class ShiftsUpdateDeleteService { if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue; const has_overlap = overlaps( - { start: toHHmmFromString(shift_a.start_time), end: toHHmmFromString(shift_a.end_time) }, - { start: toHHmmFromString(shift_b.start_time), end: toHHmmFromString(shift_b.end_time) }, + { start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) }, + { start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) }, ); if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` }; } diff --git a/src/~misc_deprecated-files/expenses-query.service.ts b/src/~misc_deprecated-files/expenses-query.service.ts index d95f1a7..6eafba2 100644 --- a/src/~misc_deprecated-files/expenses-query.service.ts +++ b/src/~misc_deprecated-files/expenses-query.service.ts @@ -124,7 +124,7 @@ // // } // // async findAll(filters: SearchExpensesDto): Promise { -// // const where = buildPrismaWhere(filters); + // const where = buildPrismaWhere(filters); // // const expenses = await this.prisma.expenses.findMany({ where }) // // return expenses; // // } diff --git a/test/customers.e2e-spec.ts b/test/customers.e2e-spec.ts index 78d52d9..98172db 100644 --- a/test/customers.e2e-spec.ts +++ b/test/customers.e2e-spec.ts @@ -1,125 +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'; +// 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; -}; +// 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 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); +// 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, - }; -} +// 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; +// describe('Customers (e2e) — autonome', () => { +// let app: INestApplication; +// let prisma: PrismaService; +// let createdId: number | null = null; - beforeAll(async () => { - app = await createApp(); - prisma = app.get(PrismaService); - }); +// 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(); - }); +// 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(`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(); +// 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) { +// 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); +// 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}$/); +// 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; +// 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 })); - }); +// 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; - } +// 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 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'); - }); +// 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(`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(`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; - } +// 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; - }); -}); +// const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`); +// expect([200, 204]).toContain(del.status); +// if (createdId === id) createdId = null; +// }); +// }); diff --git a/test/pay-periods-approval.e2e-spec.ts b/test/pay-periods-approval.e2e-spec.ts index 0e780d5..e061c16 100644 --- a/test/pay-periods-approval.e2e-spec.ts +++ b/test/pay-periods-approval.e2e-spec.ts @@ -1,143 +1,143 @@ -// test/pay-periods-approval.e2e-spec.ts -const supertest = require('supertest'); -import { INestApplication } from '@nestjs/common'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { createApp } from './utils/testing-app'; -import { makeEmployee } from './factories/employee.factory'; -import { makeTimesheet } from './factories/timesheet.factory'; +// // test/pay-periods-approval.e2e-spec.ts +// const supertest = require('supertest'); +// import { INestApplication } from '@nestjs/common'; +// import { PrismaService } from 'src/prisma/prisma.service'; +// import { createApp } from './utils/testing-app'; +// import { makeEmployee } from './factories/employee.factory'; +// import { makeTimesheet } from './factories/timesheet.factory'; -describe('PayPeriods approval (e2e)', () => { - const BASE = '/pay-periods'; - let app: INestApplication; - let prisma: PrismaService; +// describe('PayPeriods approval (e2e)', () => { +// const BASE = '/pay-periods'; +// let app: INestApplication; +// let prisma: PrismaService; - let periodYear: number; - let periodNumber: number; +// let periodYear: number; +// let periodNumber: number; - let employeeId: number; - let timesheetId: number; - let shiftId: number; - let expenseId: number; +// let employeeId: number; +// let timesheetId: number; +// let shiftId: number; +// let expenseId: number; - const isoDay = (d: Date) => - new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString(); - const isoTime = (h: number, m = 0) => - new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); +// const isoDay = (d: Date) => +// new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString(); +// const isoTime = (h: number, m = 0) => +// new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString(); - beforeAll(async () => { - app = await createApp(); - prisma = app.get(PrismaService); +// beforeAll(async () => { +// app = await createApp(); +// prisma = app.get(PrismaService); - // 1) Récupère un pay period existant - const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } }); - if (!period) throw new Error('Aucun pay period en DB (seed requis).'); +// // 1) Récupère un pay period existant +// const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } }); +// if (!period) throw new Error('Aucun pay period en DB (seed requis).'); - periodYear = period.year; - periodNumber = period.period_number; +// periodYear = period.year; +// periodNumber = period.period_number; - // 2) Crée un employé + timesheet (non approuvé) - const empRes = await supertest(app.getHttpServer()) - .post('/employees') - .send(makeEmployee()); - if (empRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create employee error:', empRes.body || empRes.text); - throw new Error('Impossible de créer un employé pour le test pay-periods.'); - } - employeeId = empRes.body.id; +// // 2) Crée un employé + timesheet (non approuvé) +// const empRes = await supertest(app.getHttpServer()) +// .post('/employees') +// .send(makeEmployee()); +// if (empRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create employee error:', empRes.body || empRes.text); +// throw new Error('Impossible de créer un employé pour le test pay-periods.'); +// } +// employeeId = empRes.body.id; - const tsRes = await supertest(app.getHttpServer()) - .post('/timesheets') - .send(makeTimesheet(employeeId, { is_approved: false })); - if (tsRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create timesheet error:', tsRes.body || tsRes.text); - throw new Error('Impossible de créer un timesheet pour le test pay-periods.'); - } - timesheetId = tsRes.body.id; +// const tsRes = await supertest(app.getHttpServer()) +// .post('/timesheets') +// .send(makeTimesheet(employeeId, { is_approved: false })); +// if (tsRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create timesheet error:', tsRes.body || tsRes.text); +// throw new Error('Impossible de créer un timesheet pour le test pay-periods.'); +// } +// timesheetId = tsRes.body.id; - // 3) Bank codes - const bcShift = await prisma.bankCodes.findFirst({ - where: { categorie: 'SHIFT' }, - select: { id: true }, - }); - if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.'); - const bcExpense = await prisma.bankCodes.findFirst({ - where: { categorie: 'EXPENSE' }, - select: { id: true }, - }); - if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.'); +// // 3) Bank codes +// const bcShift = await prisma.bankCodes.findFirst({ +// where: { categorie: 'SHIFT' }, +// select: { id: true }, +// }); +// if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.'); +// const bcExpense = await prisma.bankCodes.findFirst({ +// where: { categorie: 'EXPENSE' }, +// select: { id: true }, +// }); +// if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.'); - // 4) Crée 1 shift + 1 expense DANS la période choisie - const dateISO = isoDay(period.start_date); +// // 4) Crée 1 shift + 1 expense DANS la période choisie +// const dateISO = isoDay(period.start_date); - const shiftRes = await supertest(app.getHttpServer()) - .post('/shifts') - .send({ - timesheet_id: timesheetId, - bank_code_id: bcShift.id, - date: dateISO, - start_time: isoTime(9), - end_time: isoTime(17), - description: 'PP approval shift', - }); - if (shiftRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create shift error:', shiftRes.body || shiftRes.text); - throw new Error('Création shift échouée.'); - } - shiftId = shiftRes.body.id; +// const shiftRes = await supertest(app.getHttpServer()) +// .post('/shifts') +// .send({ +// timesheet_id: timesheetId, +// bank_code_id: bcShift.id, +// date: dateISO, +// start_time: isoTime(9), +// end_time: isoTime(17), +// description: 'PP approval shift', +// }); +// if (shiftRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create shift error:', shiftRes.body || shiftRes.text); +// throw new Error('Création shift échouée.'); +// } +// shiftId = shiftRes.body.id; - const expenseRes = await supertest(app.getHttpServer()) - .post('/Expenses') // <- respecte ta casse de route - .send({ - timesheet_id: timesheetId, - bank_code_id: bcExpense.id, - date: dateISO, - amount: 42, - description: 'PP approval expense', - is_approved: false, - }); - if (expenseRes.status !== 201) { - // eslint-disable-next-line no-console - console.warn('Create expense error:', expenseRes.body || expenseRes.text); - throw new Error('Création expense échouée.'); - } - expenseId = expenseRes.body.id; - }); +// const expenseRes = await supertest(app.getHttpServer()) +// .post('/Expenses') // <- respecte ta casse de route +// .send({ +// timesheet_id: timesheetId, +// bank_code_id: bcExpense.id, +// date: dateISO, +// amount: 42, +// description: 'PP approval expense', +// is_approved: false, +// }); +// if (expenseRes.status !== 201) { +// // eslint-disable-next-line no-console +// console.warn('Create expense error:', expenseRes.body || expenseRes.text); +// throw new Error('Création expense échouée.'); +// } +// expenseId = expenseRes.body.id; +// }); - afterAll(async () => { - await app.close(); - await prisma.$disconnect(); - }); +// afterAll(async () => { +// await app.close(); +// await prisma.$disconnect(); +// }); - it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => { - const res = await supertest(app.getHttpServer()) - .patch(`${BASE}/${periodYear}/${periodNumber}/approval`) - .send(); // aucun body requis par ton contrôleur - expect([200, 204]).toContain(res.status); - if (res.body?.message) { - expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`); - } +// it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => { +// const res = await supertest(app.getHttpServer()) +// .patch(`${BASE}/${periodYear}/${periodNumber}/approval`) +// .send(); // aucun body requis par ton contrôleur +// expect([200, 204]).toContain(res.status); +// if (res.body?.message) { +// expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`); +// } - // Vérifie cascade: - const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`); - expect(tsCheck.status).toBe(200); - expect(tsCheck.body?.is_approved).toBe(true); +// // Vérifie cascade: +// const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`); +// expect(tsCheck.status).toBe(200); +// expect(tsCheck.body?.is_approved).toBe(true); - const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`); - expect(shiftCheck.status).toBe(200); - expect(shiftCheck.body?.is_approved).toBe(true); +// const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`); +// expect(shiftCheck.status).toBe(200); +// expect(shiftCheck.body?.is_approved).toBe(true); - const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`); - expect(expCheck.status).toBe(200); - expect(expCheck.body?.is_approved).toBe(true); - }); +// const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`); +// expect(expCheck.status).toBe(200); +// expect(expCheck.body?.is_approved).toBe(true); +// }); - it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => { - const bad = await supertest(app.getHttpServer()) - .patch(`${BASE}/2099/999/approval`) - .send(); - expect(bad.status).toBe(404); - }); -}); +// it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => { +// const bad = await supertest(app.getHttpServer()) +// .patch(`${BASE}/2099/999/approval`) +// .send(); +// expect(bad.status).toBe(404); +// }); +// });