import { assert, assertEquals, assertExists } from "jsr:@std/assert@1"; import type { PgDb } from "./pg.ts"; import { callCoreFn } from "./rpc.ts"; import { withTestDb, closeTestPool } from "./test_helpers.ts"; import { createPool, PgDb as PgDbClass } from "./pg.ts"; const TENANT_ID = 999002; type Fixture = { companyId: number; projectId: number; budgetItemId: number }; async function seedFixture(db: PgDb): Promise { await db.prepare( `INSERT INTO companies (code, name, kind, tenant_id) VALUES ('COSTCO', 'Cost 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 ('COST-0001', 'Obra Costos', 'activo', 'arctec-dos-logos-fold', ?, ?)`, ).run(companyId, TENANT_ID); const projectId = await db.lastInsertId(); await db.prepare( `INSERT INTO budget_chapters (project_id, code, name, sort_order) VALUES (?, 'CAP01', 'Capítulo prueba', 1)`, ).run(projectId); const chapterId = await db.lastInsertId(); await db.prepare( `INSERT INTO budget_items (project_id, chapter_id, code, description, unit, quantity, unit_price, amount) VALUES (?, ?, 'ITM01', 'Concepto prueba', 'PZA', 100, 50, 5000)`, ).run(projectId, chapterId); const budgetItemId = await db.lastInsertId(); return { companyId, projectId, budgetItemId }; } function dataId(env: { ok: boolean; data?: Record }, key = "id"): number { assert(env.ok, `RPC falló: ${JSON.stringify(env)}`); const data = env.data as Record; const nested = data.expense ?? data.warehouse ?? data; const id = (nested as Record)[key] ?? data[key]; assertExists(id); return Number(id); } Deno.test("RPC gastos: CRUD completo", async () => { await withTestDb(async (db) => { const fx = await seedFixture(db); const base = { tenant_id: TENANT_ID }; const created = await callCoreFn(db, "core.fn_expense_create", { ...base, company_id: fx.companyId, project_id: fx.projectId, budget_item_id: fx.budgetItemId, description: "Gasto prueba RPC", quantity: 2, unit: "PZA", unit_price: 100, subtotal: 200, total: 232, tax_included: false, is_deductible: true, counts_toward_budget: true, created_by_id: 1, created_by_name: "Tester", }); assertEquals(created.ok, true); assertEquals(created.code, "CREATED"); const expenseId = dataId(created as { ok: boolean; data: Record }); const listed = await callCoreFn(db, "core.fn_expense_list", { ...base, project_id: fx.projectId }); assertEquals(listed.ok, true); const expenses = (listed.data as { expenses: unknown[] }).expenses; assert(expenses.length >= 1); const got = await callCoreFn(db, "core.fn_expense_get", { id: expenseId }); assertEquals(got.ok, true); const updated = await callCoreFn(db, "core.fn_expense_update", { id: expenseId, tenant_id: TENANT_ID, description: "Gasto editado", notes: "nota", }); assertEquals(updated.ok, true); const iva = await callCoreFn(db, "core.fn_iva_period_summary", { ...base, company_id: fx.companyId, year: new Date().getFullYear(), month: new Date().getMonth() + 1, }); assertEquals(iva.ok, true); const settings = await callCoreFn(db, "core.fn_cost_settings_get", base); assertEquals(settings.ok, true); const saved = await callCoreFn(db, "core.fn_cost_settings_save", { ...base, budget_warn_pct: 80, budget_critical_pct: 95, default_iva_rate: 0.16, }); assertEquals(saved.ok, true); const att = await callCoreFn(db, "core.fn_expense_attachment_add", { expense_id: expenseId, original_name: "factura.pdf", mime: "application/pdf", size_bytes: 100, sha256: "", iv: "00", storage_name: "test.pdf", uploaded_by_id: 1, uploaded_by_name: "Tester", }); assertEquals(att.ok, true); const voided = await callCoreFn(db, "core.fn_expense_void", { id: expenseId }); assertEquals(voided.ok, true); }); }); Deno.test("RPC almacén: central, obra, entrada, transferencia, salida", async () => { await withTestDb(async (db) => { const fx = await seedFixture(db); const base = { tenant_id: TENANT_ID }; const central = await callCoreFn(db, "core.fn_warehouse_ensure_central", base); assertEquals(central.ok, true); const centralId = dataId(central as { ok: boolean; data: Record }); const projectWh = await callCoreFn(db, "core.fn_warehouse_open_project", { ...base, project_id: fx.projectId, }); assertEquals(projectWh.ok, true); const projectWhId = dataId(projectWh as { ok: boolean; data: Record }); const listed = await callCoreFn(db, "core.fn_warehouse_list", base); assertEquals(listed.ok, true); const mat = await callCoreFn(db, "core.fn_warehouse_material_upsert", { ...base, code: "CEM001", name: "Cemento", unit: "BTO", }); assertEquals(mat.ok, true); const materialId = dataId(mat as { ok: boolean; data: Record }); const entry = await callCoreFn(db, "core.fn_warehouse_entry_create", { ...base, warehouse_id: centralId, material_id: materialId, quantity: 50, unit_cost: 120, created_by_id: 1, created_by_name: "Tester", }); assertEquals(entry.ok, true); assertEquals(entry.code, "CREATED"); const stockCentral = await callCoreFn(db, "core.fn_warehouse_stock_list", { ...base, warehouse_id: centralId, global: false, }); assertEquals(stockCentral.ok, true); const transfer = await callCoreFn(db, "core.fn_warehouse_transfer_create", { ...base, from_warehouse_id: centralId, to_warehouse_id: projectWhId, material_id: materialId, quantity: 20, created_by_name: "Tester", }); assertEquals(transfer.ok, true); const exit = await callCoreFn(db, "core.fn_warehouse_exit_create", { ...base, warehouse_id: projectWhId, material_id: materialId, quantity: 5, budget_item_id: fx.budgetItemId, destination_note: "Uso en obra", created_by_id: 1, created_by_name: "Tester", }); assertEquals(exit.ok, true); const movements = await callCoreFn(db, "core.fn_warehouse_movement_list", { warehouse_id: projectWhId, }); assertEquals(movements.ok, true); const globalStock = await callCoreFn(db, "core.fn_warehouse_stock_list", { ...base, global: true, }); assertEquals(globalStock.ok, true); const consolidated = (globalStock.data as { consolidated: Array<{ material_id: number; qty_on_hand: number }> }).consolidated; const cem = consolidated.find((row) => Number(row.material_id) === materialId); assertExists(cem); assertEquals(Number(cem.qty_on_hand), 45); await db.prepare( `INSERT INTO projects (code, name, status, theme_id, company_id, tenant_id) VALUES ('COST-0002', 'Obra sin almacén', 'activo', 'arctec-dos-logos-fold', ?, ?)`, ).run(fx.companyId, TENANT_ID); const listedAgain = await callCoreFn(db, "core.fn_warehouse_list", base); const warehouses = (listedAgain.data as { warehouses: Array<{ kind: string; project_id?: number }> }).warehouses; assertEquals(warehouses.filter((w) => w.kind === "central").length, 1); assertEquals(warehouses.filter((w) => w.kind === "project").length, 1); const supplier = await callCoreFn(db, "core.fn_supplier_create", { tenant_id: TENANT_ID, name: "Cemex Test", }); assertEquals(supplier.ok, true); const supplierId = Number(((supplier.data as { supplier: { id: number } }).supplier).id); const units = await callCoreFn(db, "core.fn_measure_unit_list", base); assertEquals(units.ok, true); const bulto = ((units.data as { units: Array<{ id: number; code: string }> }).units) .find((u) => u.code === "bulto"); assertExists(bulto); const catalog = await callCoreFn(db, "core.fn_warehouse_material_upsert", { ...base, code: "VAR001", name: "Varilla 3/8", unit_id: bulto.id, supplier_ids: [supplierId], }); assertEquals(catalog.ok, true); const listedMats = await callCoreFn(db, "core.fn_warehouse_material_list", base); assertEquals(listedMats.ok, true); const mats = (listedMats.data as { materials: Array<{ code: string; suppliers: Array<{ id: number }> }> }).materials; const varilla = mats.find((m) => m.code === "VAR001"); assertExists(varilla); assertEquals(varilla.suppliers.length, 1); const closed = await callCoreFn(db, "core.fn_warehouse_close_project", { project_id: fx.projectId, transfer_remaining: true, closed_by_name: "Tester", close_notes: "Cierre de prueba", }); assertEquals(closed.ok, true); const afterClose = await callCoreFn(db, "core.fn_warehouse_list", base); const openProjectWh = ((afterClose.data as { warehouses: Array<{ kind: string; status: string }> }).warehouses) .filter((w) => w.kind === "project" && w.status === "open"); assertEquals(openProjectWh.length, 0); }); }); Deno.test("RPC control presupuestal: summary, items, chapters, deviations", async () => { await withTestDb(async (db) => { const fx = await seedFixture(db); const base = { tenant_id: TENANT_ID, project_id: fx.projectId }; await callCoreFn(db, "core.fn_expense_create", { ...base, company_id: fx.companyId, description: "Gasto para control", subtotal: 500, total: 580, counts_toward_budget: true, created_by_id: 1, created_by_name: "Tester", }); for (const fn of [ "core.fn_cost_control_project_summary", "core.fn_cost_control_by_item", "core.fn_cost_control_by_chapter", "core.fn_cost_control_top_deviations", ]) { const env = await callCoreFn(db, fn, base); assertEquals(env.ok, true, `${fn} debe responder ok`); } }); }); Deno.test("RPC catálogos: proveedores y unidades de medida", async () => { await withTestDb(async (db) => { const created = await callCoreFn(db, "core.fn_supplier_create", { tenant_id: TENANT_ID, name: "Home Depot", rfc: "HDM123456ABC", phone: "9980000000", }); assertEquals(created.ok, true, `crear proveedor: ${JSON.stringify(created)}`); const supplier = (created.data as { supplier: { id: number; rfc: string } }).supplier; assertEquals(supplier.rfc, "HDM123456ABC"); const listed = await callCoreFn(db, "core.fn_supplier_list", { tenant_id: TENANT_ID, status: "activo" }); assertEquals(listed.ok, true); const suppliers = (listed.data as { suppliers: { name: string }[] }).suppliers; assert(suppliers.some((s) => s.name === "Home Depot")); const units = await callCoreFn(db, "core.fn_measure_unit_list", { tenant_id: TENANT_ID }); assertEquals(units.ok, true); const unitRows = (units.data as { units: { code: string }[] }).units; assert(unitRows.some((u) => u.code === "bulto")); const extra = await callCoreFn(db, "core.fn_measure_unit_add", { tenant_id: TENANT_ID, code: "tinaco", label: "Tinaco", }); assertEquals(extra.ok, true); }); }); Deno.test("RPC IAM v2: roles y permisos por role_id", async () => { const url = Deno.env.get("DATABASE_URL_IAM"); if (!url) throw new Error("DATABASE_URL_IAM requerido"); const pool = createPool(url, { max: 2 }); const ROLLBACK = Symbol("test-rollback"); try { await pool.begin(async (tx) => { const db = new PgDbClass(tx as never); await tx`SELECT set_config('app.tenant_id', ${String(TENANT_ID)}, true)`; const users = await db.prepare(`SELECT iam.fn_user_list($1::jsonb) AS result`).get({ tenant_id: TENANT_ID, }) as { result: { ok: boolean } }; assertEquals(users.result.ok, true); const perms = await db.prepare(`SELECT iam.fn_permission_list($1::jsonb) AS result`).get({}) as { result: { ok: boolean; data: { permissions: unknown[] } }; }; assertEquals(perms.result.ok, true); assert(perms.result.data.permissions.length > 0); const roleRow = await db.prepare( `SELECT id FROM roles WHERE tenant_id = ? AND code = 'operativo' LIMIT 1`, ).get(TENANT_ID) as { id: number } | undefined; if (roleRow) { const roleGet = await db.prepare(`SELECT iam.fn_role_permissions_get($1::jsonb) AS result`).get({ role_id: roleRow.id, }) as { result: { ok: boolean } }; assertEquals(roleGet.result.ok, true); const roleSave = await db.prepare(`SELECT iam.fn_role_permissions_save($1::jsonb) AS result`).get({ role_id: roleRow.id, permissions: ["workers.view", "warehouse.view", "documents.view", "reports.view"], }) as { result: { ok: boolean } }; assertEquals(roleSave.result.ok, true); } throw ROLLBACK; }); } catch (e) { if (e !== ROLLBACK) throw e; } finally { await pool.end({ timeout: 1 }); } }); Deno.test({ name: "cleanup test pool", sanitizeResources: false, sanitizeOps: false, }, async () => { await closeTestPool(); });