mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 17:33:16 +00:00
Fase 2 (driver): - api/pg.ts: adaptador delgado sobre postgres.js (prepare/get/all/run, placeholders ? -> $n, withTenant con set_config para RLS), con parsers de tipo custom (numeric/date/timestamp(tz)/bigint) para que el resto del codigo heredado de SQLite (fechas/montos como string, ids como number) siga funcionando sin reescribir cada call-site a mano. - api/platform_db.ts, api/iam_db.ts (nuevo), api/db.ts: pools separados por base/esquema (panels_platform, panels_product.iam, panels_product.core), owner pool para bootstrap/scripts/lookups administrativos que cruzan tenant a proposito. - api/redis.ts: clientes iam/core separados (ACL panels_iam_redis / panels_core_redis). - api/sessions.ts + auth.ts: sesiones ahora en Redis (cookie = id opaco, no HMAC autocontenido); revocacion real (logout, cambio de password). - api/storage.ts (Fase 4c): documentos/PDFs via Contabo Object Storage (S3), con fallback a disco local si no hay credenciales S3 (dev). - api/scope.ts: middleware withCoreScope/requireCoreAuth que abre la transaccion con app.tenant_id fijado (RLS) para cada request. - api/cache.ts (Fase 4e): cache Redis con tenant_id obligatorio en la llave; aplicado a /v1/catalogs. Fase 3 (reescritura SQL, ~80 endpoints en main.ts/companies.ts/budget.ts/ payroll.ts/payroll_http.ts/excel.ts/saas.ts/smtp.ts): - Todo async/await, sintaxis Postgres (COALESCE, ~ regex, ON CONFLICT, now()/current_date, booleanos reales, RETURNING via lastInsertId()). - IDOR cross-tenant cerrado: GET/PATCH /v1/projects/:id, /v1/workers/:id ya no dependen de que el handler recuerde el WHERE tenant_id -- Row Level Security lo hace estructuralmente (verificado con un segundo tenant real: 404 en vez de fuga de datos). - API key ya no ve todos los tenants: ahora exige X-Tenant-Id explicito. Fase 3b (tests): api/test_helpers.ts corre cada test en una transaccion que siempre se revierte, contra el mismo baseline de Liquibase que produccion (ya no un esquema SQLite escrito a mano). payroll_test.ts reescrito con fixtures reales; 11/11 pasan contra Postgres. Fase 4 (IAM/RBAC): iam.roles/permissions/role_permissions formalizados (ver db/iam ya en fase 1); uploaded_by/created_by ahora son snapshot desnormalizado (uploaded_by_id/name); seed() en runtime eliminado, reemplazado por scripts/bootstrap-admin.ts (one-shot). Fase 4d (zona horaria): nuevo endpoint /v1/configuracion (GET/PUT), PAYROLL_TZ hardcodeado reemplazado por tenant_settings.timezone, document_validity.ts ya no usa new Date() crudo. Verificado end-to-end contra Postgres+Redis reales: login, sesiones, catalogos con cache, alta de trabajador, subida/descarga de documento cifrado, y el fix de IDOR probado con un segundo tenant real (403/404 en vez de fuga de datos). Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts";
|
|
import {
|
|
evaluateDocumentValidity,
|
|
freshnessRequired,
|
|
DOCUMENT_TYPE_SEED,
|
|
PROJECT_DOCUMENT_TYPE_SEED,
|
|
COMPANY_DOCUMENT_TYPE_SEED,
|
|
type DocTypePolicy,
|
|
type WorkerImssContext,
|
|
} from "./document_validity.ts";
|
|
|
|
const curpPolicy: DocTypePolicy = {
|
|
code: "curp",
|
|
label: "CURP",
|
|
required: true,
|
|
validity_mode: "freshness",
|
|
freshness_days: 90,
|
|
requires_issued_at: true,
|
|
requires_expires_at: false,
|
|
};
|
|
|
|
const inePolicy: DocTypePolicy = {
|
|
code: "ine",
|
|
label: "INE",
|
|
required: true,
|
|
validity_mode: "expiry",
|
|
freshness_days: null,
|
|
requires_issued_at: false,
|
|
requires_expires_at: true,
|
|
};
|
|
|
|
Deno.test("freshnessRequired: sin alta exige frescor", () => {
|
|
assertEquals(
|
|
freshnessRequired({ imss_status: "sin_alta", last_rehire_at: null, current_alta_at: null }),
|
|
true,
|
|
);
|
|
});
|
|
|
|
Deno.test("freshnessRequired: alta continua no exige frescor", () => {
|
|
assertEquals(
|
|
freshnessRequired({
|
|
imss_status: "alta",
|
|
last_rehire_at: null,
|
|
current_alta_at: "2024-01-01",
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
Deno.test("freshnessRequired: rehire sin alta posterior exige frescor", () => {
|
|
assertEquals(
|
|
freshnessRequired({
|
|
imss_status: "alta",
|
|
last_rehire_at: "2026-06-01",
|
|
current_alta_at: "2025-01-01",
|
|
}),
|
|
true,
|
|
);
|
|
});
|
|
|
|
Deno.test("freshnessRequired: rehire con alta posterior no exige frescor", () => {
|
|
assertEquals(
|
|
freshnessRequired({
|
|
imss_status: "alta",
|
|
last_rehire_at: "2026-01-01",
|
|
current_alta_at: "2026-02-01",
|
|
}),
|
|
false,
|
|
);
|
|
});
|
|
|
|
Deno.test("CURP stale when freshness required", () => {
|
|
const ctx: WorkerImssContext = {
|
|
imss_status: "sin_alta",
|
|
last_rehire_at: null,
|
|
current_alta_at: null,
|
|
};
|
|
const result = evaluateDocumentValidity(
|
|
curpPolicy,
|
|
{ type_code: "curp", issued_at: "2025-01-01", expires_at: null, uploaded_at: "2025-01-02" },
|
|
ctx,
|
|
"2026-08-20",
|
|
);
|
|
assertEquals(result.validity_status, "stale_for_alta");
|
|
assertEquals(result.valid, false);
|
|
});
|
|
|
|
Deno.test("CURP old but ok when already alta", () => {
|
|
const ctx: WorkerImssContext = {
|
|
imss_status: "alta",
|
|
last_rehire_at: null,
|
|
current_alta_at: "2025-02-01",
|
|
};
|
|
const result = evaluateDocumentValidity(
|
|
curpPolicy,
|
|
{ type_code: "curp", issued_at: "2025-01-01", expires_at: null, uploaded_at: "2025-01-02" },
|
|
ctx,
|
|
"2026-08-20",
|
|
);
|
|
assertEquals(result.validity_status, "ok");
|
|
assertEquals(result.valid, true);
|
|
});
|
|
|
|
Deno.test("INE expired always invalid", () => {
|
|
const ctx: WorkerImssContext = {
|
|
imss_status: "alta",
|
|
last_rehire_at: null,
|
|
current_alta_at: "2026-01-01",
|
|
};
|
|
const result = evaluateDocumentValidity(
|
|
inePolicy,
|
|
{ type_code: "ine", issued_at: null, expires_at: "2025-12-31", uploaded_at: "2025-01-01" },
|
|
ctx,
|
|
"2026-08-20",
|
|
);
|
|
assertEquals(result.validity_status, "expired");
|
|
assertEquals(result.valid, false);
|
|
});
|
|
|
|
Deno.test("missing issued_at for freshness", () => {
|
|
const result = evaluateDocumentValidity(
|
|
curpPolicy,
|
|
{ type_code: "curp", issued_at: null, expires_at: null, uploaded_at: "2026-08-01" },
|
|
{ imss_status: "sin_alta", last_rehire_at: null, current_alta_at: null },
|
|
);
|
|
assertEquals(result.validity_status, "missing_dates");
|
|
});
|
|
|
|
Deno.test("núcleo de persona no se agranda", () => {
|
|
const required = DOCUMENT_TYPE_SEED.filter((d) => d.required).map((d) => d.code).sort();
|
|
assertEquals(required, ["curp", "domicilio", "foto", "ine", "nss", "rfc"]);
|
|
});
|
|
|
|
Deno.test("núcleo de obra no se agranda", () => {
|
|
const required = PROJECT_DOCUMENT_TYPE_SEED.filter((d) => d.required).map((d) => d.code).sort();
|
|
assertEquals(required, ["contrato", "presupuesto"]);
|
|
});
|
|
|
|
Deno.test("empresa no tiene documentos obligatorios", () => {
|
|
assertEquals(COMPANY_DOCUMENT_TYPE_SEED.every((d) => d.required === 0), true);
|
|
});
|