import { assertEquals, assertExists, assertRejects } from "jsr:@std/assert@1"; import type { PgDb } from "./pg.ts"; import { withTestDb } from "./test_helpers.ts"; import { addDays, assembleWeek, canAssembleWeek, commissionAmount, createDestajoJob, createLoan, destajoPeriodBounds, ensureWeek, firstDueForLoan, getWeekBundle, jobRemainder, nextSaturdayAfterThisWeek, patchDestajoCut, payWeek, roundMoney, setAttendance, thisSaturday, weekAssembleDate, weekContaining, } from "./payroll.ts"; // tenant_id fijo y solo-de-tests: cada test corre en su propia transacción // que siempre se revierte (ver test_helpers.ts), así que reusar el mismo // id entre tests nunca colisiona -- nada de esto llega a persistir. const TENANT_ID = 999001; type Fixture = { companyId: number; projectId: number; workerId: number }; async function seedFixture(db: PgDb, opts: { workType?: "N" | "D"; dailyWage?: number } = {}): Promise { await db.prepare( `INSERT INTO companies (code, name, kind, tenant_id) VALUES ('TSTCO', 'Test Co', 'principal', ?)`, ).run(TENANT_ID); const companyId = await db.lastInsertId(); await db.prepare( `INSERT INTO projects (code, name, status, theme_id, company_id, tenant_id) VALUES ('TST-0001', 'Obra Norte', 'activo', 'arctec-dos-logos-fold', ?, ?)`, ).run(companyId, TENANT_ID); const projectId = await db.lastInsertId(); await db.prepare( `INSERT INTO workers (first_name, last_name_p, last_name_m, curp, rfc, nss, phone, email, address, hire_type, company_id, position, risk_code, work_type, daily_wage, tenant_id) VALUES ('Juan', 'Perez', 'Lopez', 'CURPTEST0001', 'RFCTEST0001', 'NSSTEST0001', '9990000001', 'juan@example.com', 'Sin domicilio', 'TSTCO', ?, 'Ayudante', 'rojo', ?, ?, ?)`, ).run(companyId, opts.workType ?? "N", opts.dailyWage ?? 500, TENANT_ID); const workerId = await db.lastInsertId(); await db.prepare( `INSERT INTO assignments (worker_id, project_id, active, start_date) VALUES (?, ?, true, current_date)`, ).run(workerId, projectId); return { companyId, projectId, workerId }; } async function addSecondProject(db: PgDb, companyId: number): Promise { await db.prepare( `INSERT INTO projects (code, name, status, theme_id, company_id, tenant_id) VALUES ('TST-0002', 'Obra Sur', 'activo', 'arctec-dos-logos-fold', ?, ?)`, ).run(companyId, TENANT_ID); return await db.lastInsertId(); } async function addSecondWorker(db: PgDb, companyId: number): Promise { await db.prepare( `INSERT INTO workers (first_name, last_name_p, last_name_m, curp, rfc, nss, phone, email, address, hire_type, company_id, position, risk_code, work_type, daily_wage, tenant_id) VALUES ('Luis', 'Destajo', 'Lopez', 'CURPTEST0002', 'RFCTEST0002', 'NSSTEST0002', '9990000002', 'luis@example.com', 'Sin domicilio', 'TSTCO', ?, 'Ayudante', 'rojo', 'D', 0, ?)`, ).run(companyId, TENANT_ID); return await db.lastInsertId(); } Deno.test("semana lun-sáb y corte destajo jue-jue", () => { const w = weekContaining("2026-08-26"); assertEquals(w.weekStart, "2026-08-24"); assertEquals(w.weekEnd, "2026-08-29"); const d = destajoPeriodBounds("2026-08-24"); assertEquals(d.periodEnd, "2026-08-27"); assertEquals(d.periodStart, "2026-08-20"); }); Deno.test("armar jueves solo a partir del jueves de esa semana", async () => { assertEquals(weekAssembleDate("2026-08-24"), "2026-08-27"); assertEquals(canAssembleWeek("2026-08-24", "2026-08-26"), false); assertEquals(canAssembleWeek("2026-08-24", "2026-08-27"), true); assertEquals(canAssembleWeek("2026-08-24", "2026-08-29"), true); assertEquals(canAssembleWeek("2026-08-24", "2026-08-31"), true); assertEquals(canAssembleWeek("2026-08-31", "2026-08-26"), false); await withTestDb(async (db) => { await seedFixture(db); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assertRejects( () => assembleWeek(db, weekId, "2026-08-26"), Error, "jueves", ); const draft = await db.prepare("SELECT status FROM payroll_weeks WHERE id=?").get(weekId) as { status: string }; assertEquals(draft.status, "draft"); await assembleWeek(db, weekId, "2026-08-28"); const after = await db.prepare("SELECT status FROM payroll_weeks WHERE id=?").get(weekId) as { status: string }; assertEquals(after.status, "assembled"); }); }); Deno.test("comisión suma al saldo", () => { assertEquals(commissionAmount(500, 10), 50); assertEquals(roundMoney(500 + 50), 550); }); Deno.test("préstamo chico vence el sábado de la semana siguiente", () => { assertEquals(thisSaturday("2026-08-18"), "2026-08-22"); assertEquals(nextSaturdayAfterThisWeek("2026-08-18"), "2026-08-29"); assertEquals( firstDueForLoan({ delivered: 500, smallMax: 500, plan: "single", grantIso: "2026-08-18", weekAssembled: false, }), "2026-08-29", ); assertEquals( firstDueForLoan({ delivered: 2000, smallMax: 500, plan: "single", grantIso: "2026-08-18", weekAssembled: false, }), "2026-08-22", ); assertEquals( firstDueForLoan({ delivered: 2000, smallMax: 500, plan: "single", grantIso: "2026-08-21", weekAssembled: true, }), "2026-08-29", ); }); Deno.test("armar jueves prellena jue-vie-sáb y congela requerida", async () => { await withTestDb(async (db) => { const { projectId, workerId } = await seedFixture(db); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-24', true)", ).run(workerId, projectId); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-25', true)", ).run(workerId, projectId); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assembleWeek(db, weekId, "2026-08-27"); const week = await db.prepare("SELECT * FROM payroll_weeks WHERE id=?").get(weekId) as { status: string; required_net: number; payable_net: number; }; assertEquals(week.status, "assembled"); assertEquals(week.required_net, 2500); assertEquals(week.payable_net, 2500); const thu = await db.prepare( "SELECT present FROM attendance WHERE worker_id=? AND work_date='2026-08-27'", ).get(workerId) as { present: boolean }; assertEquals(thu.present, true); }); }); Deno.test("falta viernes baja a pagar y no toca requerida", async () => { await withTestDb(async (db) => { const { projectId, workerId } = await seedFixture(db); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-24', true)", ).run(workerId, projectId); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-25', true)", ).run(workerId, projectId); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assembleWeek(db, weekId, "2026-08-27"); const r = await setAttendance(db, TENANT_ID, { project_id: projectId, worker_id: workerId, work_date: "2026-08-28", present: false, }); assertEquals("ok" in r && r.ok, true); const week = await db.prepare("SELECT required_net, payable_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number; payable_net: number; }; assertEquals(week.required_net, 2500); assertEquals(week.payable_net, 2000); }); }); Deno.test("préstamo no baja saldo al armar; sí al pagar", async () => { await withTestDb(async (db) => { const { projectId, workerId } = await seedFixture(db); for (const day of ["2026-08-24", "2026-08-25", "2026-08-26"]) { await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, ?, true)", ).run(workerId, projectId, day); } await createLoan(db, TENANT_ID, { worker_id: workerId, delivered: 2000, commission_pct: 10, grantIso: "2026-08-24" }); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assembleWeek(db, weekId, "2026-08-27"); const before = await db.prepare("SELECT balance FROM loans WHERE worker_id=?").get(workerId) as { balance: number }; assertEquals(before.balance, 2200); const week = await db.prepare("SELECT required_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number }; assertEquals(week.required_net, 800); await payWeek(db, weekId, "2026-08-29"); const after = await db.prepare("SELECT balance FROM loans WHERE worker_id=?").get(workerId) as { balance: number }; assertEquals(after.balance, 0); }); }); Deno.test("tablero: recuperación de préstamos no es sobrante", async () => { await withTestDb(async (db) => { const { projectId, workerId } = await seedFixture(db); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-24', true)", ).run(workerId, projectId); await db.prepare( "INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (?, ?, '2026-08-25', true)", ).run(workerId, projectId); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assembleWeek(db, weekId, "2026-08-27"); await createLoan(db, TENANT_ID, { worker_id: workerId, delivered: 1000, commission_pct: 20, grantIso: "2026-08-24" }); const bundle = await getWeekBundle(db, TENANT_ID, "2026-08-24"); const board = bundle.board as { required_net: number; payable_net: number; loan_recovery: number; faltante: number; }; assertEquals(board.required_net, 2500); assertEquals(board.loan_recovery, 1200); assertEquals(board.payable_net, 1300); assertEquals(board.faltante, 0); }); }); Deno.test("un día no se marca en dos obras", async () => { await withTestDb(async (db) => { const { companyId, projectId, workerId } = await seedFixture(db); const project2Id = await addSecondProject(db, companyId); await setAttendance(db, TENANT_ID, { project_id: projectId, worker_id: workerId, work_date: "2026-08-24", present: true }); const blocked = await setAttendance(db, TENANT_ID, { project_id: project2Id, worker_id: workerId, work_date: "2026-08-24", present: true, }); assertEquals("error" in blocked, true); }); }); Deno.test("préstamo de personal: el día en otra obra no se prellena aquí", async () => { await withTestDb(async (db) => { const { companyId, projectId, workerId } = await seedFixture(db); const project2Id = await addSecondProject(db, companyId); await setAttendance(db, TENANT_ID, { project_id: project2Id, worker_id: workerId, work_date: "2026-08-27", present: true }); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); await assembleWeek(db, weekId, "2026-08-27"); const onA = await db.prepare( "SELECT present FROM attendance WHERE worker_id=? AND project_id=? AND work_date='2026-08-27'", ).get(workerId, projectId) as { present: boolean } | undefined; assertEquals(onA, undefined); const onB = await db.prepare( "SELECT present FROM attendance WHERE worker_id=? AND project_id=? AND work_date='2026-08-27'", ).get(workerId, project2Id) as { present: boolean }; assertEquals(onB.present, true); }); }); Deno.test("destajo: estimado del corte, realizado menor, remanente y extra viernes", async () => { await withTestDb(async (db) => { const { companyId, projectId, workerId } = await seedFixture(db); // Igual que el original: el jornalero de la fixture no debe seguir en // el roster de "obra", para que el requerido sea solo el destajo. await db.prepare("DELETE FROM assignments WHERE worker_id=?").run(workerId); const worker2Id = await addSecondWorker(db, companyId); const weekId = await ensureWeek(db, TENANT_ID, "2026-08-24"); const job = await createDestajoJob(db, TENANT_ID, weekId, { project_id: projectId, worker_id: worker2Id, concepto: "Muro tablaroca", unit_code: "m2", qty_total_estimated: 200, unit_price: 100, qty_planned: 80, }); await assembleWeek(db, weekId, "2026-08-27"); let week = await db.prepare("SELECT required_net, payable_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number; payable_net: number; }; assertEquals(week.required_net, 8000); const cut = await db.prepare("SELECT id FROM destajo_cut_lines WHERE job_id=?").get(job.id) as { id: number }; await patchDestajoCut(db, weekId, cut.id, { qty_actual: 50 }); week = await db.prepare("SELECT required_net, payable_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number; payable_net: number; }; assertEquals(week.required_net, 8000); assertEquals(week.payable_net, 5000); await patchDestajoCut(db, weekId, cut.id, { qty_extra: 10 }); week = await db.prepare("SELECT required_net, payable_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number; payable_net: number; }; assertEquals(week.payable_net, 6000); await payWeek(db, weekId, "2026-08-29"); assertEquals(await jobRemainder(db, job.id), 140); const nextId = await ensureWeek(db, TENANT_ID, addDays("2026-08-24", 7)); assertExists(nextId); const bundle = await getWeekBundle(db, TENANT_ID, "2026-08-31"); assertEquals((bundle.destajo_jobs as { remainder: number }[])[0].remainder, 140); }); });