mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 16:13:17 +00:00
Reutiliza el catálogo de proveedores y agrega carga de facturas, conceptos de la semana, préstamos editables, finiquitos, dispersión, historial de personal, reportes y jornal de cotización. Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
219 lines
11 KiB
TypeScript
219 lines
11 KiB
TypeScript
import { assert, assertEquals } from "jsr:@std/assert@1";
|
|
import { callCoreFn } from "./rpc.ts";
|
|
import { withTestDb } from "./test_helpers.ts";
|
|
import type { PgDb } from "./pg.ts";
|
|
|
|
const TENANT_ID = 999003;
|
|
|
|
async function seed(db: PgDb) {
|
|
await db.prepare(
|
|
`INSERT INTO companies (code, name, kind, tenant_id) VALUES ('PNDCO', 'Pend 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 ('PND-1', 'Obra Pend', 'activo', 'arctec-dos-logos-fold', ?, ?)`,
|
|
).run(companyId, TENANT_ID);
|
|
const projectId = await db.lastInsertId();
|
|
await db.prepare(
|
|
`INSERT INTO budget_items (project_id, code, description, unit, quantity, unit_price, amount)
|
|
VALUES (?, 'MAT-1', 'Cemento', 'bulto', 1, 100, 100)`,
|
|
).run(projectId);
|
|
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, status)
|
|
VALUES ('Ana', 'Lopez', 'Diaz', 'CURPPEND0001', 'RFCPEND0001', 'NSSPEND0001', '9991111111',
|
|
'ana@example.com', 'Calle 1', 'PNDCO', ?, 'Ayudante', 'rojo', 'N', 400, ?, 'activo')`,
|
|
).run(companyId, TENANT_ID);
|
|
const workerId = await db.lastInsertId();
|
|
await db.prepare(
|
|
`INSERT INTO assignments (worker_id, project_id, active, start_date) VALUES (?, ?, true, DATE '2026-09-21')`,
|
|
).run(workerId, projectId);
|
|
return { companyId, projectId, workerId };
|
|
}
|
|
|
|
Deno.test("proveedor: RFC duplicado e inactivo fuera del listado activo", async () => {
|
|
await withTestDb(async (db) => {
|
|
const created = await callCoreFn(db, "core.fn_supplier_create", {
|
|
tenant_id: TENANT_ID, name: "Aceros", rfc: "ACE123456AB1", regimen_fiscal: "601", postal_code: "97000",
|
|
});
|
|
assertEquals(created.ok, true, JSON.stringify(created));
|
|
const dup = await callCoreFn(db, "core.fn_supplier_create", {
|
|
tenant_id: TENANT_ID, name: "Otro", rfc: "ACE123456AB1",
|
|
});
|
|
assertEquals(dup.ok, false);
|
|
const id = (created.data as { supplier: { id: number } }).supplier.id;
|
|
const off = await callCoreFn(db, "core.fn_supplier_update", { tenant_id: TENANT_ID, id, status: "inactivo" });
|
|
assertEquals(off.ok, true);
|
|
const listed = await callCoreFn(db, "core.fn_supplier_list", { tenant_id: TENANT_ID, status: "activo", q: "aceros" });
|
|
const rows = (listed.data as { suppliers: { name: string }[] }).suppliers;
|
|
assert(!rows.some((s) => s.name === "Aceros"));
|
|
});
|
|
});
|
|
|
|
Deno.test("facturas: RFC conocido, desconocido, UUID repetido y un solo gasto", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
await callCoreFn(db, "core.fn_supplier_create", { tenant_id: TENANT_ID, name: "Aceros", rfc: "ACE123456AB1" });
|
|
const known = await callCoreFn(db, "core.fn_invoice_import", {
|
|
tenant_id: TENANT_ID,
|
|
company_id: fx.companyId,
|
|
rows: [{
|
|
direction: "recibida", rfc: "ACE123456AB1", supplier_name: "Aceros", uuid: "UUID-1",
|
|
folio: "10", invoice_date: "2026-09-01", description: "Cemento", amount: 100, iva: 16, total: 116,
|
|
project_code: "PND-1", item_code: "MAT-1",
|
|
}],
|
|
});
|
|
assertEquals(known.ok, true, JSON.stringify(known));
|
|
const again = await callCoreFn(db, "core.fn_invoice_import", {
|
|
tenant_id: TENANT_ID,
|
|
company_id: fx.companyId,
|
|
rows: [{
|
|
direction: "recibida", rfc: "ACE123456AB1", uuid: "UUID-1", invoice_date: "2026-09-01", amount: 100,
|
|
}],
|
|
});
|
|
assertEquals((again.data as { skipped: number }).skipped, 1);
|
|
const unknown = await callCoreFn(db, "core.fn_invoice_import", {
|
|
tenant_id: TENANT_ID,
|
|
company_id: fx.companyId,
|
|
rows: [{ direction: "recibida", rfc: "XXX123456AB1", uuid: "UUID-2", invoice_date: "2026-09-02", amount: 50, supplier_name: "Nuevo" }],
|
|
});
|
|
assertEquals((unknown.data as { review: number }).review, 1);
|
|
const expenses = await db.prepare(
|
|
`SELECT COUNT(*)::int AS n FROM expense_entries WHERE source = 'invoice' AND tenant_id = ?`,
|
|
).get(TENANT_ID) as { n: number };
|
|
assertEquals(expenses.n, 1);
|
|
});
|
|
});
|
|
|
|
Deno.test("nómina: bono, descuento, horas, festivo y semana pagada", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
const week = await callCoreFn(db, "core.fn_payroll_week_ensure", { tenant_id: TENANT_ID, week_start: "2026-09-21" });
|
|
assertEquals(week.ok, true, JSON.stringify(week));
|
|
const weekId = (week.data as { week_id: number }).week_id;
|
|
await callCoreFn(db, "core.fn_holiday_save", { tenant_id: TENANT_ID, holiday_date: "2026-09-21", name: "Puente" });
|
|
const bono = await callCoreFn(db, "core.fn_payroll_adjustment_save", {
|
|
tenant_id: TENANT_ID, week_id: weekId, worker_id: fx.workerId, project_id: fx.projectId,
|
|
kind: "bono", concept: "Bono", amount: 100,
|
|
});
|
|
assertEquals(bono.ok, true, JSON.stringify(bono));
|
|
const missing = await callCoreFn(db, "core.fn_payroll_adjustment_save", {
|
|
tenant_id: TENANT_ID, week_id: weekId, worker_id: fx.workerId, kind: "descuento", concept: "Falta", amount: 40,
|
|
});
|
|
assertEquals(missing.ok, false);
|
|
await callCoreFn(db, "core.fn_payroll_adjustment_save", {
|
|
tenant_id: TENANT_ID, week_id: weekId, worker_id: fx.workerId, project_id: fx.projectId,
|
|
kind: "descuento", concept: "Herramienta", amount: 40, note: "Perdió herramienta",
|
|
});
|
|
await callCoreFn(db, "core.fn_payroll_adjustment_save", {
|
|
tenant_id: TENANT_ID, week_id: weekId, worker_id: fx.workerId, project_id: fx.projectId,
|
|
kind: "horas", concept: "Extra", hours: 8,
|
|
});
|
|
const bundle = await callCoreFn(db, "core.fn_payroll_week_bundle", { week_id: weekId, tenant_id: TENANT_ID });
|
|
assertEquals(bundle.ok, true, JSON.stringify(bundle));
|
|
await db.prepare(`UPDATE payroll_weeks SET status = 'paid' WHERE id = ?`).run(weekId);
|
|
const blocked = await callCoreFn(db, "core.fn_payroll_adjustment_save", {
|
|
tenant_id: TENANT_ID, week_id: weekId, worker_id: fx.workerId, kind: "bono", concept: "Tarde", amount: 1,
|
|
});
|
|
assertEquals(blocked.ok, false);
|
|
});
|
|
});
|
|
|
|
Deno.test("préstamo: edición sin pagos y saldo con pagos", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
const created = await callCoreFn(db, "core.fn_loan_create", {
|
|
tenant_id: TENANT_ID, worker_id: fx.workerId, delivered: 1000, commission_enabled: false,
|
|
});
|
|
assertEquals(created.ok, true, JSON.stringify(created));
|
|
const id = (created.data as { id: number }).id;
|
|
const edited = await callCoreFn(db, "core.fn_loan_update", { id, delivered: 800, commission_pct: 0, note: "Corregido" });
|
|
assertEquals(edited.ok, true, JSON.stringify(edited));
|
|
const week = await callCoreFn(db, "core.fn_payroll_week_ensure", { tenant_id: TENANT_ID, week_start: "2026-09-21" });
|
|
const weekId = (week.data as { week_id: number }).week_id;
|
|
await db.prepare(`INSERT INTO loan_payments (loan_id, week_id, amount) VALUES (?, ?, 200)`).run(id, weekId);
|
|
const statement = await callCoreFn(db, "core.fn_loan_statement", { id });
|
|
const loan = (statement.data as { loan: { balance: number; delivered: number } }).loan;
|
|
assertEquals(Number(loan.delivered), 800);
|
|
assertEquals(Number(loan.balance), 600);
|
|
const locked = await callCoreFn(db, "core.fn_loan_update", { id, delivered: 50 });
|
|
const after = await callCoreFn(db, "core.fn_loan_statement", { id });
|
|
assertEquals(Number((after.data as { loan: { delivered: number } }).loan.delivered), 800);
|
|
assertEquals(locked.ok, true);
|
|
});
|
|
});
|
|
|
|
Deno.test("finiquito: el neto es líneas menos descuentos y préstamo", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
await callCoreFn(db, "core.fn_loan_create", {
|
|
tenant_id: TENANT_ID, worker_id: fx.workerId, delivered: 300, commission_enabled: false,
|
|
});
|
|
const saved = await callCoreFn(db, "core.fn_settlement_save", {
|
|
tenant_id: TENANT_ID, worker_id: fx.workerId, company_id: fx.companyId, project_id: fx.projectId,
|
|
kind: "finiquito", termination_date: "2026-09-24",
|
|
lines: [{ concept: "Sueldo", amount: 1000, note: "" }],
|
|
});
|
|
assertEquals(saved.ok, true, JSON.stringify(saved));
|
|
const id = (saved.data as { id: number }).id;
|
|
const got = await callCoreFn(db, "core.fn_settlement_get", { id });
|
|
const net = Number((got.data as { settlement: { net: number } }).settlement.net);
|
|
assertEquals(net, 700);
|
|
});
|
|
});
|
|
|
|
Deno.test("dispersión: definitiva no se corrige", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
const week = await callCoreFn(db, "core.fn_payroll_week_ensure", { tenant_id: TENANT_ID, week_start: "2026-09-21" });
|
|
const weekId = (week.data as { week_id: number }).week_id;
|
|
const opened = await callCoreFn(db, "core.fn_dispersion_get", { tenant_id: TENANT_ID, week_id: weekId });
|
|
const id = (opened.data as { dispersion: { id: number } }).dispersion.id;
|
|
await callCoreFn(db, "core.fn_dispersion_cut", { id });
|
|
const fin = await callCoreFn(db, "core.fn_dispersion_finalize", { id });
|
|
assertEquals(fin.ok, true);
|
|
const line = await db.prepare(`SELECT id FROM payroll_dispersion_lines WHERE dispersion_id = ? LIMIT 1`).get(id) as { id: number } | undefined;
|
|
if (line) {
|
|
const upd = await callCoreFn(db, "core.fn_dispersion_line_update", { line_id: line.id, amount: 1 });
|
|
assertEquals(upd.ok, false);
|
|
}
|
|
assert(fx.workerId > 0);
|
|
});
|
|
});
|
|
|
|
Deno.test("historial: alta, baja y reingreso", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
const alta = await callCoreFn(db, "core.fn_worker_movement_list", { worker_id: fx.workerId });
|
|
const kinds = (alta.data as { movements: { kind: string }[] }).movements.map((m) => m.kind);
|
|
assert(kinds.includes("alta"));
|
|
await callCoreFn(db, "core.fn_worker_set_pipeline", { id: fx.workerId, tenant_id: TENANT_ID, pipeline_status: "baja" });
|
|
await callCoreFn(db, "core.fn_worker_set_pipeline", { id: fx.workerId, tenant_id: TENANT_ID, pipeline_status: "activo" });
|
|
const later = await callCoreFn(db, "core.fn_worker_movement_list", { worker_id: fx.workerId });
|
|
const all = (later.data as { movements: { kind: string }[] }).movements.map((m) => m.kind);
|
|
assert(all.includes("baja"));
|
|
assert(all.includes("reingreso"));
|
|
});
|
|
});
|
|
|
|
Deno.test("reporte mensual e IMSS con jornal distinto", async () => {
|
|
await withTestDb(async (db) => {
|
|
const fx = await seed(db);
|
|
await callCoreFn(db, "core.fn_payroll_week_ensure", { tenant_id: TENANT_ID, week_start: "2026-09-07" });
|
|
await callCoreFn(db, "core.fn_payroll_week_ensure", { tenant_id: TENANT_ID, week_start: "2026-09-14" });
|
|
const report = await callCoreFn(db, "core.fn_payroll_report", {
|
|
tenant_id: TENANT_ID, date_from: "2026-09-01", date_to: "2026-09-30", worker_id: fx.workerId,
|
|
});
|
|
assertEquals(report.ok, true, JSON.stringify(report));
|
|
await callCoreFn(db, "core.fn_worker_imss_wage_set", { id: fx.workerId, imss_daily_wage: 250 });
|
|
const quote = await callCoreFn(db, "core.fn_imss_quote", {
|
|
tenant_id: TENANT_ID, worker_id: fx.workerId, date_from: "2026-09-01", date_to: "2026-09-30",
|
|
});
|
|
const row = (quote.data as { quotes: { daily_wage: number; imss_daily_wage: number; used_real_wage: boolean }[] }).quotes[0];
|
|
assertEquals(Number(row.daily_wage), 400);
|
|
assertEquals(Number(row.imss_daily_wage), 250);
|
|
assertEquals(row.used_real_wage, false);
|
|
});
|
|
});
|