targo-backend/prisma/mock-seeds-scripts/12-expenses.ts
2025-10-14 13:28:42 -04:00

164 lines
5.8 KiB
TypeScript

import { NotFoundException } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// ====== Config ======
const WEEKS_BACK = 4; // 4 semaines avant + semaine courante
const INCLUDE_CURRENT = true; // inclure la semaine courante
const STEP_CENTS = 25; // montants en quarts de dollar (.00/.25/.50/.75)
// ====== Helpers dates (ancre DIMANCHE UTC) ======
function sundayOfThisWeekUTC(now = new Date()) {
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
const day = d.getUTCDay(); // 0=Dim, 1=Lun, ...
d.setUTCDate(d.getUTCDate() - day); // recule jusqu'au dimanche
d.setUTCHours(0, 0, 0, 0);
return d;
}
function sundayNWeeksBefore(sunday: Date, n: number) {
const d = new Date(sunday);
d.setUTCDate(d.getUTCDate() - n * 7);
return d;
}
// Génère L→V à partir du dimanche (Lundi = dimanche + 1)
function weekDatesMonToFriFromSunday(sunday: Date) {
return Array.from({ length: 5 }, (_, i) => {
const d = new Date(sunday);
d.setUTCDate(sunday.getUTCDate() + (i + 1)); // +1..+5
return d;
});
}
// ====== Helpers random / amount ======
function rndInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// String "xx.yy" à partir de cents entiers (pas de float binaire en DB)
function centsToAmountString(cents: number): string {
const sign = cents < 0 ? '-' : '';
const abs = Math.abs(cents);
const dollars = Math.floor(abs / 100);
const c = abs % 100;
return `${sign}${dollars}.${c.toString().padStart(2, '0')}`;
}
function to2(value: string): string {
return (Math.round(parseFloat(value) * 100) / 100).toFixed(2);
}
// Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus)
function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number {
const qmin = Math.ceil(minCents / step);
const qmax = Math.floor(maxCents / step);
const q = rndInt(qmin, qmax);
return q * step;
}
function rndAmount(minCents: number, maxCents: number): string {
return centsToAmountString(rndQuantizedCents(minCents, maxCents));
}
// ====== Lookup timesheet (AUCUNE création ici) ======
async function findTimesheet(employee_id: number, start_date: Date) {
return prisma.timesheets.findUnique({
where: { employee_id_start_date: { employee_id, start_date } },
select: { id: true },
});
}
async function main() {
// Codes d'EXPENSES (exemples)
const BANKS = ['G517', 'G503', 'G502', 'G202'] as const;
// Précharger les bank codes
const bcRows = await prisma.bankCodes.findMany({
where: { bank_code: { in: BANKS as unknown as string[] } },
select: { id: true, bank_code: true },
});
const bcMap = new Map(bcRows.map(c => [c.bank_code, c.id]));
for (const c of BANKS) {
if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`);
}
// Employés
const employees = await prisma.employees.findMany({ select: { id: true } });
if (!employees.length) {
console.warn('Aucun employé — rien à insérer.');
return;
}
// Fenêtre de semaines ancrées au DIMANCHE
const sundayThisWeek = sundayOfThisWeekUTC();
const sundays: Date[] = [];
if (INCLUDE_CURRENT) sundays.push(sundayThisWeek);
for (let n = 1; n <= WEEKS_BACK; n++) sundays.push(sundayNWeeksBefore(sundayThisWeek, n));
let created = 0;
for (const sunday of sundays) {
const weekDays = weekDatesMonToFriFromSunday(sunday); // L→V
const monday = weekDays[0];
const friday = weekDays[4];
for (const e of employees) {
// Utiliser le timesheet EXISTANT (ancré au DIMANCHE)
const ts = await findTimesheet(e.id, sunday);
if (!ts) throw new NotFoundException(`Timesheet manquant pour emp ${e.id} @ ${sunday.toISOString().slice(0,10)}`);
// Idempotence: si déjà au moins une expense L→V, on skip la semaine
const already = await prisma.expenses.findFirst({
where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } },
select: { id: true },
});
if (already) continue;
// 1 à 3 expenses (jours distincts)
const count = rndInt(1, 3);
const dayIndexes = [0, 1, 2, 3, 4].sort(() => Math.random() - 0.5).slice(0, count);
for (const idx of dayIndexes) {
const date = weekDays[idx];
const code = BANKS[rndInt(0, BANKS.length - 1)];
const bank_code_id = bcMap.get(code)!;
// Montants (cents) quantisés à 25¢ => aucun flottant binaire en DB
let amount: string = '0.00';
let mileage: string = '0.00';
switch (code) {
case 'G503': // kilométrage
mileage = to2(rndAmount(1000, 7500)); // 10.00 à 75.00
break;
case 'G502': // per_diem
amount = to2(rndAmount(1500, 3000)); // 15.00 à 30.00
break;
case 'G202': // on_call / prime de garde
amount = to2(rndAmount(2000, 15000)); // 20.00 à 150.00
break;
case 'G517': // expenses
default:
amount = to2(rndAmount(500, 5000)); // 5.00 à 50.00
break;
}
await prisma.expenses.create({
data: {
timesheet_id: ts.id,
bank_code_id,
date,
amount,
mileage,
attachment: null,
comment: `Expense ${code} (emp ${e.id})`,
is_approved: Math.random() < 0.65,
supervisor_comment: Math.random() < 0.25 ? 'OK' : null,
},
});
created++;
}
}
}
const total = await prisma.expenses.count();
console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (ancre dimanche, L→V, sem courante ${INCLUDE_CURRENT ? 'incluse' : 'exclue'} + ${WEEKS_BACK} précédentes)`);
}
main().finally(() => prisma.$disconnect());