diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml
index d7ec0df..216d163 100644
--- a/db/core/changelog-master.xml
+++ b/db/core/changelog-master.xml
@@ -50,5 +50,6 @@
+
diff --git a/db/core/changesets/041-fn-worker-document-store-replace.sql b/db/core/changesets/041-fn-worker-document-store-replace.sql
new file mode 100644
index 0000000..0f98976
--- /dev/null
+++ b/db/core/changesets/041-fn-worker-document-store-replace.sql
@@ -0,0 +1,153 @@
+--liquibase formatted sql
+-- PANELS · core · reemplazo de documento por tipo (siempre archiva versiones previas)
+
+--changeset panel:core-041a-fn-worker-document-store-replace splitStatements:false
+CREATE OR REPLACE FUNCTION core.fn_worker_document_store(payload jsonb)
+RETURNS jsonb
+LANGUAGE plpgsql
+SECURITY INVOKER
+SET search_path = core
+AS $$
+DECLARE
+ v_worker_id bigint := NULLIF(payload->>'worker_id', '')::bigint;
+ v_type_code text := btrim(COALESCE(payload->>'type_code', payload->>'type', ''));
+ v_original_name text := btrim(COALESCE(payload->>'original_name', payload->>'filename', ''));
+ v_mime text := btrim(COALESCE(payload->>'mime', ''));
+ v_size_bytes bigint := NULLIF(payload->>'size_bytes', '')::bigint;
+ v_sha256 text := btrim(COALESCE(payload->>'sha256', ''));
+ v_iv text := btrim(COALESCE(payload->>'iv', ''));
+ v_storage_name text := btrim(COALESCE(payload->>'storage_name', ''));
+ v_uploaded_by_id integer := NULLIF(payload->>'uploaded_by_id', '')::integer;
+ v_uploaded_by_name text := COALESCE(NULLIF(btrim(payload->>'uploaded_by_name'), ''), '');
+ v_issued_at date := NULLIF(btrim(payload->>'issued_at'), '')::date;
+ v_expires_at date := NULLIF(btrim(payload->>'expires_at'), '')::date;
+ v_imss_company_id integer := NULLIF(payload->>'imss_company_id', '')::integer;
+ v_imss_alta_at date := NULLIF(btrim(payload->>'imss_alta_at'), '')::date;
+ v_imss_baja_at date := NULLIF(btrim(payload->>'imss_baja_at'), '')::date;
+ v_today date := current_date;
+ v_movement_date date;
+ v_doc_id bigint;
+ v_company record;
+BEGIN
+ IF v_worker_id IS NULL THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ 'fn_worker_document_store: worker_id es obligatorio',
+ jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'worker_id')
+ );
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM workers WHERE id = v_worker_id) THEN
+ RETURN core.rpc_err(
+ 'NOT_FOUND',
+ format('fn_worker_document_store: trabajador id=%s no encontrado', v_worker_id),
+ jsonb_build_object('fn', 'fn_worker_document_store', 'worker_id', v_worker_id)
+ );
+ END IF;
+ IF v_type_code = '' THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ 'fn_worker_document_store: type_code es obligatorio',
+ jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'type_code')
+ );
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM document_types WHERE code = v_type_code) THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ format('fn_worker_document_store: tipo de documento no válido (%s)', v_type_code),
+ jsonb_build_object('fn', 'fn_worker_document_store', 'type_code', v_type_code)
+ );
+ END IF;
+ IF v_original_name = '' OR v_mime = '' OR v_sha256 = '' OR v_iv = '' OR v_storage_name = '' THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ 'fn_worker_document_store: original_name, mime, sha256, iv y storage_name son obligatorios tras la carga a almacenamiento',
+ jsonb_build_object('fn', 'fn_worker_document_store')
+ );
+ END IF;
+ IF v_size_bytes IS NULL OR v_size_bytes < 0 THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ 'fn_worker_document_store: size_bytes debe ser un entero no negativo',
+ jsonb_build_object('fn', 'fn_worker_document_store', 'field', 'size_bytes')
+ );
+ END IF;
+
+ v_imss_alta_at := COALESCE(v_imss_alta_at, CASE WHEN v_type_code = 'alta_imss' THEN v_today END);
+ v_imss_baja_at := COALESCE(v_imss_baja_at, CASE WHEN v_type_code = 'baja_imss' THEN v_today END);
+ v_movement_date := CASE WHEN v_type_code = 'baja_imss' THEN v_imss_baja_at ELSE v_imss_alta_at END;
+
+ UPDATE documents
+ SET is_current = false
+ WHERE worker_id = v_worker_id
+ AND type_code = v_type_code;
+
+ INSERT INTO documents (
+ worker_id, type_code, original_name, mime, size_bytes, sha256, iv, storage_name,
+ is_current, parse_status, issued_at, expires_at, imss_company_id, imss_alta_at,
+ uploaded_by_id, uploaded_by_name
+ ) VALUES (
+ v_worker_id, v_type_code, v_original_name, v_mime, v_size_bytes, v_sha256, v_iv, v_storage_name,
+ true, 'manual',
+ COALESCE(v_issued_at, v_movement_date),
+ v_expires_at,
+ v_imss_company_id,
+ v_movement_date,
+ v_uploaded_by_id,
+ v_uploaded_by_name
+ )
+ RETURNING id INTO v_doc_id;
+
+ IF v_type_code = 'alta_imss' THEN
+ v_imss_company_id := COALESCE(
+ v_imss_company_id,
+ (SELECT company_id FROM workers WHERE id = v_worker_id)
+ );
+ SELECT id, code INTO v_company FROM companies WHERE id = v_imss_company_id;
+ IF NOT FOUND THEN
+ RETURN core.rpc_err(
+ 'VALIDATION',
+ 'fn_worker_document_store: empresa patrón no válida para alta IMSS',
+ jsonb_build_object('fn', 'fn_worker_document_store', 'worker_id', v_worker_id)
+ );
+ END IF;
+ UPDATE workers SET
+ imss_status = 'alta',
+ imss_company_id = v_company.id,
+ imss_alta_at = v_imss_alta_at,
+ imss_baja_at = NULL,
+ company_id = v_company.id,
+ hire_type = v_company.code,
+ updated_at = now()
+ WHERE id = v_worker_id;
+ END IF;
+
+ IF v_type_code = 'baja_imss' THEN
+ UPDATE workers SET
+ imss_status = 'baja_imss',
+ imss_baja_at = v_imss_baja_at,
+ imss_company_id = NULL,
+ company_id = NULL,
+ hire_type = '',
+ updated_at = now()
+ WHERE id = v_worker_id;
+ END IF;
+
+ IF (SELECT status FROM workers WHERE id = v_worker_id) = 'baja' THEN
+ UPDATE workers SET pipeline_status = 'baja' WHERE id = v_worker_id;
+ END IF;
+
+ RETURN core.rpc_created(
+ jsonb_build_object(
+ 'id', v_doc_id,
+ 'worker_id', v_worker_id,
+ 'type_code', v_type_code,
+ 'is_current', true,
+ 'storage_name', v_storage_name
+ ),
+ format('Metadatos del documento %s guardados para trabajador %s (versión actual)', v_type_code, v_worker_id),
+ jsonb_build_object('fn', 'fn_worker_document_store', 'id', v_doc_id, 'worker_id', v_worker_id)
+ );
+EXCEPTION WHEN OTHERS THEN
+ RETURN core.rpc_from_exception('fn_worker_document_store', SQLSTATE, SQLERRM);
+END;
+$$;