import { assertEquals, assertExists } from "jsr:@std/assert@1"; import { Database } from "@db/sqlite"; import { addDays, assembleWeek, commissionAmount, createDestajoJob, createLoan, destajoPeriodBounds, ensureWeek, firstDueForLoan, getWeekBundle, jobRemainder, nextSaturdayAfterThisWeek, patchDestajoCut, payWeek, roundMoney, setAttendance, thisSaturday, weekContaining, } from "./payroll.ts"; function mem() { const db = new Database(":memory:"); db.exec("PRAGMA foreign_keys = ON"); db.exec(` CREATE TABLE companies (id INTEGER PRIMARY KEY, name TEXT, tenant_id INTEGER); CREATE TABLE projects ( id INTEGER PRIMARY KEY, code TEXT, name TEXT, status TEXT DEFAULT 'activo', tenant_id INTEGER ); CREATE TABLE workers ( id INTEGER PRIMARY KEY, first_name TEXT, last_name_p TEXT, position TEXT DEFAULT 'Ayudante', work_type TEXT DEFAULT 'N', daily_wage REAL DEFAULT 0, status TEXT DEFAULT 'activo', pipeline_status TEXT DEFAULT 'activo', tenant_id INTEGER ); CREATE TABLE assignments ( id INTEGER PRIMARY KEY, worker_id INTEGER, project_id INTEGER, active INTEGER DEFAULT 1 ); CREATE TABLE attendance ( id INTEGER PRIMARY KEY, worker_id INTEGER, project_id INTEGER, work_date TEXT, present INTEGER DEFAULT 1, UNIQUE (worker_id, project_id, work_date) ); CREATE TABLE loans ( id INTEGER PRIMARY KEY, worker_id INTEGER, amount REAL, delivered REAL DEFAULT 0, balance REAL, weekly_payment REAL DEFAULT 0, note TEXT, commission_pct REAL DEFAULT 0, commission_amount REAL DEFAULT 0, plan TEXT DEFAULT 'single', installments_n INTEGER DEFAULT 1, first_due TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE payroll_settings ( tenant_id INTEGER PRIMARY KEY, loan_commission_enabled INTEGER DEFAULT 1, loan_commission_pct REAL DEFAULT 10, loan_small_max REAL DEFAULT 500 ); CREATE TABLE payroll_weeks ( id INTEGER PRIMARY KEY, week_start TEXT, week_end TEXT, status TEXT DEFAULT 'draft', required_net REAL DEFAULT 0, payable_net REAL DEFAULT 0, assembled_at TEXT, paid_at TEXT, tenant_id INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE UNIQUE INDEX idx_weeks ON payroll_weeks(tenant_id, week_start); CREATE TABLE payroll_sheets ( id INTEGER PRIMARY KEY, week_id INTEGER, kind TEXT, project_id INTEGER ); CREATE UNIQUE INDEX idx_sheets ON payroll_sheets(week_id, kind, IFNULL(project_id, 0)); CREATE TABLE payroll_week_lines ( id INTEGER PRIMARY KEY, sheet_id INTEGER, worker_id INTEGER, destajo_cut_line_id INTEGER, project_id INTEGER, days REAL DEFAULT 0, daily_wage REAL DEFAULT 0, qty_planned REAL DEFAULT 0, qty_actual REAL DEFAULT 0, qty_extra REAL DEFAULT 0, unit_price REAL DEFAULT 0, unit_code TEXT, concepto TEXT, amount REAL DEFAULT 0, gross REAL DEFAULT 0, discounts REAL DEFAULT 0, loan_id INTEGER, loan_discount REAL DEFAULT 0, loan_label TEXT, required_net REAL DEFAULT 0, payable_net REAL DEFAULT 0 ); CREATE TABLE destajo_units ( id INTEGER PRIMARY KEY, tenant_id INTEGER, code TEXT, label TEXT, UNIQUE(tenant_id, code) ); CREATE TABLE destajo_periods ( id INTEGER PRIMARY KEY, period_start TEXT, period_end TEXT, week_id INTEGER, tenant_id INTEGER, UNIQUE(tenant_id, period_end) ); CREATE TABLE destajo_jobs ( id INTEGER PRIMARY KEY, project_id INTEGER, worker_id INTEGER, concepto TEXT, unit_code TEXT, qty_total_estimated REAL, unit_price REAL, status TEXT DEFAULT 'open', tenant_id INTEGER DEFAULT 0 ); CREATE TABLE destajo_cut_lines ( id INTEGER PRIMARY KEY, period_id INTEGER, job_id INTEGER, qty_planned REAL DEFAULT 0, qty_actual REAL DEFAULT 0, qty_extra REAL DEFAULT 0, UNIQUE(period_id, job_id) ); CREATE TABLE loan_payments ( id INTEGER PRIMARY KEY, loan_id INTEGER, week_id INTEGER, amount REAL, installment_n INTEGER, label TEXT, created_at TEXT DEFAULT (datetime('now')) ); `); db.prepare("INSERT INTO projects (id, code, name, status, tenant_id) VALUES (1, 'PRY-0001', 'Obra Norte', 'activo', 1)").run(); db.prepare( "INSERT INTO workers (id, first_name, last_name_p, work_type, daily_wage, tenant_id) VALUES (1, 'Juan', 'Perez', 'N', 500, 1)", ).run(); db.prepare("INSERT INTO assignments (worker_id, project_id, active) VALUES (1, 1, 1)").run(); return db; } 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("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", () => { const db = mem(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-24', 1)").run(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-25', 1)").run(); const weekId = ensureWeek(db, 1, "2026-08-24"); assembleWeek(db, weekId); const week = 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 = db.prepare( "SELECT present FROM attendance WHERE worker_id=1 AND work_date='2026-08-27'", ).get() as { present: number }; assertEquals(thu.present, 1); }); Deno.test("falta viernes baja a pagar y no toca requerida", () => { const db = mem(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-24', 1)").run(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-25', 1)").run(); const weekId = ensureWeek(db, 1, "2026-08-24"); assembleWeek(db, weekId); const r = setAttendance(db, 1, { project_id: 1, worker_id: 1, work_date: "2026-08-28", present: false }); assertEquals("ok" in r && r.ok, true); const week = 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", () => { const db = mem(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-24', 1)").run(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-25', 1)").run(); db.prepare("INSERT INTO attendance (worker_id, project_id, work_date, present) VALUES (1, 1, '2026-08-26', 1)").run(); createLoan(db, 1, { worker_id: 1, delivered: 2000, commission_pct: 10, grantIso: "2026-08-24" }); const weekId = ensureWeek(db, 1, "2026-08-24"); assembleWeek(db, weekId); const before = db.prepare("SELECT balance FROM loans WHERE worker_id=1").get() as { balance: number }; assertEquals(before.balance, 2200); const week = db.prepare("SELECT required_net FROM payroll_weeks WHERE id=?").get(weekId) as { required_net: number }; assertEquals(week.required_net, 800); payWeek(db, weekId); const after = db.prepare("SELECT balance FROM loans WHERE worker_id=1").get() as { balance: number }; assertEquals(after.balance, 0); }); Deno.test("un día no se marca en dos obras", () => { const db = mem(); db.prepare("INSERT INTO projects (id, code, name, status, tenant_id) VALUES (2, 'PRY-0002', 'Obra Sur', 'activo', 1)").run(); setAttendance(db, 1, { project_id: 1, worker_id: 1, work_date: "2026-08-24", present: true }); const blocked = setAttendance(db, 1, { project_id: 2, worker_id: 1, 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í", () => { const db = mem(); db.prepare("INSERT INTO projects (id, code, name, status, tenant_id) VALUES (2, 'PRY-0002', 'Obra Sur', 'activo', 1)").run(); setAttendance(db, 1, { project_id: 2, worker_id: 1, work_date: "2026-08-27", present: true }); const weekId = ensureWeek(db, 1, "2026-08-24"); assembleWeek(db, weekId); const onA = db.prepare( "SELECT present FROM attendance WHERE worker_id=1 AND project_id=1 AND work_date='2026-08-27'", ).get() as { present: number } | undefined; assertEquals(onA, undefined); const onB = db.prepare( "SELECT present FROM attendance WHERE worker_id=1 AND project_id=2 AND work_date='2026-08-27'", ).get() as { present: number }; assertEquals(onB.present, 1); }); Deno.test("destajo: estimado del corte, realizado menor, remanente y extra viernes", () => { const db = mem(); db.prepare("DELETE FROM assignments WHERE worker_id=1").run(); db.prepare( "INSERT INTO workers (id, first_name, last_name_p, work_type, daily_wage, tenant_id) VALUES (2, 'Luis', 'Destajo', 'D', 0, 1)", ).run(); const weekId = ensureWeek(db, 1, "2026-08-24"); const job = createDestajoJob(db, 1, weekId, { project_id: 1, worker_id: 2, concepto: "Muro tablaroca", unit_code: "m2", qty_total_estimated: 200, unit_price: 100, qty_planned: 80, }); assembleWeek(db, weekId); let week = 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 = db.prepare("SELECT id FROM destajo_cut_lines WHERE job_id=?").get(job.id) as { id: number }; patchDestajoCut(db, weekId, cut.id, { qty_actual: 50 }); week = 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); patchDestajoCut(db, weekId, cut.id, { qty_extra: 10 }); week = 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); payWeek(db, weekId); assertEquals(jobRemainder(db, job.id), 140); const nextId = ensureWeek(db, 1, addDays("2026-08-24", 7)); assertExists(nextId); const bundle = getWeekBundle(db, 1, "2026-08-31"); assertEquals((bundle.destajo_jobs as { remainder: number }[])[0].remainder, 140); });