/** Vigencia documental y contexto de alta IMSS. */ export type ValidityMode = "none" | "freshness" | "expiry"; export type ValidityStatus = "ok" | "missing" | "expired" | "stale_for_alta" | "missing_dates"; export type ImssStatus = "sin_alta" | "alta" | "baja_imss"; export type DocTypePolicy = { code: string; label: string; required: boolean; validity_mode: ValidityMode; freshness_days: number | null; requires_issued_at: boolean; requires_expires_at: boolean; }; export type CurrentDocDates = { type_code: string; issued_at: string | null; expires_at: string | null; uploaded_at: string; imss_alta_at?: string | null; }; export type WorkerImssContext = { imss_status: ImssStatus; last_rehire_at: string | null; /** Fecha de la alta IMSS vigente (documento o worker.imss_alta_at). */ current_alta_at: string | null; }; function parseDay(value: string | null | undefined): Date | null { if (!value) return null; const raw = String(value).trim().slice(0, 10); if (!/^\d{4}-\d{2}-\d{2}$/.test(raw)) return null; const d = new Date(`${raw}T12:00:00`); return Number.isNaN(d.getTime()) ? null : d; } function daysBetween(from: Date, to: Date): number { return Math.floor((to.getTime() - from.getTime()) / 86_400_000); } /** True cuando el expediente debe exigir frescor (alta o reingreso pendiente de alta). */ export function freshnessRequired(ctx: WorkerImssContext): boolean { if (ctx.imss_status !== "alta") return true; if (!ctx.last_rehire_at) return false; const rehire = parseDay(ctx.last_rehire_at); const alta = parseDay(ctx.current_alta_at); if (!rehire) return false; if (!alta) return true; return alta < rehire; } /** * `today` es un string ISO (YYYY-MM-DD) en la zona horaria del tenant * (Fase 4d) -- ya no `new Date()` crudo (hora del servidor/UTC). El * caller (api/db.ts#checklistItemsFor) lo resuelve con * tenantTimezone()/todayInTimezone() antes de llegar aquí. */ export function evaluateDocumentValidity( policy: DocTypePolicy, doc: CurrentDocDates | null | undefined, ctx: WorkerImssContext, today: string = new Date().toISOString().slice(0, 10), ): { present: boolean; valid: boolean; validity_status: ValidityStatus; issued_at: string | null; expires_at: string | null; } { if (!doc) { return { present: false, valid: false, validity_status: "missing", issued_at: null, expires_at: null, }; } const issuedAt = doc.issued_at; const expiresAt = doc.expires_at; const uploadedAt = doc.uploaded_at; const needFresh = freshnessRequired(ctx); if (policy.validity_mode === "expiry") { const exp = parseDay(expiresAt); const todayDate = parseDay(today); if (exp && todayDate && exp < todayDate) { return { present: true, valid: false, validity_status: "expired", issued_at: issuedAt, expires_at: expiresAt, }; } return { present: true, valid: true, validity_status: "ok", issued_at: issuedAt, expires_at: expiresAt, }; } if (policy.validity_mode === "freshness") { if (needFresh) { const uploaded = parseDay(uploadedAt); const days = policy.freshness_days ?? 90; const todayForFreshness = parseDay(today) ?? new Date(); if (uploaded && daysBetween(uploaded, todayForFreshness) > days) { return { present: true, valid: false, validity_status: "stale_for_alta", issued_at: issuedAt, expires_at: expiresAt, }; } } return { present: true, valid: true, validity_status: "ok", issued_at: issuedAt, expires_at: expiresAt, }; } return { present: true, valid: true, validity_status: "ok", issued_at: issuedAt, expires_at: expiresAt, }; } export type CatalogDocType = { code: string; label: string; required: number; category: string; validity_mode: ValidityMode; freshness_days: number | null; requires_issued_at: number; requires_expires_at: number; }; /** 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: 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 }, { code: "aviso_privacidad", label: "Aviso de privacidad", required: 0, category: "laboral", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "dc3_nom031", label: "DC-3 NOM-031 (construcción)", required: 0, category: "sst", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "epp", label: "Acuse de entrega de EPP", required: 0, category: "sst", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "medico", label: "Certificado médico", required: 0, category: "sst", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "dc3_altura", label: "DC-3 trabajo en alturas", required: 0, category: "oficio", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "dc3_soldadura", label: "DC-3 soldadura", required: 0, category: "oficio", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "dc3_montacargas", label: "DC-3 montacargas", required: 0, category: "oficio", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "licencia", label: "Licencia de conducir", required: 0, category: "oficio", validity_mode: "expiry", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "cedula", label: "Cédula profesional", required: 0, category: "oficio", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, { code: "otro", label: "Otro", required: 0, category: "otro", validity_mode: "none", freshness_days: null, requires_issued_at: 0, requires_expires_at: 0 }, ]; export const PROJECT_DOCUMENT_TYPE_SEED: Array<{ code: string; label: string; required: number; category: string; }> = [ { code: "contrato", label: "Contrato", required: 1, category: "contrato" }, { code: "presupuesto", label: "Presupuesto", required: 1, category: "contrato" }, { code: "planos", label: "Planos", required: 0, category: "tecnico" }, { code: "licencia_construccion", label: "Licencia de construcción", required: 0, category: "permisos" }, { code: "uso_suelo", label: "Uso de suelo", required: 0, category: "permisos" }, { code: "proteccion_civil", label: "Anuencia de Protección Civil", required: 0, category: "permisos" }, { code: "mia", label: "MIA / resolutivo ambiental", required: 0, category: "ambiental" }, { code: "siroc_acuse", label: "Acuse SIROC", required: 0, category: "imss" }, { code: "programa_sst", label: "Programa de seguridad", required: 0, category: "sst" }, { code: "seguro_obra", label: "Seguro de obra", required: 0, category: "contrato" }, { code: "fianza", label: "Fianza", required: 0, category: "contrato" }, { code: "otro", label: "Otro", required: 0, category: "otro" }, ]; export const COMPANY_DOCUMENT_TYPE_SEED: Array<{ code: string; label: string; required: number; category: string; }> = [ { code: "repse_federal", label: "REPSE federal (STPS)", required: 0, category: "repse" }, { code: "repse_estatal", label: "REPSE estatal (SATQ)", required: 0, category: "repse" }, { code: "opinion_sat", label: "Opinión de cumplimiento SAT (32-D)", required: 0, category: "fiscal" }, { code: "opinion_imss", label: "Opinión de cumplimiento IMSS", required: 0, category: "fiscal" }, { code: "opinion_infonavit", label: "Opinión de cumplimiento INFONAVIT", required: 0, category: "fiscal" }, { code: "acta_constitutiva", label: "Acta constitutiva", required: 0, category: "legal" }, { code: "poder_notarial", label: "Poder notarial", required: 0, category: "legal" }, { code: "otro", label: "Otro", required: 0, category: "otro" }, ];