panels-origin/api/payroll_test.ts
Cursor Agent 9c8b6ea19c
fix: devolver días L–S y celdas de asistencia en el paquete de nómina
La UI de Jornal ya tenía checkboxes por día, pero GET /v1/payroll/weeks
no enviaba days ni cells, así que las columnas L–S no se renderizaban
y solo se veía el total Días en cero.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 20:58:41 +00:00

392 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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,
weekDays,
} 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<Fixture> {
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<number> {
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<number> {
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");
assertEquals(weekDays("2026-08-24"), [
"2026-08-24",
"2026-08-25",
"2026-08-26",
"2026-08-27",
"2026-08-28",
"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);
});
});
type JornalCell = {
date: string;
present: boolean;
other_project_id: number | null;
other_project_name: string | null;
};
Deno.test("paquete de semana incluye días L–S y celdas de asistencia", 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,
});
await setAttendance(db, TENANT_ID, {
project_id: project2Id,
worker_id: workerId,
work_date: "2026-08-25",
present: true,
});
const bundle = await getWeekBundle(db, TENANT_ID, "2026-08-24");
assertEquals(bundle.days, [
"2026-08-24",
"2026-08-25",
"2026-08-26",
"2026-08-27",
"2026-08-28",
"2026-08-29",
]);
const obra = (bundle.sheets as Array<{
kind: string;
project_id: number;
lines: Array<{ worker_id: number; cells?: JornalCell[] }>;
}>).find((sheet) => sheet.kind === "obra" && Number(sheet.project_id) === projectId);
assertExists(obra);
const line = obra.lines.find((row) => Number(row.worker_id) === workerId);
assertExists(line);
assertEquals(line.cells?.length, 6);
assertEquals(line.cells?.[0], {
date: "2026-08-24",
present: true,
other_project_id: null,
other_project_name: null,
});
assertEquals(line.cells?.[1]?.present, false);
assertEquals(line.cells?.[1]?.other_project_id, project2Id);
assertEquals(line.cells?.[1]?.other_project_name, "Obra Sur");
});
});
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);
});
});