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
This commit is contained in:
parent
ddb6fa2ada
commit
5452641f19
197
prisma-legacy/schema.prisma
Normal file
197
prisma-legacy/schema.prisma
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "public"."users_phone_number_key";
|
||||||
|
|
@ -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");
|
||||||
|
|
@ -11,7 +11,7 @@ generator client {
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL_DEV")
|
url = env("DATABASE_URL_STAGING")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Users {
|
model Users {
|
||||||
|
|
@ -19,7 +19,7 @@ model Users {
|
||||||
first_name String
|
first_name String
|
||||||
last_name String
|
last_name String
|
||||||
email String @unique
|
email String @unique
|
||||||
phone_number String @unique
|
phone_number String
|
||||||
residence String?
|
residence String?
|
||||||
role Roles @default(GUEST)
|
role Roles @default(GUEST)
|
||||||
|
|
||||||
|
|
@ -180,7 +180,7 @@ model Shifts {
|
||||||
comment String?
|
comment String?
|
||||||
|
|
||||||
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
archive ShiftsArchive[] @relation("ShiftsToArchive")
|
||||||
|
@@unique([timesheet_id, date, start_time], name: "unique_ts_id_date_start_time")
|
||||||
@@map("shifts")
|
@@map("shifts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,7 +232,7 @@ model Expenses {
|
||||||
is_approved Boolean @default(false)
|
is_approved Boolean @default(false)
|
||||||
|
|
||||||
archive ExpensesArchive[] @relation("ExpensesToArchive")
|
archive ExpensesArchive[] @relation("ExpensesToArchive")
|
||||||
|
@@unique([timesheet_id, date, amount, mileage], name: "unique_ts_id_date_amount_mileage")
|
||||||
@@map("expenses")
|
@@map("expenses")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
270
scripts/done/import-employees-from-csv.ts
Normal file
270
scripts/done/import-employees-from-csv.ts
Normal file
|
|
@ -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<string, UserSummary>();
|
||||||
|
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();
|
||||||
|
});
|
||||||
106
scripts/done/import-users-from-csv.ts
Normal file
106
scripts/done/import-users-from-csv.ts
Normal file
|
|
@ -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();
|
||||||
|
});
|
||||||
67
scripts/done/init-preferences.ts
Normal file
67
scripts/done/init-preferences.ts
Normal file
|
|
@ -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();
|
||||||
|
});
|
||||||
164
scripts/migrate-expenses.ts
Normal file
164
scripts/migrate-expenses.ts
Normal file
|
|
@ -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<NewEmployee> => {
|
||||||
|
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<string> => {
|
||||||
|
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<number> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
210
scripts/migrate-shifts.ts
Normal file
210
scripts/migrate-shifts.ts
Normal file
|
|
@ -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<NewEmployee> => {
|
||||||
|
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<string> => {
|
||||||
|
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<number> => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
118
scripts/migrate-timesheets.ts
Normal file
118
scripts/migrate-timesheets.ts
Normal file
|
|
@ -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<NewEmployee> => {
|
||||||
|
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<string> => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
21
scripts/migration.service.ts
Normal file
21
scripts/migration.service.ts
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import { ValidationError } from 'class-validator';
|
||||||
import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module';
|
import { TimeAndAttendanceModule } from 'src/time-and-attendance/time-and-attendance.module';
|
||||||
import { AuthenticationModule } from 'src/identity-and-account/authentication/auth.module';
|
import { AuthenticationModule } from 'src/identity-and-account/authentication/auth.module';
|
||||||
import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-account.module';
|
import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-account.module';
|
||||||
|
import { PrismaLegacyModule } from 'src/prisma-legacy/prisma.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -20,6 +21,7 @@ import { IdentityAndAccountModule } from 'src/identity-and-account/identity-and-
|
||||||
ScheduleModule.forRoot(), //cronjobs
|
ScheduleModule.forRoot(), //cronjobs
|
||||||
NotificationsModule,
|
NotificationsModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
PrismaLegacyModule,
|
||||||
TimeAndAttendanceModule,
|
TimeAndAttendanceModule,
|
||||||
IdentityAndAccountModule,
|
IdentityAndAccountModule,
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export const toStringFromDate = (date: Date) =>
|
||||||
|
|
||||||
|
|
||||||
//converts HHmm format to string
|
//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 [hh, mm] = hhmm.split(':').map(Number);
|
||||||
const date = new Date('1970-01-01T00:00:00.000Z');
|
const date = new Date('1970-01-01T00:00:00.000Z');
|
||||||
date.setUTCHours(hh, mm, 0, 0);
|
date.setUTCHours(hh, mm, 0, 0);
|
||||||
|
|
|
||||||
10
src/main.ts
10
src/main.ts
|
|
@ -18,6 +18,10 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
import { writeFileSync } from 'fs';
|
import { writeFileSync } from 'fs';
|
||||||
import * as session from 'express-session';
|
import * as session from 'express-session';
|
||||||
import * as passport from 'passport';
|
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
|
const SESSION_TOKEN_DURATION_MINUTES = 180
|
||||||
|
|
||||||
|
|
@ -89,5 +93,11 @@ async function bootstrap() {
|
||||||
|
|
||||||
await ensureAttachmentsTmpDir();
|
await ensureAttachmentsTmpDir();
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
|
||||||
|
|
||||||
|
// migration function calls
|
||||||
|
// await extractOldTimesheets();
|
||||||
|
// await extractOldShifts();
|
||||||
|
// await extractOldExpenses();
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|
|
||||||
9
src/prisma-legacy/prisma.module.ts
Normal file
9
src/prisma-legacy/prisma.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { PrismaLegacyService } from './prisma.service';
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [PrismaLegacyService],
|
||||||
|
exports: [PrismaLegacyService],
|
||||||
|
})
|
||||||
|
export class PrismaLegacyModule {}
|
||||||
18
src/prisma-legacy/prisma.service.ts
Normal file
18
src/prisma-legacy/prisma.service.ts
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import { BankCodesResolver } from "src/common/mappers/bank-type-id.mapper";
|
||||||
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
import { EmailToIdResolver } from "src/common/mappers/email-id.mapper";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
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";
|
import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto";
|
||||||
|
|
||||||
@Injectable()
|
@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
|
//TODO: validate date and time to ensure "banana" is not accepted using an if statement and a REGEX
|
||||||
const date = toDateFromString(dto.date);
|
const date = toDateFromString(dto.date);
|
||||||
const start_time = toHHmmFromString(dto.start_time);
|
const start_time = toDateFromHHmm(dto.start_time);
|
||||||
const end_time = toHHmmFromString(dto.end_time);
|
const end_time = toDateFromHHmm(dto.end_time);
|
||||||
|
|
||||||
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
|
return { success: true, data: { date, start_time, end_time, bank_code_id: bank_code_id.data } };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { Injectable } from "@nestjs/common";
|
||||||
import { Normalized } from "src/time-and-attendance/utils/type.utils";
|
import { Normalized } from "src/time-and-attendance/utils/type.utils";
|
||||||
import { Result } from "src/common/errors/result-error.factory";
|
import { Result } from "src/common/errors/result-error.factory";
|
||||||
import { EmployeeTimesheetResolver } from "src/common/mappers/timesheet.mapper";
|
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";
|
import { ShiftDto } from "src/time-and-attendance/shifts/dtos/shift-create.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
|
@ -145,8 +145,8 @@ export class ShiftsUpdateDeleteService {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
date: toDateFromString(dto.date),
|
date: toDateFromString(dto.date),
|
||||||
start_time: toHHmmFromString(dto.start_time),
|
start_time: toDateFromHHmm(dto.start_time),
|
||||||
end_time: toHHmmFromString(dto.end_time),
|
end_time: toDateFromHHmm(dto.end_time),
|
||||||
bank_code_id: bank_code_id.data
|
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;
|
if (shift_a.date !== shift_b.date || shift_a.id === shift_b.id) continue;
|
||||||
const has_overlap = overlaps(
|
const has_overlap = overlaps(
|
||||||
{ start: toHHmmFromString(shift_a.start_time), end: toHHmmFromString(shift_a.end_time) },
|
{ start: toDateFromHHmm(shift_a.start_time), end: toDateFromHHmm(shift_a.end_time) },
|
||||||
{ start: toHHmmFromString(shift_b.start_time), end: toHHmmFromString(shift_b.end_time) },
|
{ start: toDateFromHHmm(shift_b.start_time), end: toDateFromHHmm(shift_b.end_time) },
|
||||||
);
|
);
|
||||||
if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` };
|
if (has_overlap) return { success: false, error: `SHIFT_OVERLAP` };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@
|
||||||
// // }
|
// // }
|
||||||
|
|
||||||
// // async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
|
// // async findAll(filters: SearchExpensesDto): Promise<Expenses[]> {
|
||||||
// // const where = buildPrismaWhere(filters);
|
// const where = buildPrismaWhere(filters);
|
||||||
// // const expenses = await this.prisma.expenses.findMany({ where })
|
// // const expenses = await this.prisma.expenses.findMany({ where })
|
||||||
// // return expenses;
|
// // return expenses;
|
||||||
// // }
|
// // }
|
||||||
|
|
|
||||||
|
|
@ -1,125 +1,125 @@
|
||||||
import * as request from 'supertest';
|
// import * as request from 'supertest';
|
||||||
import { INestApplication } from '@nestjs/common';
|
// import { INestApplication } from '@nestjs/common';
|
||||||
import { createApp } from './utils/testing-app';
|
// import { createApp } from './utils/testing-app';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
// import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
|
|
||||||
type CustomerPayload = {
|
// type CustomerPayload = {
|
||||||
user_id?: string;
|
// user_id?: string;
|
||||||
first_name: string;
|
// first_name: string;
|
||||||
last_name: string;
|
// last_name: string;
|
||||||
email?: string;
|
// email?: string;
|
||||||
phone_number: number;
|
// phone_number: number;
|
||||||
residence?: string;
|
// residence?: string;
|
||||||
invoice_id: number;
|
// invoice_id: number;
|
||||||
};
|
// };
|
||||||
|
|
||||||
const BASE = '/customers';
|
// const BASE = '/customers';
|
||||||
|
|
||||||
const uniqueEmail = () =>
|
// const uniqueEmail = () =>
|
||||||
`customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`;
|
// `customer+${Date.now()}_${Math.random().toString(36).slice(2,8)}@test.local`;
|
||||||
const uniquePhone = () =>
|
// const uniquePhone = () =>
|
||||||
Math.floor(100_000_000 + Math.random() * 900_000_000);
|
// Math.floor(100_000_000 + Math.random() * 900_000_000);
|
||||||
|
|
||||||
function makeCustomerPayload(overrides: Partial<CustomerPayload> = {}): CustomerPayload {
|
// function makeCustomerPayload(overrides: Partial<CustomerPayload> = {}): CustomerPayload {
|
||||||
return {
|
// return {
|
||||||
first_name: 'Gandalf',
|
// first_name: 'Gandalf',
|
||||||
last_name: 'TheGray',
|
// last_name: 'TheGray',
|
||||||
email: uniqueEmail(),
|
// email: uniqueEmail(),
|
||||||
phone_number: uniquePhone(),
|
// phone_number: uniquePhone(),
|
||||||
residence: '1 Ringbearer’s Way, Mount Doom, ME',
|
// residence: '1 Ringbearer’s Way, Mount Doom, ME',
|
||||||
invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000),
|
// invoice_id: Math.floor(1_000_000 + Math.random() * 9_000_000),
|
||||||
...overrides,
|
// ...overrides,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
describe('Customers (e2e) — autonome', () => {
|
// describe('Customers (e2e) — autonome', () => {
|
||||||
let app: INestApplication;
|
// let app: INestApplication;
|
||||||
let prisma: PrismaService;
|
// let prisma: PrismaService;
|
||||||
let createdId: number | null = null;
|
// let createdId: number | null = null;
|
||||||
|
|
||||||
beforeAll(async () => {
|
// beforeAll(async () => {
|
||||||
app = await createApp();
|
// app = await createApp();
|
||||||
prisma = app.get(PrismaService);
|
// prisma = app.get(PrismaService);
|
||||||
});
|
// });
|
||||||
|
|
||||||
afterAll(async () => {
|
// afterAll(async () => {
|
||||||
if (createdId) {
|
// if (createdId) {
|
||||||
try { await prisma.customers.delete({ where: { id: createdId } }); } catch {}
|
// try { await prisma.customers.delete({ where: { id: createdId } }); } catch {}
|
||||||
}
|
// }
|
||||||
await app.close();
|
// await app.close();
|
||||||
await prisma.$disconnect();
|
// await prisma.$disconnect();
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`GET ${BASE} → 200 (array)`, async () => {
|
// it(`GET ${BASE} → 200 (array)`, async () => {
|
||||||
const res = await request(app.getHttpServer()).get(BASE);
|
// const res = await request(app.getHttpServer()).get(BASE);
|
||||||
expect(res.status).toBe(200);
|
// expect(res.status).toBe(200);
|
||||||
expect(Array.isArray(res.body)).toBe(true);
|
// expect(Array.isArray(res.body)).toBe(true);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => {
|
// it(`POST ${BASE} (valid) → 201 puis GET /:id → 200`, async () => {
|
||||||
const payload = makeCustomerPayload();
|
// const payload = makeCustomerPayload();
|
||||||
|
|
||||||
const createRes = await request(app.getHttpServer()).post(BASE).send(payload);
|
// const createRes = await request(app.getHttpServer()).post(BASE).send(payload);
|
||||||
if (createRes.status !== 201) {
|
// if (createRes.status !== 201) {
|
||||||
|
|
||||||
console.log('Create error:', createRes.body || createRes.text);
|
// console.log('Create error:', createRes.body || createRes.text);
|
||||||
}
|
// }
|
||||||
expect(createRes.status).toBe(201);
|
// expect(createRes.status).toBe(201);
|
||||||
|
|
||||||
expect(createRes.body).toEqual(
|
// expect(createRes.body).toEqual(
|
||||||
expect.objectContaining({
|
// expect.objectContaining({
|
||||||
id: expect.any(Number),
|
// id: expect.any(Number),
|
||||||
user_id: expect.any(String),
|
// user_id: expect.any(String),
|
||||||
invoice_id: payload.invoice_id,
|
// invoice_id: payload.invoice_id,
|
||||||
})
|
// })
|
||||||
);
|
// );
|
||||||
expect(createRes.body.user_id).toMatch(/^[0-9a-fA-F-]{36}$/);
|
// 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}`);
|
// const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`);
|
||||||
expect(getRes.status).toBe(200);
|
// expect(getRes.status).toBe(200);
|
||||||
expect(getRes.body).toEqual(expect.objectContaining({ id: createdId }));
|
// expect(getRes.body).toEqual(expect.objectContaining({ id: createdId }));
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => {
|
// it(`PATCH ${BASE}/:id → 200 (first_name mis à jour)`, async () => {
|
||||||
if (!createdId) {
|
// if (!createdId) {
|
||||||
const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
|
// const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
|
||||||
expect(create.status).toBe(201);
|
// expect(create.status).toBe(201);
|
||||||
createdId = create.body.id;
|
// createdId = create.body.id;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const patchRes = await request(app.getHttpServer())
|
// const patchRes = await request(app.getHttpServer())
|
||||||
.patch(`${BASE}/${createdId}`)
|
// .patch(`${BASE}/${createdId}`)
|
||||||
.send({ first_name: 'Mithrandir' });
|
// .send({ first_name: 'Mithrandir' });
|
||||||
expect([200, 204]).toContain(patchRes.status);
|
// expect([200, 204]).toContain(patchRes.status);
|
||||||
|
|
||||||
const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`);
|
// const getRes = await request(app.getHttpServer()).get(`${BASE}/${createdId}`);
|
||||||
expect(getRes.status).toBe(200);
|
// expect(getRes.status).toBe(200);
|
||||||
expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir');
|
// expect(getRes.body.first_name ?? 'Mithrandir').toBe('Mithrandir');
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`GET ${BASE}/:id (not found) → 404/400`, async () => {
|
// it(`GET ${BASE}/:id (not found) → 404/400`, async () => {
|
||||||
const res = await request(app.getHttpServer()).get(`${BASE}/999999`);
|
// const res = await request(app.getHttpServer()).get(`${BASE}/999999`);
|
||||||
expect([404, 400]).toContain(res.status);
|
// expect([404, 400]).toContain(res.status);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`POST ${BASE} (invalid payload) → 400`, async () => {
|
// it(`POST ${BASE} (invalid payload) → 400`, async () => {
|
||||||
const res = await request(app.getHttpServer()).post(BASE).send({});
|
// const res = await request(app.getHttpServer()).post(BASE).send({});
|
||||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
// expect(res.status).toBeGreaterThanOrEqual(400);
|
||||||
expect(res.status).toBeLessThan(500);
|
// expect(res.status).toBeLessThan(500);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`DELETE ${BASE}/:id → 200/204`, async () => {
|
// it(`DELETE ${BASE}/:id → 200/204`, async () => {
|
||||||
let id = createdId;
|
// let id = createdId;
|
||||||
if (!id) {
|
// if (!id) {
|
||||||
const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
|
// const create = await request(app.getHttpServer()).post(BASE).send(makeCustomerPayload());
|
||||||
expect(create.status).toBe(201);
|
// expect(create.status).toBe(201);
|
||||||
id = create.body.id;
|
// id = create.body.id;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`);
|
// const del = await request(app.getHttpServer()).delete(`${BASE}/${id}`);
|
||||||
expect([200, 204]).toContain(del.status);
|
// expect([200, 204]).toContain(del.status);
|
||||||
if (createdId === id) createdId = null;
|
// if (createdId === id) createdId = null;
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,143 @@
|
||||||
// test/pay-periods-approval.e2e-spec.ts
|
// // test/pay-periods-approval.e2e-spec.ts
|
||||||
const supertest = require('supertest');
|
// const supertest = require('supertest');
|
||||||
import { INestApplication } from '@nestjs/common';
|
// import { INestApplication } from '@nestjs/common';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
// import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { createApp } from './utils/testing-app';
|
// import { createApp } from './utils/testing-app';
|
||||||
import { makeEmployee } from './factories/employee.factory';
|
// import { makeEmployee } from './factories/employee.factory';
|
||||||
import { makeTimesheet } from './factories/timesheet.factory';
|
// import { makeTimesheet } from './factories/timesheet.factory';
|
||||||
|
|
||||||
describe('PayPeriods approval (e2e)', () => {
|
// describe('PayPeriods approval (e2e)', () => {
|
||||||
const BASE = '/pay-periods';
|
// const BASE = '/pay-periods';
|
||||||
let app: INestApplication;
|
// let app: INestApplication;
|
||||||
let prisma: PrismaService;
|
// let prisma: PrismaService;
|
||||||
|
|
||||||
let periodYear: number;
|
// let periodYear: number;
|
||||||
let periodNumber: number;
|
// let periodNumber: number;
|
||||||
|
|
||||||
let employeeId: number;
|
// let employeeId: number;
|
||||||
let timesheetId: number;
|
// let timesheetId: number;
|
||||||
let shiftId: number;
|
// let shiftId: number;
|
||||||
let expenseId: number;
|
// let expenseId: number;
|
||||||
|
|
||||||
const isoDay = (d: Date) =>
|
// const isoDay = (d: Date) =>
|
||||||
new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString();
|
// new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())).toISOString();
|
||||||
const isoTime = (h: number, m = 0) =>
|
// const isoTime = (h: number, m = 0) =>
|
||||||
new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString();
|
// new Date(Date.UTC(1970, 0, 1, h, m, 0)).toISOString();
|
||||||
|
|
||||||
beforeAll(async () => {
|
// beforeAll(async () => {
|
||||||
app = await createApp();
|
// app = await createApp();
|
||||||
prisma = app.get(PrismaService);
|
// prisma = app.get(PrismaService);
|
||||||
|
|
||||||
// 1) Récupère un pay period existant
|
// // 1) Récupère un pay period existant
|
||||||
const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } });
|
// const period = await prisma.payPeriods.findFirst({ orderBy: { period_number: 'asc' } });
|
||||||
if (!period) throw new Error('Aucun pay period en DB (seed requis).');
|
// if (!period) throw new Error('Aucun pay period en DB (seed requis).');
|
||||||
|
|
||||||
periodYear = period.year;
|
// periodYear = period.year;
|
||||||
periodNumber = period.period_number;
|
// periodNumber = period.period_number;
|
||||||
|
|
||||||
// 2) Crée un employé + timesheet (non approuvé)
|
// // 2) Crée un employé + timesheet (non approuvé)
|
||||||
const empRes = await supertest(app.getHttpServer())
|
// const empRes = await supertest(app.getHttpServer())
|
||||||
.post('/employees')
|
// .post('/employees')
|
||||||
.send(makeEmployee());
|
// .send(makeEmployee());
|
||||||
if (empRes.status !== 201) {
|
// if (empRes.status !== 201) {
|
||||||
// eslint-disable-next-line no-console
|
// // eslint-disable-next-line no-console
|
||||||
console.warn('Create employee error:', empRes.body || empRes.text);
|
// console.warn('Create employee error:', empRes.body || empRes.text);
|
||||||
throw new Error('Impossible de créer un employé pour le test pay-periods.');
|
// throw new Error('Impossible de créer un employé pour le test pay-periods.');
|
||||||
}
|
// }
|
||||||
employeeId = empRes.body.id;
|
// employeeId = empRes.body.id;
|
||||||
|
|
||||||
const tsRes = await supertest(app.getHttpServer())
|
// const tsRes = await supertest(app.getHttpServer())
|
||||||
.post('/timesheets')
|
// .post('/timesheets')
|
||||||
.send(makeTimesheet(employeeId, { is_approved: false }));
|
// .send(makeTimesheet(employeeId, { is_approved: false }));
|
||||||
if (tsRes.status !== 201) {
|
// if (tsRes.status !== 201) {
|
||||||
// eslint-disable-next-line no-console
|
// // eslint-disable-next-line no-console
|
||||||
console.warn('Create timesheet error:', tsRes.body || tsRes.text);
|
// console.warn('Create timesheet error:', tsRes.body || tsRes.text);
|
||||||
throw new Error('Impossible de créer un timesheet pour le test pay-periods.');
|
// throw new Error('Impossible de créer un timesheet pour le test pay-periods.');
|
||||||
}
|
// }
|
||||||
timesheetId = tsRes.body.id;
|
// timesheetId = tsRes.body.id;
|
||||||
|
|
||||||
// 3) Bank codes
|
// // 3) Bank codes
|
||||||
const bcShift = await prisma.bankCodes.findFirst({
|
// const bcShift = await prisma.bankCodes.findFirst({
|
||||||
where: { categorie: 'SHIFT' },
|
// where: { categorie: 'SHIFT' },
|
||||||
select: { id: true },
|
// select: { id: true },
|
||||||
});
|
// });
|
||||||
if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.');
|
// if (!bcShift) throw new Error('Aucun bank code SHIFT trouvé.');
|
||||||
const bcExpense = await prisma.bankCodes.findFirst({
|
// const bcExpense = await prisma.bankCodes.findFirst({
|
||||||
where: { categorie: 'EXPENSE' },
|
// where: { categorie: 'EXPENSE' },
|
||||||
select: { id: true },
|
// select: { id: true },
|
||||||
});
|
// });
|
||||||
if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.');
|
// if (!bcExpense) throw new Error('Aucun bank code EXPENSE trouvé.');
|
||||||
|
|
||||||
// 4) Crée 1 shift + 1 expense DANS la période choisie
|
// // 4) Crée 1 shift + 1 expense DANS la période choisie
|
||||||
const dateISO = isoDay(period.start_date);
|
// const dateISO = isoDay(period.start_date);
|
||||||
|
|
||||||
const shiftRes = await supertest(app.getHttpServer())
|
// const shiftRes = await supertest(app.getHttpServer())
|
||||||
.post('/shifts')
|
// .post('/shifts')
|
||||||
.send({
|
// .send({
|
||||||
timesheet_id: timesheetId,
|
// timesheet_id: timesheetId,
|
||||||
bank_code_id: bcShift.id,
|
// bank_code_id: bcShift.id,
|
||||||
date: dateISO,
|
// date: dateISO,
|
||||||
start_time: isoTime(9),
|
// start_time: isoTime(9),
|
||||||
end_time: isoTime(17),
|
// end_time: isoTime(17),
|
||||||
description: 'PP approval shift',
|
// description: 'PP approval shift',
|
||||||
});
|
// });
|
||||||
if (shiftRes.status !== 201) {
|
// if (shiftRes.status !== 201) {
|
||||||
// eslint-disable-next-line no-console
|
// // eslint-disable-next-line no-console
|
||||||
console.warn('Create shift error:', shiftRes.body || shiftRes.text);
|
// console.warn('Create shift error:', shiftRes.body || shiftRes.text);
|
||||||
throw new Error('Création shift échouée.');
|
// throw new Error('Création shift échouée.');
|
||||||
}
|
// }
|
||||||
shiftId = shiftRes.body.id;
|
// shiftId = shiftRes.body.id;
|
||||||
|
|
||||||
const expenseRes = await supertest(app.getHttpServer())
|
// const expenseRes = await supertest(app.getHttpServer())
|
||||||
.post('/Expenses') // <- respecte ta casse de route
|
// .post('/Expenses') // <- respecte ta casse de route
|
||||||
.send({
|
// .send({
|
||||||
timesheet_id: timesheetId,
|
// timesheet_id: timesheetId,
|
||||||
bank_code_id: bcExpense.id,
|
// bank_code_id: bcExpense.id,
|
||||||
date: dateISO,
|
// date: dateISO,
|
||||||
amount: 42,
|
// amount: 42,
|
||||||
description: 'PP approval expense',
|
// description: 'PP approval expense',
|
||||||
is_approved: false,
|
// is_approved: false,
|
||||||
});
|
// });
|
||||||
if (expenseRes.status !== 201) {
|
// if (expenseRes.status !== 201) {
|
||||||
// eslint-disable-next-line no-console
|
// // eslint-disable-next-line no-console
|
||||||
console.warn('Create expense error:', expenseRes.body || expenseRes.text);
|
// console.warn('Create expense error:', expenseRes.body || expenseRes.text);
|
||||||
throw new Error('Création expense échouée.');
|
// throw new Error('Création expense échouée.');
|
||||||
}
|
// }
|
||||||
expenseId = expenseRes.body.id;
|
// expenseId = expenseRes.body.id;
|
||||||
});
|
// });
|
||||||
|
|
||||||
afterAll(async () => {
|
// afterAll(async () => {
|
||||||
await app.close();
|
// await app.close();
|
||||||
await prisma.$disconnect();
|
// await prisma.$disconnect();
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => {
|
// it(`PATCH ${BASE}/:year/:periodNumber/approval → 200 (cascade approval)`, async () => {
|
||||||
const res = await supertest(app.getHttpServer())
|
// const res = await supertest(app.getHttpServer())
|
||||||
.patch(`${BASE}/${periodYear}/${periodNumber}/approval`)
|
// .patch(`${BASE}/${periodYear}/${periodNumber}/approval`)
|
||||||
.send(); // aucun body requis par ton contrôleur
|
// .send(); // aucun body requis par ton contrôleur
|
||||||
expect([200, 204]).toContain(res.status);
|
// expect([200, 204]).toContain(res.status);
|
||||||
if (res.body?.message) {
|
// if (res.body?.message) {
|
||||||
expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`);
|
// expect(String(res.body.message)).toContain(`${periodYear}-${periodNumber}`);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Vérifie cascade:
|
// // Vérifie cascade:
|
||||||
const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`);
|
// const tsCheck = await supertest(app.getHttpServer()).get(`/timesheets/${timesheetId}`);
|
||||||
expect(tsCheck.status).toBe(200);
|
// expect(tsCheck.status).toBe(200);
|
||||||
expect(tsCheck.body?.is_approved).toBe(true);
|
// expect(tsCheck.body?.is_approved).toBe(true);
|
||||||
|
|
||||||
const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`);
|
// const shiftCheck = await supertest(app.getHttpServer()).get(`/shifts/${shiftId}`);
|
||||||
expect(shiftCheck.status).toBe(200);
|
// expect(shiftCheck.status).toBe(200);
|
||||||
expect(shiftCheck.body?.is_approved).toBe(true);
|
// expect(shiftCheck.body?.is_approved).toBe(true);
|
||||||
|
|
||||||
const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`);
|
// const expCheck = await supertest(app.getHttpServer()).get(`/Expenses/${expenseId}`);
|
||||||
expect(expCheck.status).toBe(200);
|
// expect(expCheck.status).toBe(200);
|
||||||
expect(expCheck.body?.is_approved).toBe(true);
|
// expect(expCheck.body?.is_approved).toBe(true);
|
||||||
});
|
// });
|
||||||
|
|
||||||
it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => {
|
// it(`PATCH ${BASE}/2099/999/approval → 404 (period not found)`, async () => {
|
||||||
const bad = await supertest(app.getHttpServer())
|
// const bad = await supertest(app.getHttpServer())
|
||||||
.patch(`${BASE}/2099/999/approval`)
|
// .patch(`${BASE}/2099/999/approval`)
|
||||||
.send();
|
// .send();
|
||||||
expect(bad.status).toBe(404);
|
// expect(bad.status).toBe(404);
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user