Saltar al contenido principal

Importador genérico de registros

Pantalla: /dashboard/settings/import (frontend/src/sections/data-import/). Base: supabase/migrations/20261116000001_data_import_platform.sql. Manual de usuario: «Administración → Importar datos» en el manual.

Reparto de responsabilidades​

Navegador Server action (sesión) Postgres (SECURITY DEFINER)
parseSpreadsheet (papaparse / xlsx) → actions/data-import.ts → import_create_job
autoMap + buildRows del destino (TS) lotes de 200 filas import_apply_chunk(job, rows, dry_run)
runImport: lotes, reintento, cancelar ├─ import_guard
├─ import_target_contacts
└─ import_target_health_history
  • El navegador hace todo lo que es texto: decodificar (UTF-8 estricto, si no Windows-1252), mapear columnas, limpiar documentos, parsear fechas, horas y diagnósticos (lib/transforms.ts). Es TS puro con pruebas sobre casos del primer archivo real.
  • La base solo valida y persiste un payload ya tipado. No parsea texto libre.
  • Nada pasa por el backend NestJS. Allí todo corre con service-role, auth.uid() es NULL y user_has_permission() siempre da falso. Con la sesión del usuario, la auditoría y el created_by son de quien importa de verdad.

Autorización​

import_guard(tenant, target), en cada RPC salvo la configuración:

  1. Hay sesión: service-role se rechaza, porque created_by, la firma y la auditoría necesitan un usuario.
  2. import_tenant_settings.enabled para el tenant. Solo el superadmin lo escribe, con import_save_tenant_settings, desde la tarjeta de la ficha del tenant.
  3. core.import.run.
  4. El destino está en allowed_targets y el usuario tiene el permiso del destino: contact.write o health.records.manage, este último vía health_can.
  5. En import_apply_chunk y import_finish_job, el trabajo es del mismo usuario.

Las cinco tablas import_* tienen RLS activado y REVOKE ALL a anon y authenticated. Solo se accede por las RPC.

Idempotencia y prueba​

  • import_external_ids (tenant, target, external_key) hace de «ID externo» de Odoo. Claves: contact:<id>, patient:<n.º historia>, encounter:<documento|h:historia>|<fecha>|<hora>. Reimportar el mismo archivo da 0 creados.
  • Dry-run («Probar»): el destino se ejecuta entero dentro de un bloque que termina con RAISE ... SQLSTATE 'XIDRY', capturado. Se revierte todo; los conteos sobreviven en variables PL/pgSQL. No toca los contadores del trabajo.
  • Cada fila corre en su propia subtransacción. Una fila mala da un error de fila y el lote sigue.
  • Las incidencias (import_job_errors) guardan fila, campo, código y gravedad. Sin valores: el archivo es PHI.

Historias clínicas: qué se escribe​

Por fila se escribe un contacto (con health_patients, metadata.history_number) y una fila en clinical_encounters:

  • reason, findings y treatment.
  • diagnoses, normalizado con health_normalize_diagnoses y validado contra health_code_entries.
  • notes con los bloques rotulados.
  • metadata.import = {job_id, file, origin_ref}.
  • status: signed (con signed_at = occurred_at y signed_by = usuario vinculado al recurso) o draft, según target_config['health.history'].

El profesional sale de options.resource_id o, si no hay, del nombre de la columna, comparado como conjunto de palabras sin tildes. Se escribe una fila de health_audit por lote.

Añadir un destino nuevo​

  1. TS: frontend/src/sections/data-import/targets/<destino>.ts con fields (clave, tipo, grupo y alias de columna ya normalizados) y buildRows, que devuelve filas tipadas e incidencias. Regístralo en targets/registry.ts y en el tipo ImportTargetKey.
  2. SQL: una migración nueva con import_target_<destino>(tenant, job, rows) → jsonb, que es interna, sin GRANT a authenticated. Añade la clave a import_known_targets(), su permiso a import_target_permission() y la rama en import_apply_chunk.
  3. i18n: dataImport.targets.<destino>, dataImport.fields.* y los códigos nuevos en dataImport.issues.
  4. Pruebas: buildRows con filas reales anonimizadas y supabase/tests/data_import/, con el caso rojo antes que el verde.

Pruebas​

cd frontend && yarn test src/sections/data-import
cd supabase/tests/data_import && ./run.sh ../../migrations/20261116000001_data_import_platform.sql