From e89b51bf43f221144a6786d1ad594ed40ff15a22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 8 Sep 2026 20:47:16 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20subir=20documentos=20sin=20fecha=20de=20?= =?UTF-8?q?emisi=C3=B3n=20ni=20vencimiento?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La vigencia de 3 meses para alta o reingreso cuenta desde la fecha de carga. Quien recibe el expediente revisa si el documento es válido. Co-authored-by: alberto.martinez --- api/document_validity.ts | 35 ++----- api/document_validity_test.ts | 39 +++++++- api/main.ts | 13 +-- db/core/changelog-master.xml | 1 + .../032-document-validity-from-upload.sql | 98 +++++++++++++++++++ .../components/padron/WorkerDocuments.vue | 46 ++------- web-panel/pages/index.vue | 2 +- web-panel/pages/padron/index.vue | 6 +- 8 files changed, 155 insertions(+), 85 deletions(-) create mode 100644 db/core/changesets/032-document-validity-from-upload.sql diff --git a/api/document_validity.ts b/api/document_validity.ts index 8ecfc39..4dbeddb 100644 --- a/api/document_validity.ts +++ b/api/document_validity.ts @@ -82,18 +82,10 @@ export function evaluateDocumentValidity( const issuedAt = doc.issued_at; const expiresAt = doc.expires_at; + const uploadedAt = doc.uploaded_at; const needFresh = freshnessRequired(ctx); if (policy.validity_mode === "expiry") { - if (policy.requires_expires_at && !expiresAt) { - return { - present: true, - valid: false, - validity_status: "missing_dates", - issued_at: issuedAt, - expires_at: expiresAt, - }; - } const exp = parseDay(expiresAt); const todayDate = parseDay(today); if (exp && todayDate && exp < todayDate) { @@ -115,20 +107,11 @@ export function evaluateDocumentValidity( } if (policy.validity_mode === "freshness") { - if (policy.requires_issued_at && !issuedAt) { - return { - present: true, - valid: false, - validity_status: "missing_dates", - issued_at: issuedAt, - expires_at: expiresAt, - }; - } - if (needFresh && issuedAt) { - const issued = parseDay(issuedAt); + if (needFresh) { + const uploaded = parseDay(uploadedAt); const days = policy.freshness_days ?? 90; const todayForFreshness = parseDay(today) ?? new Date(); - if (issued && daysBetween(issued, todayForFreshness) > days) { + if (uploaded && daysBetween(uploaded, todayForFreshness) > days) { return { present: true, valid: false, @@ -170,11 +153,11 @@ export type CatalogDocType = { /** Núcleo = required 1. El resto es catálogo: se sube si se tiene, no cuenta como faltante. */ export const DOCUMENT_TYPE_SEED: CatalogDocType[] = [ { code: "foto", label: "Foto", required: 1, category: "identidad", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, - { code: "ine", label: "INE", required: 1, category: "identidad", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 1 }, - { code: "curp", label: "Constancia CURP", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 1, requires_expires_at: 0 }, - { code: "nss", label: "Constancia NSS", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 1, requires_expires_at: 0 }, - { code: "rfc", label: "Constancia RFC", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 1, requires_expires_at: 0 }, - { code: "domicilio", label: "Comprobante de domicilio", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 1, requires_expires_at: 0 }, + { code: "ine", label: "INE", required: 1, category: "identidad", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, + { code: "curp", label: "Constancia CURP", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 0, requires_expires_at: 0 }, + { code: "nss", label: "Constancia NSS", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 0, requires_expires_at: 0 }, + { code: "rfc", label: "Constancia RFC", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 0, requires_expires_at: 0 }, + { code: "domicilio", label: "Comprobante de domicilio", required: 1, category: "identidad", validity_mode: "freshness", freshness_days: 90, requires_issued_at: 0, requires_expires_at: 0 }, { code: "alta_imss", label: "Alta IMSS", required: 0, category: "imss", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "baja_imss", label: "Baja IMSS", required: 0, category: "imss", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "contrato", label: "Contrato laboral", required: 0, category: "laboral", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, diff --git a/api/document_validity_test.ts b/api/document_validity_test.ts index 216bcff..b2649f7 100644 --- a/api/document_validity_test.ts +++ b/api/document_validity_test.ts @@ -15,7 +15,7 @@ const curpPolicy: DocTypePolicy = { required: true, validity_mode: "freshness", freshness_days: 90, - requires_issued_at: true, + requires_issued_at: false, requires_expires_at: false, }; @@ -26,7 +26,7 @@ const inePolicy: DocTypePolicy = { validity_mode: "expiry", freshness_days: null, requires_issued_at: false, - requires_expires_at: true, + requires_expires_at: false, }; Deno.test("freshnessRequired: sin alta exige frescor", () => { @@ -69,7 +69,7 @@ Deno.test("freshnessRequired: rehire con alta posterior no exige frescor", () => ); }); -Deno.test("CURP stale when freshness required", () => { +Deno.test("CURP stale when freshness required and upload is older than 90 days", () => { const ctx: WorkerImssContext = { imss_status: "sin_alta", last_rehire_at: null, @@ -85,6 +85,22 @@ Deno.test("CURP stale when freshness required", () => { assertEquals(result.valid, false); }); +Deno.test("CURP recent upload is fresh even if issued_at is old", () => { + const ctx: WorkerImssContext = { + imss_status: "sin_alta", + last_rehire_at: null, + current_alta_at: null, + }; + const result = evaluateDocumentValidity( + curpPolicy, + { type_code: "curp", issued_at: "2024-01-01", expires_at: null, uploaded_at: "2026-08-01" }, + ctx, + "2026-08-20", + ); + assertEquals(result.validity_status, "ok"); + assertEquals(result.valid, true); +}); + Deno.test("CURP old but ok when already alta", () => { const ctx: WorkerImssContext = { imss_status: "alta", @@ -117,13 +133,26 @@ Deno.test("INE expired always invalid", () => { assertEquals(result.valid, false); }); -Deno.test("missing issued_at for freshness", () => { +Deno.test("freshness without issued_at uses uploaded_at", () => { 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 }, + "2026-08-20", ); - assertEquals(result.validity_status, "missing_dates"); + assertEquals(result.validity_status, "ok"); + assertEquals(result.valid, true); +}); + +Deno.test("INE without expires_at is valid; reviewer checks the file", () => { + const result = evaluateDocumentValidity( + inePolicy, + { type_code: "ine", issued_at: null, expires_at: null, uploaded_at: "2026-08-01" }, + { imss_status: "sin_alta", last_rehire_at: null, current_alta_at: null }, + "2026-08-20", + ); + assertEquals(result.validity_status, "ok"); + assertEquals(result.valid, true); }); Deno.test("núcleo de persona no se agranda", () => { diff --git a/api/main.ts b/api/main.ts index b400b4f..98c8959 100644 --- a/api/main.ts +++ b/api/main.ts @@ -1174,11 +1174,7 @@ app.post("/v1/workers/:id/documents", ...requireCoreAuth, requirePermission("doc {}, { route }, ); - const policy = (catEnv.data?.document_types ?? []).find((t) => String(t.code) === type) as { - validity_mode: string; - requires_issued_at: boolean; - requires_expires_at: boolean; - } | undefined; + const policy = (catEnv.data?.document_types ?? []).find((t) => String(t.code) === type); if (!policy) { return respondApiError(c, "VALIDATION", `Tipo de documento no válido en ${route}: ${type}`, { route, @@ -1192,13 +1188,6 @@ app.post("/v1/workers/:id/documents", ...requireCoreAuth, requirePermission("doc const imssCompanyId = Number(form.get("imss_company_id") || 0) || null; const imssAltaAt = String(form.get("imss_alta_at") || "").trim() || null; const imssBajaAt = String(form.get("imss_baja_at") || "").trim() || null; - - if (policy.requires_issued_at && !issuedAt) { - return respondApiError(c, "VALIDATION", `Indique la fecha de emisión del documento en ${route}`, { route, worker_id: id }); - } - if (policy.requires_expires_at && !expiresAt) { - return respondApiError(c, "VALIDATION", `Indique la fecha de vigencia / vencimiento en ${route}`, { route, worker_id: id }); - } if (type === "alta_imss" && !imssCompanyId) { return respondApiError(c, "VALIDATION", `Seleccione la empresa patrón del alta IMSS en ${route}`, { route, worker_id: id }); } diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml index ecb60c8..c02820f 100644 --- a/db/core/changelog-master.xml +++ b/db/core/changelog-master.xml @@ -38,5 +38,6 @@ + diff --git a/db/core/changesets/032-document-validity-from-upload.sql b/db/core/changesets/032-document-validity-from-upload.sql new file mode 100644 index 0000000..6fcecd8 --- /dev/null +++ b/db/core/changesets/032-document-validity-from-upload.sql @@ -0,0 +1,98 @@ +--liquibase formatted sql +-- PANELS · core · vigencia documental desde la fecha de carga + +--changeset panel:core-032a-doc-types-no-required-dates endDelimiter:; splitStatements:true +UPDATE core.document_types + SET requires_issued_at = false, + requires_expires_at = false; + +--changeset panel:core-032b-fn-worker-eval-doc-validity splitStatements:false +CREATE OR REPLACE FUNCTION core._worker_eval_doc_validity( + p_policy jsonb, + p_doc jsonb, + p_ctx jsonb, + p_today date +) +RETURNS jsonb +LANGUAGE plpgsql +IMMUTABLE +AS $$ +DECLARE + v_mode text := COALESCE(p_policy->>'validity_mode', 'none'); + v_issued date; + v_expires date; + v_uploaded date; + v_need_fresh boolean; + v_days integer; +BEGIN + IF p_doc IS NULL THEN + RETURN jsonb_build_object( + 'present', false, + 'valid', false, + 'validity_status', 'missing', + 'issued_at', NULL, + 'expires_at', NULL + ); + END IF; + v_issued := NULLIF(p_doc->>'issued_at', '')::date; + v_expires := NULLIF(p_doc->>'expires_at', '')::date; + BEGIN + v_uploaded := COALESCE( + NULLIF(left(p_doc->>'uploaded_at', 10), '')::date, + NULL + ); + EXCEPTION WHEN OTHERS THEN + v_uploaded := NULL; + END; + v_need_fresh := core._worker_freshness_required(p_ctx); + IF v_mode = 'expiry' THEN + IF v_expires IS NOT NULL AND v_expires < p_today THEN + RETURN jsonb_build_object( + 'present', true, + 'valid', false, + 'validity_status', 'expired', + 'issued_at', v_issued, + 'expires_at', v_expires + ); + END IF; + RETURN jsonb_build_object( + 'present', true, + 'valid', true, + 'validity_status', 'ok', + 'issued_at', v_issued, + 'expires_at', v_expires + ); + END IF; + IF v_mode = 'freshness' THEN + IF v_need_fresh AND v_uploaded IS NOT NULL THEN + v_days := COALESCE((p_policy->>'freshness_days')::integer, 90); + IF (p_today - v_uploaded) > v_days THEN + RETURN jsonb_build_object( + 'present', true, + 'valid', false, + 'validity_status', 'stale_for_alta', + 'issued_at', v_issued, + 'expires_at', v_expires + ); + END IF; + END IF; + RETURN jsonb_build_object( + 'present', true, + 'valid', true, + 'validity_status', 'ok', + 'issued_at', v_issued, + 'expires_at', v_expires + ); + END IF; + RETURN jsonb_build_object( + 'present', true, + 'valid', true, + 'validity_status', 'ok', + 'issued_at', v_issued, + 'expires_at', v_expires + ); +END; +$$; + +--changeset panel:core-032c-grants endDelimiter:; splitStatements:true +GRANT EXECUTE ON FUNCTION core._worker_eval_doc_validity(jsonb, jsonb, jsonb, date) TO panels_core_app; diff --git a/web-panel/components/padron/WorkerDocuments.vue b/web-panel/components/padron/WorkerDocuments.vue index 4d06eab..0de7098 100644 --- a/web-panel/components/padron/WorkerDocuments.vue +++ b/web-panel/components/padron/WorkerDocuments.vue @@ -1,7 +1,7 @@