From cf07a84e26e95f9087ebca91cb907fe06fcf2942 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 08:12:00 -0600 Subject: [PATCH] =?UTF-8?q?Migraci=C3=B3n=20desde=20erp-clinicas/database?= =?UTF-8?q?=20-=20Est=C3=A1ndar=20multi-repo=20v2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- .gitignore | 4 + HERENCIA-ERP-CORE.md | 222 +++++++ README.md | 84 ++- init/00-extensions.sql | 25 + init/01-create-schemas.sql | 15 + init/02-rls-functions.sql | 37 ++ init/03-clinical-tables.sql | 628 ++++++++++++++++++ init/04-seed-data.sql | 34 + schemas/01-clinica-core-schema-ddl.sql | 446 +++++++++++++ schemas/04-financial-ext-schema-ddl.sql | 148 +++++ schemas/05-hr-ext-fase8-schema-ddl.sql | 354 ++++++++++ schemas/06-inventory-ext-fase8-schema-ddl.sql | 189 ++++++ schemas/07-purchase-ext-fase8-schema-ddl.sql | 148 +++++ schemas/08-clinica-ext-fase8-schema-ddl.sql | 151 +++++ seeds/fase8/00-removal-strategies.sql | 11 + seeds/fase8/01-clinica-skills.sql | 103 +++ seeds/fase8/02-clinica-catalogos.sql | 83 +++ 17 files changed, 2680 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 HERENCIA-ERP-CORE.md create mode 100644 init/00-extensions.sql create mode 100644 init/01-create-schemas.sql create mode 100644 init/02-rls-functions.sql create mode 100644 init/03-clinical-tables.sql create mode 100644 init/04-seed-data.sql create mode 100644 schemas/01-clinica-core-schema-ddl.sql create mode 100644 schemas/04-financial-ext-schema-ddl.sql create mode 100644 schemas/05-hr-ext-fase8-schema-ddl.sql create mode 100644 schemas/06-inventory-ext-fase8-schema-ddl.sql create mode 100644 schemas/07-purchase-ext-fase8-schema-ddl.sql create mode 100644 schemas/08-clinica-ext-fase8-schema-ddl.sql create mode 100644 seeds/fase8/00-removal-strategies.sql create mode 100644 seeds/fase8/01-clinica-skills.sql create mode 100644 seeds/fase8/02-clinica-catalogos.sql diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1900a45 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +.env diff --git a/HERENCIA-ERP-CORE.md b/HERENCIA-ERP-CORE.md new file mode 100644 index 0000000..505bb9a --- /dev/null +++ b/HERENCIA-ERP-CORE.md @@ -0,0 +1,222 @@ +# Herencia de Base de Datos - ERP Core -> Clínicas + +**Fecha:** 2025-12-08 +**Versión:** 1.0 +**Vertical:** Clínicas +**Nivel:** 2B.2 + +--- + +## RESUMEN + +La vertical de Clínicas hereda los schemas base del ERP Core y extiende con schemas específicos del dominio de gestión médica y expediente clínico. + +**Ubicación DDL Core:** `apps/erp-core/database/ddl/` + +--- + +## ARQUITECTURA DE HERENCIA + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ERP CORE (Base) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ auth │ │ core │ │financial│ │inventory│ │ hr │ │ +│ │ 26 tbl │ │ 12 tbl │ │ 15 tbl │ │ 15 tbl │ │ 6 tbl │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ sales │ │analytics│ │ system │ │ crm │ │ +│ │ 6 tbl │ │ 5 tbl │ │ 10 tbl │ │ 5 tbl │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ TOTAL: ~100 tablas heredadas │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ HEREDA + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ CLÍNICAS (Extensiones) │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ medical │ │ appointments │ │ patients │ │ +│ │ (expediente) │ │ (citas) │ │ (pacientes) │ │ +│ └───────────────┘ └───────────────┘ └───────────────┘ │ +│ EXTENSIONES: ~35 tablas (planificadas) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## SCHEMAS HEREDADOS DEL CORE + +| Schema | Tablas | Uso en Clínicas | +|--------|--------|-----------------| +| `auth` | 26 | Autenticación, usuarios médicos | +| `core` | 12 | Partners (pacientes), catálogos | +| `financial` | 15 | Facturas de servicios médicos | +| `inventory` | 15 | Medicamentos, insumos | +| `hr` | 6 | Personal médico | +| `sales` | 6 | Servicios médicos | +| `crm` | 5 | Seguimiento de pacientes | +| `analytics` | 5 | Estadísticas médicas | +| `system` | 10 | Recordatorios, notificaciones | + +**Total heredado:** ~100 tablas + +--- + +## SCHEMAS ESPECÍFICOS DE CLÍNICAS (Planificados) + +### 1. Schema `patients` (estimado 10+ tablas) + +**Propósito:** Gestión de pacientes + +```sql +-- Tablas principales planificadas: +patients.patients -- Pacientes (extiende core.partners) +patients.patient_contacts -- Contactos de emergencia +patients.insurance_policies -- Pólizas de seguro +patients.medical_history -- Antecedentes médicos +patients.allergies -- Alergias +patients.family_history -- Antecedentes familiares +``` + +### 2. Schema `medical` (estimado 15+ tablas) + +**Propósito:** Expediente clínico electrónico + +```sql +-- Tablas principales planificadas: +medical.consultations -- Consultas médicas +medical.diagnoses -- Diagnósticos (CIE-10) +medical.prescriptions -- Recetas médicas +medical.prescription_lines -- Medicamentos recetados +medical.vital_signs -- Signos vitales +medical.lab_results -- Resultados de laboratorio +medical.imaging_studies -- Estudios de imagen +medical.clinical_notes -- Notas clínicas +medical.treatments -- Tratamientos +``` + +### 3. Schema `appointments` (estimado 10+ tablas) + +**Propósito:** Gestión de citas + +```sql +-- Tablas principales planificadas: +appointments.doctors -- Médicos +appointments.specialties -- Especialidades +appointments.doctor_schedules -- Horarios de médicos +appointments.consulting_rooms -- Consultorios +appointments.appointments -- Citas +appointments.appointment_types -- Tipos de cita +appointments.reminders -- Recordatorios +``` + +--- + +## SPECS DEL CORE APLICABLES + +**Documento detallado:** `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` + +### Correcciones de DDL Core (2025-12-08) + +El DDL del ERP-Core fue corregido para resolver FK inválidas: + +1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`) +2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`) +3. **category_stock_accounts**: FK corregida a `core.product_categories` +4. **product_categories**: ALTERs ahora apuntan a schema `core` + +### SPECS Obligatorias + +| Spec Core | Aplicación en Clínicas | SP | Estado | +|-----------|----------------------|----:|--------| +| SPEC-SISTEMA-SECUENCIAS | Foliado de expedientes y citas | 8 | ✅ DDL LISTO | +| SPEC-SEGURIDAD-API-KEYS-PERMISOS | Control de acceso a expedientes | 31 | ✅ DDL LISTO | +| SPEC-INTEGRACION-CALENDAR | Agenda de citas médicas | 8 | PENDIENTE | +| SPEC-RRHH-EVALUACIONES-SKILLS | Credenciales médicas | 26 | ✅ DDL LISTO | +| SPEC-MAIL-THREAD-TRACKING | Historial de comunicación | 13 | ✅ DDL LISTO | +| SPEC-WIZARD-TRANSIENT-MODEL | Wizards de receta y referencia | 8 | PENDIENTE | +| SPEC-FIRMA-ELECTRONICA-NOM151 | Firma de expedientes clínicos | 13 | PENDIENTE | +| SPEC-TWO-FACTOR-AUTHENTICATION | Seguridad de acceso | 13 | ✅ DDL LISTO | +| SPEC-OAUTH2-SOCIAL-LOGIN | Portal de pacientes | 8 | ✅ DDL LISTO | + +### SPECS Opcionales + +| Spec Core | Decisión | Razón | +|-----------|----------|-------| +| SPEC-VALORACION-INVENTARIO | EVALUAR | Solo si hay farmacia interna | +| SPEC-PRICING-RULES | EVALUAR | Para paquetes de servicios | +| SPEC-TAREAS-RECURRENTES | EVALUAR | Para citas periódicas | + +### SPECS No Aplican + +| Spec Core | Razón | +|-----------|-------| +| SPEC-PORTAL-PROVEEDORES | No hay compras complejas | +| SPEC-BLANKET-ORDERS | No aplica en servicios médicos | +| SPEC-INVENTARIOS-CICLICOS | Solo si hay farmacia grande | +| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | No hay proyectos de este tipo | + +### Cumplimiento Normativo + +| Norma | Descripción | SPECS Relacionadas | +|-------|-------------|-------------------| +| NOM-024-SSA3-2012 | Expediente clínico electrónico | SPEC-SEGURIDAD, SPEC-MAIL-THREAD | +| LFPDPPP | Protección de datos personales | SPEC-SEGURIDAD, SPEC-2FA | +| NOM-004-SSA3-2012 | Expediente clínico | SPEC-FIRMA-ELECTRONICA | + +--- + +## CUMPLIMIENTO NORMATIVO + +Este sistema debe cumplir con: + +| Norma | Descripción | Impacto | +|-------|-------------|---------| +| NOM-024-SSA3-2012 | Expediente clínico electrónico | Estructura de datos | +| LFPDPPP | Protección de datos personales | Seguridad y acceso | +| NOM-004-SSA3-2012 | Expediente clínico | Contenido mínimo | + +--- + +## ORDEN DE EJECUCIÓN DDL (Futuro) + +```bash +# PASO 1: Cargar ERP Core (base) +cd apps/erp-core/database +./scripts/reset-database.sh --force + +# PASO 2: Cargar extensiones de Clínicas +cd apps/verticales/clinicas/database +psql $DATABASE_URL -f init/00-extensions.sql +psql $DATABASE_URL -f init/01-create-schemas.sql +psql $DATABASE_URL -f init/02-patients-tables.sql +psql $DATABASE_URL -f init/03-medical-tables.sql +psql $DATABASE_URL -f init/04-appointments-tables.sql +``` + +--- + +## MAPEO DE NOMENCLATURA + +| Core | Clínicas | +|------|----------| +| `core.partners` | Pacientes base | +| `hr.employees` | Personal médico | +| `inventory.products` | Medicamentos, insumos | +| `sales.sale_orders` | Servicios médicos | +| `financial.invoices` | Facturas de consultas | + +--- + +## REFERENCIAS + +- ERP Core DDL: `apps/erp-core/database/ddl/` +- ERP Core README: `apps/erp-core/database/README.md` +- Directivas: `orchestration/directivas/` +- Inventarios: `orchestration/inventarios/` + +--- + +**Documento de herencia oficial** +**Última actualización:** 2025-12-08 diff --git a/README.md b/README.md index d9c3ebb..d42da1f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,83 @@ -# erp-clinicas-database-v2 +# Base de Datos - ERP Clínicas -Database de erp-clinicas - Workspace V2 \ No newline at end of file +## Resumen + +| Aspecto | Valor | +|---------|-------| +| **Schema principal** | `clinica` | +| **Tablas específicas** | 13 | +| **ENUMs** | 4 | +| **Hereda de ERP-Core** | 144 tablas (12 schemas) | + +## Prerequisitos + +1. **ERP-Core instalado** con todos sus schemas: + - auth, core, financial, inventory, purchase, sales, projects, analytics, system, billing, crm, hr + +2. **Extensiones PostgreSQL**: + - pgcrypto (encriptación) + - pg_trgm (búsqueda de texto) + +## Orden de Ejecución DDL + +```bash +# 1. Instalar ERP-Core primero +cd apps/erp-core/database +./scripts/reset-database.sh + +# 2. Instalar extensión Clínicas +cd apps/verticales/clinicas/database +psql $DATABASE_URL -f init/00-extensions.sql +psql $DATABASE_URL -f init/01-create-schemas.sql +psql $DATABASE_URL -f init/02-rls-functions.sql +psql $DATABASE_URL -f init/03-clinical-tables.sql +psql $DATABASE_URL -f init/04-seed-data.sql +``` + +## Tablas Implementadas + +### Schema: clinica (13 tablas) + +| Tabla | Módulo | Descripción | +|-------|--------|-------------| +| specialties | CL-002 | Catálogo de especialidades médicas | +| doctors | CL-002 | Médicos (extiende hr.employees) | +| patients | CL-001 | Pacientes (extiende core.partners) | +| patient_contacts | CL-001 | Contactos de emergencia | +| patient_insurance | CL-001 | Información de seguros | +| appointment_slots | CL-002 | Horarios disponibles | +| appointments | CL-002 | Citas médicas | +| medical_records | CL-003 | Expediente clínico electrónico | +| consultations | CL-003 | Consultas realizadas | +| vital_signs | CL-003 | Signos vitales | +| diagnoses | CL-003 | Diagnósticos (CIE-10) | +| prescriptions | CL-003 | Recetas médicas | +| prescription_items | CL-003 | Medicamentos en receta | + +## ENUMs + +| Enum | Valores | +|------|---------| +| appointment_status | scheduled, confirmed, in_progress, completed, cancelled, no_show | +| patient_gender | male, female, other, prefer_not_to_say | +| blood_type | A+, A-, B+, B-, AB+, AB-, O+, O-, unknown | +| consultation_status | draft, in_progress, completed, cancelled | + +## Row Level Security + +Todas las tablas tienen RLS habilitado con aislamiento por tenant: + +```sql +tenant_id = current_setting('app.current_tenant_id', true)::UUID +``` + +## Consideraciones de Seguridad + +- **NOM-024-SSA3-2012**: Expediente clínico electrónico +- **Datos sensibles**: medical_records, consultations requieren encriptación +- **Auditoría completa**: Todas las tablas tienen campos de auditoría + +## Referencias + +- [HERENCIA-ERP-CORE.md](./HERENCIA-ERP-CORE.md) +- [DATABASE_INVENTORY.yml](../orchestration/inventarios/DATABASE_INVENTORY.yml) diff --git a/init/00-extensions.sql b/init/00-extensions.sql new file mode 100644 index 0000000..bed7df7 --- /dev/null +++ b/init/00-extensions.sql @@ -0,0 +1,25 @@ +-- ============================================================================ +-- EXTENSIONES PostgreSQL - ERP Clínicas +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- Prerequisito: ERP-Core debe estar instalado +-- ============================================================================ + +-- Verificar que ERP-Core esté instalado +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN + RAISE EXCEPTION 'ERP-Core no instalado. Ejecutar primero DDL de erp-core.'; + END IF; +END $$; + +-- Extensión para encriptación de datos sensibles (expedientes médicos) +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- Extensión para búsqueda de texto (diagnósticos CIE-10) +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- ============================================================================ +-- FIN EXTENSIONES +-- ============================================================================ diff --git a/init/01-create-schemas.sql b/init/01-create-schemas.sql new file mode 100644 index 0000000..fd058a9 --- /dev/null +++ b/init/01-create-schemas.sql @@ -0,0 +1,15 @@ +-- ============================================================================ +-- SCHEMAS - ERP Clínicas +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- ============================================================================ + +-- Schema principal para operaciones clínicas +CREATE SCHEMA IF NOT EXISTS clinica; + +COMMENT ON SCHEMA clinica IS 'Schema para operaciones de clínica/consultorio médico'; + +-- ============================================================================ +-- FIN SCHEMAS +-- ============================================================================ diff --git a/init/02-rls-functions.sql b/init/02-rls-functions.sql new file mode 100644 index 0000000..2a31da8 --- /dev/null +++ b/init/02-rls-functions.sql @@ -0,0 +1,37 @@ +-- ============================================================================ +-- FUNCIONES RLS - ERP Clínicas +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- Nota: Usa las funciones de contexto de ERP-Core (auth schema) +-- ============================================================================ + +-- Las funciones principales están en ERP-Core: +-- auth.get_current_tenant_id() +-- auth.get_current_user_id() +-- auth.get_current_company_id() + +-- Función auxiliar para verificar acceso a expediente médico +CREATE OR REPLACE FUNCTION clinica.can_access_medical_record( + p_patient_id UUID, + p_user_id UUID DEFAULT NULL +) +RETURNS BOOLEAN AS $$ +DECLARE + v_user_id UUID; + v_has_access BOOLEAN := FALSE; +BEGIN + v_user_id := COALESCE(p_user_id, current_setting('app.current_user_id', true)::UUID); + + -- TODO: Implementar lógica de permisos específicos + -- Por ahora, cualquier usuario del tenant puede acceder + RETURN TRUE; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +COMMENT ON FUNCTION clinica.can_access_medical_record IS +'Verifica si el usuario tiene permiso para acceder al expediente médico del paciente'; + +-- ============================================================================ +-- FIN FUNCIONES RLS +-- ============================================================================ diff --git a/init/03-clinical-tables.sql b/init/03-clinical-tables.sql new file mode 100644 index 0000000..5a97245 --- /dev/null +++ b/init/03-clinical-tables.sql @@ -0,0 +1,628 @@ +-- ============================================================================ +-- TABLAS CLÍNICAS - ERP Clínicas +-- ============================================================================ +-- Módulos: CL-001 (Pacientes), CL-002 (Citas), CL-003 (Expediente) +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth.tenants, auth.users, core.partners) +-- 2. Schema clinica creado +-- ============================================================================ + +-- ============================================================================ +-- TYPES (ENUMs) +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE clinica.appointment_status AS ENUM ( + 'scheduled', 'confirmed', 'in_progress', 'completed', 'cancelled', 'no_show' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE clinica.patient_gender AS ENUM ( + 'male', 'female', 'other', 'prefer_not_to_say' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE clinica.blood_type AS ENUM ( + 'A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE clinica.consultation_status AS ENUM ( + 'draft', 'in_progress', 'completed', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================ +-- CATÁLOGOS BASE +-- ============================================================================ + +-- Tabla: specialties (Especialidades médicas) +CREATE TABLE IF NOT EXISTS clinica.specialties ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + consultation_duration INTEGER DEFAULT 30, -- minutos + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + CONSTRAINT uq_specialties_code UNIQUE (tenant_id, code) +); + +-- Tabla: doctors (Médicos - extiende hr.employees) +CREATE TABLE IF NOT EXISTS clinica.doctors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + employee_id UUID, -- FK a hr.employees (ERP Core) + user_id UUID REFERENCES auth.users(id), + specialty_id UUID NOT NULL REFERENCES clinica.specialties(id), + license_number VARCHAR(50) NOT NULL, -- Cédula profesional + license_expiry DATE, + secondary_specialties UUID[], -- Array de specialty_ids + consultation_fee DECIMAL(12,2), + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id), + CONSTRAINT uq_doctors_license UNIQUE (tenant_id, license_number) +); + +-- ============================================================================ +-- PACIENTES (CL-001) +-- ============================================================================ + +-- Tabla: patients (Pacientes - extiende core.partners) +CREATE TABLE IF NOT EXISTS clinica.patients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + partner_id UUID REFERENCES core.partners(id), -- Vinculo a partner + + -- Identificación + patient_number VARCHAR(30) NOT NULL, + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + middle_name VARCHAR(100), + + -- Datos personales + birth_date DATE, + gender clinica.patient_gender, + curp VARCHAR(18), + + -- Contacto + email VARCHAR(255), + phone VARCHAR(20), + mobile VARCHAR(20), + + -- Dirección + street VARCHAR(255), + city VARCHAR(100), + state VARCHAR(100), + zip_code VARCHAR(10), + country VARCHAR(100) DEFAULT 'México', + + -- Datos médicos básicos + blood_type clinica.blood_type DEFAULT 'unknown', + allergies TEXT[], + chronic_conditions TEXT[], + + -- Seguro médico + has_insurance BOOLEAN DEFAULT FALSE, + insurance_provider VARCHAR(100), + insurance_policy VARCHAR(50), + + -- Control + is_active BOOLEAN NOT NULL DEFAULT TRUE, + last_visit_date DATE, + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_patients_number UNIQUE (tenant_id, patient_number) +); + +-- Tabla: patient_contacts (Contactos de emergencia) +CREATE TABLE IF NOT EXISTS clinica.patient_contacts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + patient_id UUID NOT NULL REFERENCES clinica.patients(id) ON DELETE CASCADE, + contact_name VARCHAR(200) NOT NULL, + relationship VARCHAR(50), -- Parentesco + phone VARCHAR(20), + mobile VARCHAR(20), + email VARCHAR(255), + is_primary BOOLEAN DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id) +); + +-- Tabla: patient_insurance (Información de seguros) +CREATE TABLE IF NOT EXISTS clinica.patient_insurance ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + patient_id UUID NOT NULL REFERENCES clinica.patients(id) ON DELETE CASCADE, + insurance_provider VARCHAR(100) NOT NULL, + policy_number VARCHAR(50) NOT NULL, + group_number VARCHAR(50), + holder_name VARCHAR(200), + holder_relationship VARCHAR(50), + coverage_type VARCHAR(50), + valid_from DATE, + valid_until DATE, + is_primary BOOLEAN DEFAULT TRUE, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- CITAS (CL-002) +-- ============================================================================ + +-- Tabla: appointment_slots (Horarios disponibles) +CREATE TABLE IF NOT EXISTS clinica.appointment_slots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), + day_of_week INTEGER NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=Domingo + start_time TIME NOT NULL, + end_time TIME NOT NULL, + slot_duration INTEGER DEFAULT 30, -- minutos + max_appointments INTEGER DEFAULT 1, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + CONSTRAINT chk_slot_times CHECK (end_time > start_time) +); + +-- Tabla: appointments (Citas médicas) +CREATE TABLE IF NOT EXISTS clinica.appointments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Referencias + patient_id UUID NOT NULL REFERENCES clinica.patients(id), + doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), + specialty_id UUID REFERENCES clinica.specialties(id), + + -- Programación + appointment_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME NOT NULL, + duration INTEGER DEFAULT 30, -- minutos + + -- Estado + status clinica.appointment_status NOT NULL DEFAULT 'scheduled', + + -- Detalles + reason TEXT, -- Motivo de consulta + notes TEXT, + is_first_visit BOOLEAN DEFAULT FALSE, + is_follow_up BOOLEAN DEFAULT FALSE, + follow_up_to UUID REFERENCES clinica.appointments(id), + + -- Recordatorios + reminder_sent BOOLEAN DEFAULT FALSE, + reminder_sent_at TIMESTAMPTZ, + + -- Confirmación + confirmed_at TIMESTAMPTZ, + confirmed_by UUID REFERENCES auth.users(id), + + -- Cancelación + cancelled_at TIMESTAMPTZ, + cancelled_by UUID REFERENCES auth.users(id), + cancellation_reason TEXT, + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT chk_appointment_times CHECK (end_time > start_time) +); + +-- ============================================================================ +-- EXPEDIENTE CLÍNICO (CL-003) +-- ============================================================================ + +-- Tabla: medical_records (Expediente clínico electrónico) +-- NOTA: Datos sensibles según NOM-024-SSA3-2012 +CREATE TABLE IF NOT EXISTS clinica.medical_records ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + patient_id UUID NOT NULL REFERENCES clinica.patients(id), + + -- Número de expediente + record_number VARCHAR(30) NOT NULL, + + -- Antecedentes + family_history TEXT, + personal_history TEXT, + surgical_history TEXT, + + -- Hábitos + smoking_status VARCHAR(50), + alcohol_status VARCHAR(50), + exercise_status VARCHAR(50), + diet_notes TEXT, + + -- Gineco-obstétricos (si aplica) + obstetric_history JSONB, + + -- Notas generales + notes TEXT, + + -- Control de acceso + is_confidential BOOLEAN DEFAULT TRUE, + access_restricted BOOLEAN DEFAULT FALSE, + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_medical_records_number UNIQUE (tenant_id, record_number), + CONSTRAINT uq_medical_records_patient UNIQUE (patient_id) +); + +-- Tabla: consultations (Consultas realizadas) +CREATE TABLE IF NOT EXISTS clinica.consultations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Referencias + medical_record_id UUID NOT NULL REFERENCES clinica.medical_records(id), + appointment_id UUID REFERENCES clinica.appointments(id), + doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), + + -- Fecha/hora + consultation_date DATE NOT NULL, + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + + -- Estado + status clinica.consultation_status DEFAULT 'draft', + + -- Motivo de consulta + chief_complaint TEXT NOT NULL, -- Motivo principal + present_illness TEXT, -- Padecimiento actual + + -- Exploración física + physical_exam JSONB, -- Estructurado por sistemas + + -- Plan + treatment_plan TEXT, + follow_up_instructions TEXT, + next_appointment_days INTEGER, + + -- Notas + notes TEXT, + private_notes TEXT, -- Solo visible para el médico + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id) +); + +-- Tabla: vital_signs (Signos vitales) +CREATE TABLE IF NOT EXISTS clinica.vital_signs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + consultation_id UUID NOT NULL REFERENCES clinica.consultations(id) ON DELETE CASCADE, + + -- Signos vitales + weight_kg DECIMAL(5,2), + height_cm DECIMAL(5,2), + bmi DECIMAL(4,2) GENERATED ALWAYS AS ( + CASE WHEN height_cm > 0 THEN weight_kg / ((height_cm/100) * (height_cm/100)) END + ) STORED, + temperature_c DECIMAL(4,2), + blood_pressure_systolic INTEGER, + blood_pressure_diastolic INTEGER, + heart_rate INTEGER, -- latidos por minuto + respiratory_rate INTEGER, -- respiraciones por minuto + oxygen_saturation INTEGER, -- porcentaje + + -- Fecha/hora de medición + measured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + measured_by UUID REFERENCES auth.users(id), + + notes TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: diagnoses (Diagnósticos - CIE-10) +CREATE TABLE IF NOT EXISTS clinica.diagnoses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + consultation_id UUID NOT NULL REFERENCES clinica.consultations(id) ON DELETE CASCADE, + + -- Código CIE-10 + icd10_code VARCHAR(10) NOT NULL, + icd10_description VARCHAR(255), + + -- Tipo + diagnosis_type VARCHAR(20) NOT NULL DEFAULT 'primary', -- primary, secondary, differential + + -- Detalles + notes TEXT, + is_chronic BOOLEAN DEFAULT FALSE, + onset_date DATE, + + -- Orden + sequence INTEGER DEFAULT 1, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: prescriptions (Recetas médicas) +CREATE TABLE IF NOT EXISTS clinica.prescriptions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + consultation_id UUID NOT NULL REFERENCES clinica.consultations(id), + + -- Número de receta + prescription_number VARCHAR(30) NOT NULL, + prescription_date DATE NOT NULL DEFAULT CURRENT_DATE, + + -- Médico + doctor_id UUID NOT NULL REFERENCES clinica.doctors(id), + + -- Instrucciones generales + general_instructions TEXT, + + -- Vigencia + valid_until DATE, + + -- Estado + is_printed BOOLEAN DEFAULT FALSE, + printed_at TIMESTAMPTZ, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_prescriptions_number UNIQUE (tenant_id, prescription_number) +); + +-- Tabla: prescription_items (Líneas de receta) +CREATE TABLE IF NOT EXISTS clinica.prescription_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + prescription_id UUID NOT NULL REFERENCES clinica.prescriptions(id) ON DELETE CASCADE, + + -- Medicamento + product_id UUID, -- FK a inventory.products (ERP Core) + medication_name VARCHAR(255) NOT NULL, + presentation VARCHAR(100), -- Tabletas, jarabe, etc. + + -- Dosificación + dosage VARCHAR(100) NOT NULL, -- "1 tableta" + frequency VARCHAR(100) NOT NULL, -- "cada 8 horas" + duration VARCHAR(100), -- "por 7 días" + quantity INTEGER, -- Cantidad a surtir + + -- Instrucciones + instructions TEXT, + + -- Orden + sequence INTEGER DEFAULT 1, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +-- Specialties +CREATE INDEX IF NOT EXISTS idx_specialties_tenant ON clinica.specialties(tenant_id); + +-- Doctors +CREATE INDEX IF NOT EXISTS idx_doctors_tenant ON clinica.doctors(tenant_id); +CREATE INDEX IF NOT EXISTS idx_doctors_specialty ON clinica.doctors(specialty_id); +CREATE INDEX IF NOT EXISTS idx_doctors_user ON clinica.doctors(user_id); + +-- Patients +CREATE INDEX IF NOT EXISTS idx_patients_tenant ON clinica.patients(tenant_id); +CREATE INDEX IF NOT EXISTS idx_patients_partner ON clinica.patients(partner_id); +CREATE INDEX IF NOT EXISTS idx_patients_name ON clinica.patients(last_name, first_name); +CREATE INDEX IF NOT EXISTS idx_patients_curp ON clinica.patients(curp); + +-- Patient contacts +CREATE INDEX IF NOT EXISTS idx_patient_contacts_tenant ON clinica.patient_contacts(tenant_id); +CREATE INDEX IF NOT EXISTS idx_patient_contacts_patient ON clinica.patient_contacts(patient_id); + +-- Patient insurance +CREATE INDEX IF NOT EXISTS idx_patient_insurance_tenant ON clinica.patient_insurance(tenant_id); +CREATE INDEX IF NOT EXISTS idx_patient_insurance_patient ON clinica.patient_insurance(patient_id); + +-- Appointment slots +CREATE INDEX IF NOT EXISTS idx_appointment_slots_tenant ON clinica.appointment_slots(tenant_id); +CREATE INDEX IF NOT EXISTS idx_appointment_slots_doctor ON clinica.appointment_slots(doctor_id); + +-- Appointments +CREATE INDEX IF NOT EXISTS idx_appointments_tenant ON clinica.appointments(tenant_id); +CREATE INDEX IF NOT EXISTS idx_appointments_patient ON clinica.appointments(patient_id); +CREATE INDEX IF NOT EXISTS idx_appointments_doctor ON clinica.appointments(doctor_id); +CREATE INDEX IF NOT EXISTS idx_appointments_date ON clinica.appointments(appointment_date); +CREATE INDEX IF NOT EXISTS idx_appointments_status ON clinica.appointments(status); + +-- Medical records +CREATE INDEX IF NOT EXISTS idx_medical_records_tenant ON clinica.medical_records(tenant_id); +CREATE INDEX IF NOT EXISTS idx_medical_records_patient ON clinica.medical_records(patient_id); + +-- Consultations +CREATE INDEX IF NOT EXISTS idx_consultations_tenant ON clinica.consultations(tenant_id); +CREATE INDEX IF NOT EXISTS idx_consultations_record ON clinica.consultations(medical_record_id); +CREATE INDEX IF NOT EXISTS idx_consultations_doctor ON clinica.consultations(doctor_id); +CREATE INDEX IF NOT EXISTS idx_consultations_date ON clinica.consultations(consultation_date); + +-- Vital signs +CREATE INDEX IF NOT EXISTS idx_vital_signs_tenant ON clinica.vital_signs(tenant_id); +CREATE INDEX IF NOT EXISTS idx_vital_signs_consultation ON clinica.vital_signs(consultation_id); + +-- Diagnoses +CREATE INDEX IF NOT EXISTS idx_diagnoses_tenant ON clinica.diagnoses(tenant_id); +CREATE INDEX IF NOT EXISTS idx_diagnoses_consultation ON clinica.diagnoses(consultation_id); +CREATE INDEX IF NOT EXISTS idx_diagnoses_icd10 ON clinica.diagnoses(icd10_code); + +-- Prescriptions +CREATE INDEX IF NOT EXISTS idx_prescriptions_tenant ON clinica.prescriptions(tenant_id); +CREATE INDEX IF NOT EXISTS idx_prescriptions_consultation ON clinica.prescriptions(consultation_id); + +-- Prescription items +CREATE INDEX IF NOT EXISTS idx_prescription_items_tenant ON clinica.prescription_items(tenant_id); +CREATE INDEX IF NOT EXISTS idx_prescription_items_prescription ON clinica.prescription_items(prescription_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY +-- ============================================================================ + +ALTER TABLE clinica.specialties ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.doctors ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.patients ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.patient_contacts ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.patient_insurance ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.appointment_slots ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.appointments ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.medical_records ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.consultations ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.vital_signs ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.diagnoses ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.prescriptions ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.prescription_items ENABLE ROW LEVEL SECURITY; + +-- Políticas de aislamiento por tenant +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_specialties ON clinica.specialties; + CREATE POLICY tenant_isolation_specialties ON clinica.specialties + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_doctors ON clinica.doctors; + CREATE POLICY tenant_isolation_doctors ON clinica.doctors + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_patients ON clinica.patients; + CREATE POLICY tenant_isolation_patients ON clinica.patients + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_patient_contacts ON clinica.patient_contacts; + CREATE POLICY tenant_isolation_patient_contacts ON clinica.patient_contacts + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_patient_insurance ON clinica.patient_insurance; + CREATE POLICY tenant_isolation_patient_insurance ON clinica.patient_insurance + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_appointment_slots ON clinica.appointment_slots; + CREATE POLICY tenant_isolation_appointment_slots ON clinica.appointment_slots + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_appointments ON clinica.appointments; + CREATE POLICY tenant_isolation_appointments ON clinica.appointments + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_medical_records ON clinica.medical_records; + CREATE POLICY tenant_isolation_medical_records ON clinica.medical_records + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_consultations ON clinica.consultations; + CREATE POLICY tenant_isolation_consultations ON clinica.consultations + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_vital_signs ON clinica.vital_signs; + CREATE POLICY tenant_isolation_vital_signs ON clinica.vital_signs + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_diagnoses ON clinica.diagnoses; + CREATE POLICY tenant_isolation_diagnoses ON clinica.diagnoses + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_prescriptions ON clinica.prescriptions; + CREATE POLICY tenant_isolation_prescriptions ON clinica.prescriptions + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_prescription_items ON clinica.prescription_items; + CREATE POLICY tenant_isolation_prescription_items ON clinica.prescription_items + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON TABLE clinica.specialties IS 'Catálogo de especialidades médicas'; +COMMENT ON TABLE clinica.doctors IS 'Médicos y especialistas - extiende hr.employees'; +COMMENT ON TABLE clinica.patients IS 'Registro de pacientes - extiende core.partners'; +COMMENT ON TABLE clinica.patient_contacts IS 'Contactos de emergencia del paciente'; +COMMENT ON TABLE clinica.patient_insurance IS 'Información de seguros médicos'; +COMMENT ON TABLE clinica.appointment_slots IS 'Horarios disponibles por médico'; +COMMENT ON TABLE clinica.appointments IS 'Citas médicas programadas'; +COMMENT ON TABLE clinica.medical_records IS 'Expediente clínico electrónico (NOM-024-SSA3)'; +COMMENT ON TABLE clinica.consultations IS 'Consultas médicas realizadas'; +COMMENT ON TABLE clinica.vital_signs IS 'Signos vitales del paciente'; +COMMENT ON TABLE clinica.diagnoses IS 'Diagnósticos según CIE-10'; +COMMENT ON TABLE clinica.prescriptions IS 'Recetas médicas'; +COMMENT ON TABLE clinica.prescription_items IS 'Medicamentos en receta'; + +-- ============================================================================ +-- FIN TABLAS CLÍNICAS +-- Total: 13 tablas, 4 ENUMs +-- ============================================================================ diff --git a/init/04-seed-data.sql b/init/04-seed-data.sql new file mode 100644 index 0000000..35648ef --- /dev/null +++ b/init/04-seed-data.sql @@ -0,0 +1,34 @@ +-- ============================================================================ +-- DATOS INICIALES - ERP Clínicas +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- ============================================================================ + +-- Especialidades médicas comunes +-- NOTA: Se insertan solo si el tenant existe (usar en script de inicialización) + +/* +-- Ejemplo de inserción (ejecutar con tenant_id específico): + +INSERT INTO clinica.specialties (tenant_id, code, name, description, consultation_duration) VALUES + ('TENANT_UUID', 'MG', 'Medicina General', 'Atención médica primaria', 30), + ('TENANT_UUID', 'PED', 'Pediatría', 'Atención médica infantil', 30), + ('TENANT_UUID', 'GIN', 'Ginecología', 'Salud de la mujer', 30), + ('TENANT_UUID', 'CARD', 'Cardiología', 'Enfermedades del corazón', 45), + ('TENANT_UUID', 'DERM', 'Dermatología', 'Enfermedades de la piel', 30), + ('TENANT_UUID', 'OFT', 'Oftalmología', 'Salud visual', 30), + ('TENANT_UUID', 'ORL', 'Otorrinolaringología', 'Oído, nariz y garganta', 30), + ('TENANT_UUID', 'TRAU', 'Traumatología', 'Sistema músculo-esquelético', 30), + ('TENANT_UUID', 'NEUR', 'Neurología', 'Sistema nervioso', 45), + ('TENANT_UUID', 'PSIQ', 'Psiquiatría', 'Salud mental', 60), + ('TENANT_UUID', 'ENDO', 'Endocrinología', 'Sistema endocrino', 45), + ('TENANT_UUID', 'GAST', 'Gastroenterología', 'Sistema digestivo', 45), + ('TENANT_UUID', 'NEFR', 'Nefrología', 'Enfermedades renales', 45), + ('TENANT_UUID', 'UROL', 'Urología', 'Sistema urinario', 30), + ('TENANT_UUID', 'ONCO', 'Oncología', 'Tratamiento del cáncer', 60); +*/ + +-- ============================================================================ +-- FIN SEED DATA +-- ============================================================================ diff --git a/schemas/01-clinica-core-schema-ddl.sql b/schemas/01-clinica-core-schema-ddl.sql new file mode 100644 index 0000000..d1a4887 --- /dev/null +++ b/schemas/01-clinica-core-schema-ddl.sql @@ -0,0 +1,446 @@ +-- ============================================================================ +-- CLINICA CORE SCHEMA - ERP Clinicas +-- Tablas principales del sistema clinico +-- ============================================================================ +-- Fecha: 2026-01-13 +-- Version: 1.0 +-- Modulos: CL-002 (Pacientes), CL-003 (Citas), CL-004 (Consultas) +-- ============================================================================ + +-- Crear schema si no existe +CREATE SCHEMA IF NOT EXISTS clinica; + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +CREATE TYPE clinica.patient_status AS ENUM ( + 'active', + 'inactive', + 'deceased' +); + +CREATE TYPE clinica.gender AS ENUM ( + 'male', + 'female', + 'other', + 'unknown' +); + +CREATE TYPE clinica.blood_type AS ENUM ( + 'A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'unknown' +); + +CREATE TYPE clinica.appointment_status AS ENUM ( + 'scheduled', + 'confirmed', + 'in_progress', + 'completed', + 'cancelled', + 'no_show' +); + +CREATE TYPE clinica.consultation_status AS ENUM ( + 'in_progress', + 'completed', + 'cancelled' +); + +CREATE TYPE clinica.prescription_status AS ENUM ( + 'active', + 'completed', + 'cancelled' +); + +-- ============================================================================ +-- TABLAS: ESPECIALIDADES Y MEDICOS (CL-002) +-- ============================================================================ + +-- Catalogo de especialidades medicas +CREATE TABLE IF NOT EXISTS clinica.specialties ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + requires_referral BOOLEAN DEFAULT false, + consultation_duration_minutes INTEGER DEFAULT 30, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT uq_specialty_code UNIQUE (tenant_id, code) +); + +COMMENT ON TABLE clinica.specialties IS 'Catalogo de especialidades medicas - CL-002'; + +-- Medicos/Doctores +CREATE TABLE IF NOT EXISTS clinica.doctors ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID, -- FK a hr.employees opcional + -- Datos personales + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(255), + phone VARCHAR(20), + -- Datos profesionales + professional_license VARCHAR(50) NOT NULL, -- Cedula profesional + specialty_license VARCHAR(50), -- Cedula de especialidad + specialty_id UUID NOT NULL, + -- Configuracion + consultation_duration_minutes INTEGER DEFAULT 30, + max_appointments_per_day INTEGER DEFAULT 20, + accepts_insurance BOOLEAN DEFAULT true, + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ, + CONSTRAINT fk_doctor_specialty FOREIGN KEY (specialty_id) REFERENCES clinica.specialties(id) +); + +COMMENT ON TABLE clinica.doctors IS 'Registro de medicos y especialistas - CL-002'; +COMMENT ON COLUMN clinica.doctors.professional_license IS 'Cedula profesional obligatoria'; + +-- ============================================================================ +-- TABLAS: PACIENTES (CL-002) +-- ============================================================================ + +-- Pacientes +CREATE TABLE IF NOT EXISTS clinica.patients ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + partner_id UUID, -- FK a core.partners opcional (para facturacion) + -- Datos personales + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + date_of_birth DATE, + gender clinica.gender DEFAULT 'unknown', + -- Identificacion + curp VARCHAR(18), + rfc VARCHAR(13), + -- Contacto + email VARCHAR(255), + phone VARCHAR(20), + mobile VARCHAR(20), + address JSONB, -- {street, city, state, zip, country} + -- Datos medicos basicos + blood_type clinica.blood_type DEFAULT 'unknown', + allergies TEXT[], + chronic_conditions TEXT[], + emergency_contact_name VARCHAR(200), + emergency_contact_phone VARCHAR(20), + -- Seguro medico + has_insurance BOOLEAN DEFAULT false, + insurance_provider VARCHAR(100), + insurance_policy_number VARCHAR(50), + -- Control + status clinica.patient_status DEFAULT 'active', + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +COMMENT ON TABLE clinica.patients IS 'Registro de pacientes - CL-002'; +COMMENT ON COLUMN clinica.patients.allergies IS 'Lista de alergias conocidas'; +COMMENT ON COLUMN clinica.patients.chronic_conditions IS 'Condiciones cronicas conocidas'; + +-- ============================================================================ +-- TABLAS: CITAS (CL-003) +-- ============================================================================ + +-- Horarios disponibles por medico +CREATE TABLE IF NOT EXISTS clinica.appointment_slots ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + doctor_id UUID NOT NULL, + -- Horario + day_of_week INTEGER NOT NULL CHECK (day_of_week BETWEEN 0 AND 6), -- 0=domingo + start_time TIME NOT NULL, + end_time TIME NOT NULL, + slot_duration_minutes INTEGER DEFAULT 30, + -- Control + active BOOLEAN DEFAULT true, + valid_from DATE DEFAULT CURRENT_DATE, + valid_until DATE, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_slot_doctor FOREIGN KEY (doctor_id) REFERENCES clinica.doctors(id) ON DELETE CASCADE, + CONSTRAINT chk_valid_time_range CHECK (end_time > start_time) +); + +COMMENT ON TABLE clinica.appointment_slots IS 'Horarios disponibles de medicos - CL-003'; + +-- Citas medicas +CREATE TABLE IF NOT EXISTS clinica.appointments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + -- Relaciones + patient_id UUID NOT NULL, + doctor_id UUID NOT NULL, + -- Programacion + scheduled_date DATE NOT NULL, + scheduled_time TIME NOT NULL, + duration_minutes INTEGER DEFAULT 30, + -- Estado + status clinica.appointment_status DEFAULT 'scheduled', + -- Detalles + reason TEXT, + notes TEXT, + -- Confirmacion + confirmed_at TIMESTAMPTZ, + confirmed_by UUID, + -- Cancelacion + cancelled_at TIMESTAMPTZ, + cancelled_by UUID, + cancellation_reason TEXT, + -- Cita pasada + check_in_at TIMESTAMPTZ, + check_out_at TIMESTAMPTZ, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + created_by UUID, + CONSTRAINT fk_appointment_patient FOREIGN KEY (patient_id) REFERENCES clinica.patients(id), + CONSTRAINT fk_appointment_doctor FOREIGN KEY (doctor_id) REFERENCES clinica.doctors(id) +); + +COMMENT ON TABLE clinica.appointments IS 'Citas medicas programadas - CL-003'; + +-- ============================================================================ +-- TABLAS: CONSULTAS Y EXPEDIENTE (CL-004) +-- ============================================================================ + +-- Consultas medicas +CREATE TABLE IF NOT EXISTS clinica.consultations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + -- Relaciones + patient_id UUID NOT NULL, + doctor_id UUID NOT NULL, + appointment_id UUID, -- puede ser consulta sin cita + -- Tiempo + started_at TIMESTAMPTZ DEFAULT NOW(), + ended_at TIMESTAMPTZ, + -- Contenido clinico + chief_complaint TEXT, -- Motivo de consulta + present_illness TEXT, -- Padecimiento actual + physical_examination TEXT, -- Exploracion fisica + assessment TEXT, -- Valoracion/Impresion diagnostica + plan TEXT, -- Plan de tratamiento + -- Signos vitales (snapshot) + vital_signs JSONB, -- {weight_kg, height_cm, temperature, blood_pressure, heart_rate, respiratory_rate, oxygen_saturation} + -- Estado + status clinica.consultation_status DEFAULT 'in_progress', + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_consultation_patient FOREIGN KEY (patient_id) REFERENCES clinica.patients(id), + CONSTRAINT fk_consultation_doctor FOREIGN KEY (doctor_id) REFERENCES clinica.doctors(id), + CONSTRAINT fk_consultation_appointment FOREIGN KEY (appointment_id) REFERENCES clinica.appointments(id) +); + +COMMENT ON TABLE clinica.consultations IS 'Consultas medicas - CL-004'; +COMMENT ON COLUMN clinica.consultations.chief_complaint IS 'Motivo de consulta principal'; +COMMENT ON COLUMN clinica.consultations.vital_signs IS 'Signos vitales en formato JSON'; + +-- Diagnosticos (CIE-10) +CREATE TABLE IF NOT EXISTS clinica.diagnoses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + consultation_id UUID NOT NULL, + -- Diagnostico + icd10_code VARCHAR(10), -- Codigo CIE-10 + description TEXT NOT NULL, + is_primary BOOLEAN DEFAULT false, + diagnosis_type VARCHAR(20) DEFAULT 'definitive', -- 'presumptive', 'definitive', 'differential' + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_diagnosis_consultation FOREIGN KEY (consultation_id) REFERENCES clinica.consultations(id) ON DELETE CASCADE +); + +COMMENT ON TABLE clinica.diagnoses IS 'Diagnosticos con codificacion CIE-10 - CL-004'; + +-- Recetas/Prescripciones +CREATE TABLE IF NOT EXISTS clinica.prescriptions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + consultation_id UUID NOT NULL, + patient_id UUID NOT NULL, + doctor_id UUID NOT NULL, + -- Datos de la receta + prescription_number VARCHAR(50), + prescription_date DATE DEFAULT CURRENT_DATE, + -- Estado + status clinica.prescription_status DEFAULT 'active', + -- Notas + instructions TEXT, + notes TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_prescription_consultation FOREIGN KEY (consultation_id) REFERENCES clinica.consultations(id), + CONSTRAINT fk_prescription_patient FOREIGN KEY (patient_id) REFERENCES clinica.patients(id), + CONSTRAINT fk_prescription_doctor FOREIGN KEY (doctor_id) REFERENCES clinica.doctors(id) +); + +COMMENT ON TABLE clinica.prescriptions IS 'Recetas medicas - CL-004'; + +-- Items de receta (medicamentos) +CREATE TABLE IF NOT EXISTS clinica.prescription_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + prescription_id UUID NOT NULL, + -- Medicamento + medication_name VARCHAR(200) NOT NULL, + medication_code VARCHAR(50), -- codigo interno o externo + -- Dosificacion + dosage VARCHAR(100) NOT NULL, -- "500mg" + frequency VARCHAR(100) NOT NULL, -- "cada 8 horas" + duration VARCHAR(100), -- "7 dias" + quantity INTEGER, + -- Instrucciones + instructions TEXT, + -- Control + sequence INTEGER DEFAULT 1, + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_prescription_item FOREIGN KEY (prescription_id) REFERENCES clinica.prescriptions(id) ON DELETE CASCADE +); + +COMMENT ON TABLE clinica.prescription_items IS 'Medicamentos en receta - CL-004'; + +-- Signos vitales (historial) +CREATE TABLE IF NOT EXISTS clinica.vital_signs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + patient_id UUID NOT NULL, + consultation_id UUID, + recorded_by UUID, + -- Mediciones + weight_kg NUMERIC(5,2), + height_cm NUMERIC(5,2), + temperature_celsius NUMERIC(4,1), + blood_pressure_systolic INTEGER, + blood_pressure_diastolic INTEGER, + heart_rate INTEGER, -- latidos por minuto + respiratory_rate INTEGER, -- respiraciones por minuto + oxygen_saturation INTEGER, -- porcentaje + -- Extras + glucose_mg_dl NUMERIC(5,1), + notes TEXT, + -- Control + recorded_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT fk_vitals_patient FOREIGN KEY (patient_id) REFERENCES clinica.patients(id), + CONSTRAINT fk_vitals_consultation FOREIGN KEY (consultation_id) REFERENCES clinica.consultations(id) +); + +COMMENT ON TABLE clinica.vital_signs IS 'Historial de signos vitales - CL-004'; + +-- ============================================================================ +-- INDICES +-- ============================================================================ + +-- Specialties +CREATE INDEX IF NOT EXISTS idx_specialties_tenant ON clinica.specialties(tenant_id); +CREATE INDEX IF NOT EXISTS idx_specialties_active ON clinica.specialties(tenant_id, active); + +-- Doctors +CREATE INDEX IF NOT EXISTS idx_doctors_tenant ON clinica.doctors(tenant_id); +CREATE INDEX IF NOT EXISTS idx_doctors_specialty ON clinica.doctors(specialty_id); +CREATE INDEX IF NOT EXISTS idx_doctors_license ON clinica.doctors(professional_license); +CREATE INDEX IF NOT EXISTS idx_doctors_active ON clinica.doctors(tenant_id, active) WHERE deleted_at IS NULL; + +-- Patients +CREATE INDEX IF NOT EXISTS idx_patients_tenant ON clinica.patients(tenant_id); +CREATE INDEX IF NOT EXISTS idx_patients_name ON clinica.patients(tenant_id, last_name, first_name); +CREATE INDEX IF NOT EXISTS idx_patients_curp ON clinica.patients(curp) WHERE curp IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_patients_phone ON clinica.patients(tenant_id, mobile); +CREATE INDEX IF NOT EXISTS idx_patients_status ON clinica.patients(tenant_id, status) WHERE deleted_at IS NULL; + +-- Appointment Slots +CREATE INDEX IF NOT EXISTS idx_slots_tenant ON clinica.appointment_slots(tenant_id); +CREATE INDEX IF NOT EXISTS idx_slots_doctor ON clinica.appointment_slots(doctor_id); +CREATE INDEX IF NOT EXISTS idx_slots_day ON clinica.appointment_slots(tenant_id, day_of_week, active); + +-- Appointments +CREATE INDEX IF NOT EXISTS idx_appointments_tenant ON clinica.appointments(tenant_id); +CREATE INDEX IF NOT EXISTS idx_appointments_patient ON clinica.appointments(patient_id); +CREATE INDEX IF NOT EXISTS idx_appointments_doctor ON clinica.appointments(doctor_id); +CREATE INDEX IF NOT EXISTS idx_appointments_date ON clinica.appointments(tenant_id, scheduled_date); +CREATE INDEX IF NOT EXISTS idx_appointments_status ON clinica.appointments(tenant_id, status); +CREATE INDEX IF NOT EXISTS idx_appointments_doctor_date ON clinica.appointments(doctor_id, scheduled_date); + +-- Consultations +CREATE INDEX IF NOT EXISTS idx_consultations_tenant ON clinica.consultations(tenant_id); +CREATE INDEX IF NOT EXISTS idx_consultations_patient ON clinica.consultations(patient_id); +CREATE INDEX IF NOT EXISTS idx_consultations_doctor ON clinica.consultations(doctor_id); +CREATE INDEX IF NOT EXISTS idx_consultations_date ON clinica.consultations(tenant_id, started_at); + +-- Diagnoses +CREATE INDEX IF NOT EXISTS idx_diagnoses_tenant ON clinica.diagnoses(tenant_id); +CREATE INDEX IF NOT EXISTS idx_diagnoses_consultation ON clinica.diagnoses(consultation_id); +CREATE INDEX IF NOT EXISTS idx_diagnoses_icd10 ON clinica.diagnoses(icd10_code) WHERE icd10_code IS NOT NULL; + +-- Prescriptions +CREATE INDEX IF NOT EXISTS idx_prescriptions_tenant ON clinica.prescriptions(tenant_id); +CREATE INDEX IF NOT EXISTS idx_prescriptions_consultation ON clinica.prescriptions(consultation_id); +CREATE INDEX IF NOT EXISTS idx_prescriptions_patient ON clinica.prescriptions(patient_id); +CREATE INDEX IF NOT EXISTS idx_prescriptions_date ON clinica.prescriptions(tenant_id, prescription_date); + +-- Prescription Items +CREATE INDEX IF NOT EXISTS idx_prescription_items_prescription ON clinica.prescription_items(prescription_id); + +-- Vital Signs +CREATE INDEX IF NOT EXISTS idx_vitals_tenant ON clinica.vital_signs(tenant_id); +CREATE INDEX IF NOT EXISTS idx_vitals_patient ON clinica.vital_signs(patient_id); +CREATE INDEX IF NOT EXISTS idx_vitals_date ON clinica.vital_signs(patient_id, recorded_at DESC); + +-- ============================================================================ +-- ROW LEVEL SECURITY +-- ============================================================================ + +ALTER TABLE clinica.specialties ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.doctors ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.patients ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.appointment_slots ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.appointments ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.consultations ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.diagnoses ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.prescriptions ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.prescription_items ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.vital_signs ENABLE ROW LEVEL SECURITY; + +-- Politicas de aislamiento por tenant +CREATE POLICY tenant_isolation_specialties ON clinica.specialties + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_doctors ON clinica.doctors + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_patients ON clinica.patients + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_slots ON clinica.appointment_slots + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_appointments ON clinica.appointments + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_consultations ON clinica.consultations + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_diagnoses ON clinica.diagnoses + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_prescriptions ON clinica.prescriptions + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_prescription_items ON clinica.prescription_items + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY tenant_isolation_vitals ON clinica.vital_signs + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN CLINICA CORE SCHEMA +-- ============================================================================ diff --git a/schemas/04-financial-ext-schema-ddl.sql b/schemas/04-financial-ext-schema-ddl.sql new file mode 100644 index 0000000..a556e99 --- /dev/null +++ b/schemas/04-financial-ext-schema-ddl.sql @@ -0,0 +1,148 @@ +-- ============================================================================ +-- FINANCIAL EXTENSIONS - FASE 8 ERP-Core +-- ERP Clínicas (Base Genérica) +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- ============================================================================ + +-- Schema +CREATE SCHEMA IF NOT EXISTS financial; + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE financial.payment_method_type AS ENUM ('inbound', 'outbound'); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE financial.reconcile_model_type AS ENUM ( + 'writeoff_button', + 'writeoff_suggestion', + 'invoice_matching' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================ +-- TABLAS +-- ============================================================================ + +-- Líneas de términos de pago +CREATE TABLE IF NOT EXISTS financial.payment_term_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + payment_term_id UUID, + value_type VARCHAR(20) NOT NULL DEFAULT 'percent', + value NUMERIC(10,2) DEFAULT 0, + days INTEGER DEFAULT 0, + day_of_month INTEGER, + applies_to VARCHAR(50), -- 'consulta', 'procedimiento', 'laboratorio', 'farmacia' + sequence INTEGER DEFAULT 10, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE financial.payment_term_lines IS 'Líneas de términos de pago - FASE 8'; +COMMENT ON COLUMN financial.payment_term_lines.applies_to IS 'Tipo de servicio al que aplica'; + +-- Métodos de pago +CREATE TABLE IF NOT EXISTS financial.payment_methods ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + code VARCHAR(20) NOT NULL, + payment_type financial.payment_method_type NOT NULL, + -- Extensiones clínica + aplica_seguro BOOLEAN DEFAULT false, + requiere_factura BOOLEAN DEFAULT false, + porcentaje_seguro NUMERIC(5,2) DEFAULT 0, + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT uq_payment_methods_tenant_code UNIQUE(tenant_id, code) +); + +COMMENT ON TABLE financial.payment_methods IS 'Métodos de pago - FASE 8'; +COMMENT ON COLUMN financial.payment_methods.aplica_seguro IS 'Si el método está asociado a pagos de seguro'; +COMMENT ON COLUMN financial.payment_methods.porcentaje_seguro IS 'Porcentaje que cubre el seguro'; + +-- Modelos de conciliación +CREATE TABLE IF NOT EXISTS financial.reconcile_models ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + rule_type financial.reconcile_model_type NOT NULL DEFAULT 'writeoff_button', + auto_reconcile BOOLEAN DEFAULT false, + match_partner BOOLEAN DEFAULT true, + match_amount BOOLEAN DEFAULT true, + tolerance NUMERIC(10,2) DEFAULT 0, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE financial.reconcile_models IS 'Modelos de conciliación automática - FASE 8'; + +-- Líneas de modelo de conciliación +CREATE TABLE IF NOT EXISTS financial.reconcile_model_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + model_id UUID NOT NULL REFERENCES financial.reconcile_models(id) ON DELETE CASCADE, + sequence INTEGER DEFAULT 10, + account_id UUID, + amount_type VARCHAR(20) DEFAULT 'percentage', + amount_value NUMERIC(10,2) DEFAULT 100, + label VARCHAR(100), + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE financial.reconcile_model_lines IS 'Líneas de modelo de conciliación - FASE 8'; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_payment_term_lines_tenant + ON financial.payment_term_lines(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payment_term_lines_payment_term + ON financial.payment_term_lines(payment_term_id); + +CREATE INDEX IF NOT EXISTS idx_payment_methods_tenant + ON financial.payment_methods(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payment_methods_code + ON financial.payment_methods(tenant_id, code); + +CREATE INDEX IF NOT EXISTS idx_reconcile_models_tenant + ON financial.reconcile_models(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_reconcile_model_lines_model + ON financial.reconcile_model_lines(model_id); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE financial.payment_term_lines ENABLE ROW LEVEL SECURITY; +ALTER TABLE financial.payment_methods ENABLE ROW LEVEL SECURITY; +ALTER TABLE financial.reconcile_models ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_payment_term_lines ON financial.payment_term_lines; +CREATE POLICY tenant_isolation_payment_term_lines ON financial.payment_term_lines + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_payment_methods ON financial.payment_methods; +CREATE POLICY tenant_isolation_payment_methods ON financial.payment_methods + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_reconcile_models ON financial.reconcile_models; +CREATE POLICY tenant_isolation_reconcile_models ON financial.reconcile_models + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN FINANCIAL EXTENSIONS +-- ============================================================================ diff --git a/schemas/05-hr-ext-fase8-schema-ddl.sql b/schemas/05-hr-ext-fase8-schema-ddl.sql new file mode 100644 index 0000000..6924627 --- /dev/null +++ b/schemas/05-hr-ext-fase8-schema-ddl.sql @@ -0,0 +1,354 @@ +-- ============================================================================ +-- HR EXTENSIONS - FASE 8 ERP-Core +-- ERP Clínicas (Base Genérica) +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- ============================================================================ + +-- Schema +CREATE SCHEMA IF NOT EXISTS hr; + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE hr.expense_status AS ENUM ( + 'draft', 'submitted', 'approved', 'posted', 'paid', 'rejected' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE hr.resume_line_type AS ENUM ( + 'experience', 'education', 'certification', 'internal' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +DO $$ BEGIN + CREATE TYPE hr.payslip_status AS ENUM ( + 'draft', 'verify', 'done', 'cancel' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- ============================================================================ +-- TABLAS +-- ============================================================================ + +-- Ubicaciones de trabajo (consultorios/sucursales) +CREATE TABLE IF NOT EXISTS hr.work_locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + address_id UUID, + -- Extensiones clínica + tipo_consultorio VARCHAR(50), -- 'general', 'especialidad', 'urgencias', 'quirofano', 'laboratorio' + capacidad INTEGER DEFAULT 1, + equipamiento TEXT[], + horario_apertura TIME, + horario_cierre TIME, + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.work_locations IS 'Ubicaciones de trabajo/consultorios - FASE 8'; +COMMENT ON COLUMN hr.work_locations.tipo_consultorio IS 'Tipo de consultorio o área'; + +-- Tipos de habilidad +CREATE TABLE IF NOT EXISTS hr.skill_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.skill_types IS 'Tipos de habilidad (Especialidad, Certificación, etc.) - FASE 8'; + +-- Habilidades/Especialidades +CREATE TABLE IF NOT EXISTS hr.skills ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + skill_type_id UUID NOT NULL REFERENCES hr.skill_types(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + -- Extensiones clínica + codigo_ssa VARCHAR(20), + requiere_cedula BOOLEAN DEFAULT false, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.skills IS 'Habilidades/Especialidades médicas - FASE 8'; +COMMENT ON COLUMN hr.skills.codigo_ssa IS 'Código SSA de la especialidad'; + +-- Niveles de habilidad +CREATE TABLE IF NOT EXISTS hr.skill_levels ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + skill_type_id UUID NOT NULL REFERENCES hr.skill_types(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + level INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.skill_levels IS 'Niveles de habilidad - FASE 8'; + +-- Habilidades de empleado +CREATE TABLE IF NOT EXISTS hr.employee_skills ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID NOT NULL, + skill_id UUID NOT NULL REFERENCES hr.skills(id) ON DELETE CASCADE, + skill_level_id UUID REFERENCES hr.skill_levels(id), + -- Extensiones clínica + cedula_profesional VARCHAR(20), + fecha_certificacion DATE, + fecha_vencimiento DATE, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.employee_skills IS 'Habilidades asignadas a empleados - FASE 8'; +COMMENT ON COLUMN hr.employee_skills.cedula_profesional IS 'Número de cédula profesional'; + +-- Hojas de gastos +CREATE TABLE IF NOT EXISTS hr.expense_sheets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + state hr.expense_status DEFAULT 'draft', + accounting_date DATE, + total_amount NUMERIC(12,2) DEFAULT 0, + -- Extensiones clínica + paciente_id UUID, + cita_id UUID, + centro_costo VARCHAR(50), + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.expense_sheets IS 'Hojas de gastos - FASE 8'; +COMMENT ON COLUMN hr.expense_sheets.paciente_id IS 'Paciente asociado al gasto (si aplica)'; + +-- Gastos individuales +CREATE TABLE IF NOT EXISTS hr.expenses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + sheet_id UUID REFERENCES hr.expense_sheets(id) ON DELETE CASCADE, + employee_id UUID NOT NULL, + name VARCHAR(200) NOT NULL, + date DATE NOT NULL DEFAULT CURRENT_DATE, + product_id UUID, + quantity NUMERIC(10,2) DEFAULT 1, + unit_amount NUMERIC(12,2) NOT NULL, + total_amount NUMERIC(12,2) NOT NULL, + state hr.expense_status DEFAULT 'draft', + reference VARCHAR(100), + description TEXT, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.expenses IS 'Gastos individuales - FASE 8'; + +-- Líneas de currículum +CREATE TABLE IF NOT EXISTS hr.employee_resume_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID NOT NULL, + line_type hr.resume_line_type NOT NULL, + name VARCHAR(200) NOT NULL, + description TEXT, + date_start DATE, + date_end DATE, + -- Control + display_type VARCHAR(20) DEFAULT 'classic', + sequence INTEGER DEFAULT 10, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.employee_resume_lines IS 'Líneas de currículum/experiencia - FASE 8'; + +-- Estructuras de nómina +CREATE TABLE IF NOT EXISTS hr.payslip_structures ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + code VARCHAR(20) NOT NULL, + -- Extensiones clínica + tipo_pago VARCHAR(50), -- 'quincenal', 'semanal', 'honorarios', 'guardia' + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + CONSTRAINT uq_payslip_structures_tenant_code UNIQUE(tenant_id, code) +); + +COMMENT ON TABLE hr.payslip_structures IS 'Estructuras de nómina - FASE 8'; + +-- Nóminas +CREATE TABLE IF NOT EXISTS hr.payslips ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID NOT NULL, + structure_id UUID REFERENCES hr.payslip_structures(id), + name VARCHAR(100), + number VARCHAR(50), + date_from DATE NOT NULL, + date_to DATE NOT NULL, + state hr.payslip_status DEFAULT 'draft', + -- Montos + basic_wage NUMERIC(12,2) DEFAULT 0, + gross NUMERIC(12,2) DEFAULT 0, + net NUMERIC(12,2) DEFAULT 0, + -- Extensiones clínica + consultorio_id UUID REFERENCES hr.work_locations(id), + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.payslips IS 'Nóminas - FASE 8'; + +-- Líneas de nómina +CREATE TABLE IF NOT EXISTS hr.payslip_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + payslip_id UUID NOT NULL REFERENCES hr.payslips(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + code VARCHAR(20), + category VARCHAR(50), + sequence INTEGER DEFAULT 10, + quantity NUMERIC(10,2) DEFAULT 1, + rate NUMERIC(12,4) DEFAULT 0, + amount NUMERIC(12,2) DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE hr.payslip_lines IS 'Líneas de nómina - FASE 8'; + +-- ============================================================================ +-- CAMPOS ADICIONALES A EMPLOYEES (si existe) +-- ============================================================================ + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'hr' AND table_name = 'employees') THEN + + -- work_location_id + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'hr' AND table_name = 'employees' + AND column_name = 'work_location_id') THEN + ALTER TABLE hr.employees ADD COLUMN work_location_id UUID + REFERENCES hr.work_locations(id); + END IF; + + -- badge_id + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'hr' AND table_name = 'employees' + AND column_name = 'badge_id') THEN + ALTER TABLE hr.employees ADD COLUMN badge_id VARCHAR(50); + END IF; + + -- cedula_profesional + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'hr' AND table_name = 'employees' + AND column_name = 'cedula_profesional') THEN + ALTER TABLE hr.employees ADD COLUMN cedula_profesional VARCHAR(20); + END IF; + + END IF; +END $$; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_work_locations_tenant ON hr.work_locations(tenant_id); +CREATE INDEX IF NOT EXISTS idx_work_locations_tipo ON hr.work_locations(tenant_id, tipo_consultorio); + +CREATE INDEX IF NOT EXISTS idx_skill_types_tenant ON hr.skill_types(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_skills_tenant ON hr.skills(tenant_id); +CREATE INDEX IF NOT EXISTS idx_skills_type ON hr.skills(skill_type_id); +CREATE INDEX IF NOT EXISTS idx_skills_codigo ON hr.skills(codigo_ssa) WHERE codigo_ssa IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_skill_levels_tenant ON hr.skill_levels(tenant_id); +CREATE INDEX IF NOT EXISTS idx_skill_levels_type ON hr.skill_levels(skill_type_id); + +CREATE INDEX IF NOT EXISTS idx_employee_skills_tenant ON hr.employee_skills(tenant_id); +CREATE INDEX IF NOT EXISTS idx_employee_skills_employee ON hr.employee_skills(employee_id); +CREATE INDEX IF NOT EXISTS idx_employee_skills_skill ON hr.employee_skills(skill_id); + +CREATE INDEX IF NOT EXISTS idx_expense_sheets_tenant ON hr.expense_sheets(tenant_id); +CREATE INDEX IF NOT EXISTS idx_expense_sheets_employee ON hr.expense_sheets(employee_id); +CREATE INDEX IF NOT EXISTS idx_expense_sheets_state ON hr.expense_sheets(tenant_id, state); + +CREATE INDEX IF NOT EXISTS idx_expenses_tenant ON hr.expenses(tenant_id); +CREATE INDEX IF NOT EXISTS idx_expenses_sheet ON hr.expenses(sheet_id); +CREATE INDEX IF NOT EXISTS idx_expenses_employee ON hr.expenses(employee_id); + +CREATE INDEX IF NOT EXISTS idx_employee_resume_lines_tenant ON hr.employee_resume_lines(tenant_id); +CREATE INDEX IF NOT EXISTS idx_employee_resume_lines_employee ON hr.employee_resume_lines(employee_id); + +CREATE INDEX IF NOT EXISTS idx_payslip_structures_tenant ON hr.payslip_structures(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_payslips_tenant ON hr.payslips(tenant_id); +CREATE INDEX IF NOT EXISTS idx_payslips_employee ON hr.payslips(employee_id); +CREATE INDEX IF NOT EXISTS idx_payslips_dates ON hr.payslips(tenant_id, date_from, date_to); + +CREATE INDEX IF NOT EXISTS idx_payslip_lines_payslip ON hr.payslip_lines(payslip_id); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE hr.work_locations ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.skill_types ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.skills ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.skill_levels ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.expense_sheets ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.expenses ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.payslip_structures ENABLE ROW LEVEL SECURITY; +ALTER TABLE hr.payslips ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_work_locations ON hr.work_locations; +CREATE POLICY tenant_isolation_work_locations ON hr.work_locations + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_skill_types ON hr.skill_types; +CREATE POLICY tenant_isolation_skill_types ON hr.skill_types + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_skills ON hr.skills; +CREATE POLICY tenant_isolation_skills ON hr.skills + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_skill_levels ON hr.skill_levels; +CREATE POLICY tenant_isolation_skill_levels ON hr.skill_levels + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_expense_sheets ON hr.expense_sheets; +CREATE POLICY tenant_isolation_expense_sheets ON hr.expense_sheets + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_expenses ON hr.expenses; +CREATE POLICY tenant_isolation_expenses ON hr.expenses + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_payslip_structures ON hr.payslip_structures; +CREATE POLICY tenant_isolation_payslip_structures ON hr.payslip_structures + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_payslips ON hr.payslips; +CREATE POLICY tenant_isolation_payslips ON hr.payslips + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN HR EXTENSIONS +-- ============================================================================ diff --git a/schemas/06-inventory-ext-fase8-schema-ddl.sql b/schemas/06-inventory-ext-fase8-schema-ddl.sql new file mode 100644 index 0000000..62552f1 --- /dev/null +++ b/schemas/06-inventory-ext-fase8-schema-ddl.sql @@ -0,0 +1,189 @@ +-- ============================================================================ +-- INVENTORY EXTENSIONS - FASE 8 ERP-Core +-- ERP Clínicas (Base Genérica) +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- ============================================================================ + +-- Schema +CREATE SCHEMA IF NOT EXISTS inventory; + +-- ============================================================================ +-- TABLAS +-- ============================================================================ + +-- Tipos de paquete +CREATE TABLE IF NOT EXISTS inventory.package_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + height NUMERIC(10,2), + width NUMERIC(10,2), + length NUMERIC(10,2), + base_weight NUMERIC(10,2), + max_weight NUMERIC(10,2), + sequence INTEGER DEFAULT 10, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE inventory.package_types IS 'Tipos de paquete - FASE 8'; + +-- Paquetes +CREATE TABLE IF NOT EXISTS inventory.packages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + package_type_id UUID REFERENCES inventory.package_types(id), + name VARCHAR(100), + product_id UUID, + -- Extensiones clínica (medicamentos) + lote VARCHAR(50), + fecha_fabricacion DATE, + fecha_caducidad DATE, + laboratorio VARCHAR(100), + registro_sanitario VARCHAR(50), + -- Control + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE inventory.packages IS 'Paquetes/lotes de productos - FASE 8'; +COMMENT ON COLUMN inventory.packages.lote IS 'Número de lote del fabricante'; +COMMENT ON COLUMN inventory.packages.registro_sanitario IS 'Registro sanitario COFEPRIS'; + +-- Categorías de almacenamiento +CREATE TABLE IF NOT EXISTS inventory.storage_categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100) NOT NULL, + max_weight NUMERIC(10,2), + allow_new_product VARCHAR(20) DEFAULT 'mixed', + -- Extensiones clínica + requiere_refrigeracion BOOLEAN DEFAULT false, + temperatura_min NUMERIC(5,2), + temperatura_max NUMERIC(5,2), + es_controlado BOOLEAN DEFAULT false, + requiere_receta BOOLEAN DEFAULT false, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE inventory.storage_categories IS 'Categorías de almacenamiento - FASE 8'; +COMMENT ON COLUMN inventory.storage_categories.es_controlado IS 'Medicamento controlado (requiere receta especial)'; +COMMENT ON COLUMN inventory.storage_categories.requiere_refrigeracion IS 'Requiere cadena de frío'; + +-- Reglas de ubicación +CREATE TABLE IF NOT EXISTS inventory.putaway_rules ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + name VARCHAR(100), + product_id UUID, + category_id UUID REFERENCES inventory.storage_categories(id), + warehouse_id UUID, + location_in_id UUID, + location_out_id UUID, + sequence INTEGER DEFAULT 10, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE inventory.putaway_rules IS 'Reglas de ubicación automática - FASE 8'; + +-- Estrategias de remoción +CREATE TABLE IF NOT EXISTS inventory.removal_strategies ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(20) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL, + description TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE inventory.removal_strategies IS 'Estrategias de remoción (FIFO, FEFO, etc.) - FASE 8'; + +-- ============================================================================ +-- CAMPOS ADICIONALES A PRODUCTS (si existe) +-- ============================================================================ + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'inventory' AND table_name = 'products') THEN + + -- tracking + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'inventory' AND table_name = 'products' + AND column_name = 'tracking') THEN + ALTER TABLE inventory.products ADD COLUMN tracking VARCHAR(20) DEFAULT 'none'; + END IF; + + -- removal_strategy_id + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'inventory' AND table_name = 'products' + AND column_name = 'removal_strategy_id') THEN + ALTER TABLE inventory.products ADD COLUMN removal_strategy_id UUID + REFERENCES inventory.removal_strategies(id); + END IF; + + -- sale_ok + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'inventory' AND table_name = 'products' + AND column_name = 'sale_ok') THEN + ALTER TABLE inventory.products ADD COLUMN sale_ok BOOLEAN DEFAULT true; + END IF; + + -- purchase_ok + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'inventory' AND table_name = 'products' + AND column_name = 'purchase_ok') THEN + ALTER TABLE inventory.products ADD COLUMN purchase_ok BOOLEAN DEFAULT true; + END IF; + + END IF; +END $$; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_package_types_tenant ON inventory.package_types(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_packages_tenant ON inventory.packages(tenant_id); +CREATE INDEX IF NOT EXISTS idx_packages_type ON inventory.packages(package_type_id); +CREATE INDEX IF NOT EXISTS idx_packages_lote ON inventory.packages(tenant_id, lote); +CREATE INDEX IF NOT EXISTS idx_packages_caducidad ON inventory.packages(tenant_id, fecha_caducidad); + +CREATE INDEX IF NOT EXISTS idx_storage_categories_tenant ON inventory.storage_categories(tenant_id); +CREATE INDEX IF NOT EXISTS idx_storage_categories_controlado + ON inventory.storage_categories(tenant_id, es_controlado) WHERE es_controlado = true; + +CREATE INDEX IF NOT EXISTS idx_putaway_rules_tenant ON inventory.putaway_rules(tenant_id); +CREATE INDEX IF NOT EXISTS idx_putaway_rules_category ON inventory.putaway_rules(category_id); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE inventory.package_types ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.packages ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.storage_categories ENABLE ROW LEVEL SECURITY; +ALTER TABLE inventory.putaway_rules ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_package_types ON inventory.package_types; +CREATE POLICY tenant_isolation_package_types ON inventory.package_types + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_packages ON inventory.packages; +CREATE POLICY tenant_isolation_packages ON inventory.packages + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_storage_categories ON inventory.storage_categories; +CREATE POLICY tenant_isolation_storage_categories ON inventory.storage_categories + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_putaway_rules ON inventory.putaway_rules; +CREATE POLICY tenant_isolation_putaway_rules ON inventory.putaway_rules + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN INVENTORY EXTENSIONS +-- ============================================================================ diff --git a/schemas/07-purchase-ext-fase8-schema-ddl.sql b/schemas/07-purchase-ext-fase8-schema-ddl.sql new file mode 100644 index 0000000..8a235dd --- /dev/null +++ b/schemas/07-purchase-ext-fase8-schema-ddl.sql @@ -0,0 +1,148 @@ +-- ============================================================================ +-- PURCHASE EXTENSIONS - FASE 8 ERP-Core +-- ERP Clínicas (Base Genérica) +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- ============================================================================ + +-- Schema +CREATE SCHEMA IF NOT EXISTS purchase; + +-- ============================================================================ +-- TABLAS +-- ============================================================================ + +-- Información de proveedores por producto +CREATE TABLE IF NOT EXISTS purchase.product_supplierinfo ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + partner_id UUID, + product_id UUID, + product_name VARCHAR(200), + product_code VARCHAR(50), + -- Precios y cantidades + min_qty NUMERIC(10,2) DEFAULT 1, + price NUMERIC(12,4) NOT NULL, + currency_id UUID, + -- Tiempos + delay INTEGER DEFAULT 1, + date_start DATE, + date_end DATE, + -- Extensiones clínica + aplica_iva BOOLEAN DEFAULT true, + requiere_receta BOOLEAN DEFAULT false, + tiempo_entrega_dias INTEGER DEFAULT 1, + -- Control + sequence INTEGER DEFAULT 10, + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE purchase.product_supplierinfo IS 'Información de productos por proveedor - FASE 8'; +COMMENT ON COLUMN purchase.product_supplierinfo.requiere_receta IS 'El producto requiere receta médica'; +COMMENT ON COLUMN purchase.product_supplierinfo.tiempo_entrega_dias IS 'Días estimados de entrega'; + +-- ============================================================================ +-- CAMPOS ADICIONALES A PURCHASE_ORDERS (si existe) +-- ============================================================================ + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'purchase' AND table_name = 'purchase_orders') THEN + + -- origin + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'purchase' AND table_name = 'purchase_orders' + AND column_name = 'origin') THEN + ALTER TABLE purchase.purchase_orders ADD COLUMN origin VARCHAR(100); + END IF; + + -- partner_ref + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'purchase' AND table_name = 'purchase_orders' + AND column_name = 'partner_ref') THEN + ALTER TABLE purchase.purchase_orders ADD COLUMN partner_ref VARCHAR(100); + END IF; + + -- date_approve + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'purchase' AND table_name = 'purchase_orders' + AND column_name = 'date_approve') THEN + ALTER TABLE purchase.purchase_orders ADD COLUMN date_approve TIMESTAMPTZ; + END IF; + + -- receipt_status + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = 'purchase' AND table_name = 'purchase_orders' + AND column_name = 'receipt_status') THEN + ALTER TABLE purchase.purchase_orders ADD COLUMN receipt_status VARCHAR(20); + END IF; + + END IF; +END $$; + +-- ============================================================================ +-- FUNCIONES +-- ============================================================================ + +-- Función para crear movimientos de stock +CREATE OR REPLACE FUNCTION purchase.action_create_stock_moves(p_order_id UUID) +RETURNS JSONB +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + v_result JSONB := '{"moves_created": 0, "errors": []}'::JSONB; + v_move_count INTEGER := 0; +BEGIN + -- Verificar que la orden existe + IF NOT EXISTS ( + SELECT 1 FROM purchase.purchase_orders + WHERE id = p_order_id + ) THEN + v_result := jsonb_set(v_result, '{errors}', + v_result->'errors' || '["Orden de compra no encontrada"]'::JSONB); + RETURN v_result; + END IF; + + -- Aquí se crearían los movimientos de stock + -- La implementación depende de la estructura de inventory.stock_moves + + v_result := jsonb_set(v_result, '{moves_created}', to_jsonb(v_move_count)); + v_result := jsonb_set(v_result, '{status}', '"success"'::JSONB); + + RETURN v_result; +END; +$$; + +COMMENT ON FUNCTION purchase.action_create_stock_moves IS 'Crea movimientos de stock desde orden de compra - FASE 8'; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_product_supplierinfo_tenant + ON purchase.product_supplierinfo(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_supplierinfo_partner + ON purchase.product_supplierinfo(partner_id); +CREATE INDEX IF NOT EXISTS idx_product_supplierinfo_product + ON purchase.product_supplierinfo(product_id); +CREATE INDEX IF NOT EXISTS idx_product_supplierinfo_dates + ON purchase.product_supplierinfo(tenant_id, date_start, date_end); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE purchase.product_supplierinfo ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_supplierinfo ON purchase.product_supplierinfo; +CREATE POLICY tenant_isolation_supplierinfo ON purchase.product_supplierinfo + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN PURCHASE EXTENSIONS +-- ============================================================================ diff --git a/schemas/08-clinica-ext-fase8-schema-ddl.sql b/schemas/08-clinica-ext-fase8-schema-ddl.sql new file mode 100644 index 0000000..2aaaa0b --- /dev/null +++ b/schemas/08-clinica-ext-fase8-schema-ddl.sql @@ -0,0 +1,151 @@ +-- ============================================================================ +-- CLINICA EXTENSIONS - FASE 8 ERP-Core +-- ERP Clínicas (Base Genérica) +-- ============================================================================ +-- Fecha: 2026-01-04 +-- Versión: 1.0 +-- ============================================================================ + +-- El schema clinica ya existe (03-clinical-tables.sql) + +-- ============================================================================ +-- TABLAS +-- ============================================================================ + +-- Personal de clínica (adaptación de collaborators) +CREATE TABLE IF NOT EXISTS clinica.personal_clinica ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + employee_id UUID NOT NULL, + consultorio_id UUID, + -- Datos del rol + rol VARCHAR(50) NOT NULL, -- 'medico', 'enfermera', 'recepcion', 'auxiliar', 'laboratorio', 'farmacia' + vigencia_desde DATE, + vigencia_hasta DATE, + es_titular BOOLEAN DEFAULT false, + horario JSONB, -- {"lunes": {"inicio": "09:00", "fin": "18:00"}, ...} + -- Control + active BOOLEAN DEFAULT true, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE clinica.personal_clinica IS 'Personal asignado a consultorios - FASE 8'; +COMMENT ON COLUMN clinica.personal_clinica.rol IS 'Rol del personal en el consultorio'; +COMMENT ON COLUMN clinica.personal_clinica.horario IS 'Horario de trabajo en formato JSON'; + +-- Calificaciones/Ratings +CREATE TABLE IF NOT EXISTS clinica.ratings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + consultation_id UUID, + patient_id UUID, + doctor_id UUID, + -- Calificación + rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), + feedback TEXT, + -- Aspectos específicos + puntualidad INTEGER CHECK (puntualidad BETWEEN 1 AND 5), + atencion INTEGER CHECK (atencion BETWEEN 1 AND 5), + instalaciones INTEGER CHECK (instalaciones BETWEEN 1 AND 5), + -- Metadata + rated_at TIMESTAMPTZ DEFAULT NOW(), + is_anonymous BOOLEAN DEFAULT false, + -- Control + created_at TIMESTAMPTZ DEFAULT NOW() +); + +COMMENT ON TABLE clinica.ratings IS 'Calificaciones de consultas - FASE 8'; +COMMENT ON COLUMN clinica.ratings.rating IS 'Calificación general de 1 a 5'; + +-- ============================================================================ +-- FKs OPCIONALES +-- ============================================================================ + +DO $$ +BEGIN + -- FK personal_clinica → hr.work_locations + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'hr' AND table_name = 'work_locations') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_personal_consultorio') THEN + ALTER TABLE clinica.personal_clinica + ADD CONSTRAINT fk_personal_consultorio + FOREIGN KEY (consultorio_id) REFERENCES hr.work_locations(id) ON DELETE SET NULL; + END IF; + END IF; + + -- FK ratings → clinica.consultations + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'clinica' AND table_name = 'consultations') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_rating_consultation') THEN + ALTER TABLE clinica.ratings + ADD CONSTRAINT fk_rating_consultation + FOREIGN KEY (consultation_id) REFERENCES clinica.consultations(id) ON DELETE SET NULL; + END IF; + END IF; + + -- FK ratings → clinica.patients + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'clinica' AND table_name = 'patients') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_rating_patient') THEN + ALTER TABLE clinica.ratings + ADD CONSTRAINT fk_rating_patient + FOREIGN KEY (patient_id) REFERENCES clinica.patients(id) ON DELETE SET NULL; + END IF; + END IF; + + -- FK ratings → clinica.doctors + IF EXISTS (SELECT 1 FROM information_schema.tables + WHERE table_schema = 'clinica' AND table_name = 'doctors') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.table_constraints + WHERE constraint_name = 'fk_rating_doctor') THEN + ALTER TABLE clinica.ratings + ADD CONSTRAINT fk_rating_doctor + FOREIGN KEY (doctor_id) REFERENCES clinica.doctors(id) ON DELETE SET NULL; + END IF; + END IF; +END $$; + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +CREATE INDEX IF NOT EXISTS idx_personal_clinica_tenant + ON clinica.personal_clinica(tenant_id); +CREATE INDEX IF NOT EXISTS idx_personal_clinica_employee + ON clinica.personal_clinica(employee_id); +CREATE INDEX IF NOT EXISTS idx_personal_clinica_consultorio + ON clinica.personal_clinica(consultorio_id); +CREATE INDEX IF NOT EXISTS idx_personal_clinica_rol + ON clinica.personal_clinica(tenant_id, rol); + +CREATE INDEX IF NOT EXISTS idx_ratings_tenant + ON clinica.ratings(tenant_id); +CREATE INDEX IF NOT EXISTS idx_ratings_consultation + ON clinica.ratings(consultation_id); +CREATE INDEX IF NOT EXISTS idx_ratings_doctor + ON clinica.ratings(doctor_id); +CREATE INDEX IF NOT EXISTS idx_ratings_patient + ON clinica.ratings(patient_id); + +-- ============================================================================ +-- RLS +-- ============================================================================ + +ALTER TABLE clinica.personal_clinica ENABLE ROW LEVEL SECURITY; +ALTER TABLE clinica.ratings ENABLE ROW LEVEL SECURITY; + +DROP POLICY IF EXISTS tenant_isolation_personal_clinica ON clinica.personal_clinica; +CREATE POLICY tenant_isolation_personal_clinica ON clinica.personal_clinica + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +DROP POLICY IF EXISTS tenant_isolation_ratings ON clinica.ratings; +CREATE POLICY tenant_isolation_ratings ON clinica.ratings + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- ============================================================================ +-- FIN CLINICA EXTENSIONS +-- ============================================================================ diff --git a/seeds/fase8/00-removal-strategies.sql b/seeds/fase8/00-removal-strategies.sql new file mode 100644 index 0000000..016ef42 --- /dev/null +++ b/seeds/fase8/00-removal-strategies.sql @@ -0,0 +1,11 @@ +-- ============================================================================ +-- SEED DATA: Estrategias de Remoción +-- FASE-8 ERP-Core - ERP Clínicas +-- ============================================================================ + +INSERT INTO inventory.removal_strategies (code, name, description) VALUES + ('fifo', 'First In First Out', 'Salida por fecha de entrada más antigua'), + ('lifo', 'Last In First Out', 'Salida por fecha de entrada más reciente'), + ('fefo', 'First Expired First Out', 'Salida por fecha de caducidad más próxima - RECOMENDADO para medicamentos'), + ('closest', 'Closest Location', 'Salida por ubicación más cercana') +ON CONFLICT (code) DO NOTHING; diff --git a/seeds/fase8/01-clinica-skills.sql b/seeds/fase8/01-clinica-skills.sql new file mode 100644 index 0000000..3f8a646 --- /dev/null +++ b/seeds/fase8/01-clinica-skills.sql @@ -0,0 +1,103 @@ +-- ============================================================================ +-- SEED DATA: Habilidades/Especialidades Médicas +-- FASE-8 ERP-Core - ERP Clínicas +-- ============================================================================ +-- NOTA: Ejecutar después de SET app.current_tenant_id = 'UUID-DEL-TENANT'; +-- ============================================================================ + +-- Tipos de habilidad médica +INSERT INTO hr.skill_types (tenant_id, name) +SELECT current_setting('app.current_tenant_id', true)::UUID, name +FROM (VALUES + ('Especialidad Médica'), + ('Subespecialidad'), + ('Certificación'), + ('Curso/Diplomado'), + ('Idioma') +) AS t(name) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT DO NOTHING; + +-- Niveles de habilidad (para cada tipo) +INSERT INTO hr.skill_levels (tenant_id, skill_type_id, name, level) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + st.id, + l.name, + l.level +FROM hr.skill_types st +CROSS JOIN (VALUES + ('Residente', 1), + ('Especialista', 2), + ('Subespecialista', 3), + ('Fellow', 4) +) AS l(name, level) +WHERE st.tenant_id = current_setting('app.current_tenant_id', true)::UUID + AND st.name IN ('Especialidad Médica', 'Subespecialidad') +ON CONFLICT DO NOTHING; + +-- Niveles para certificaciones +INSERT INTO hr.skill_levels (tenant_id, skill_type_id, name, level) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + st.id, + l.name, + l.level +FROM hr.skill_types st +CROSS JOIN (VALUES + ('En trámite', 1), + ('Vigente', 2), + ('Recertificado', 3) +) AS l(name, level) +WHERE st.tenant_id = current_setting('app.current_tenant_id', true)::UUID + AND st.name = 'Certificación' +ON CONFLICT DO NOTHING; + +-- Especialidades médicas comunes +INSERT INTO hr.skills (tenant_id, skill_type_id, name, codigo_ssa, requiere_cedula) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + id, + unnest(ARRAY[ + 'Medicina General', + 'Medicina Familiar', + 'Pediatría', + 'Ginecología y Obstetricia', + 'Medicina Interna', + 'Cardiología', + 'Dermatología', + 'Oftalmología', + 'Otorrinolaringología', + 'Traumatología y Ortopedia', + 'Neurología', + 'Psiquiatría', + 'Urología', + 'Gastroenterología', + 'Neumología' + ]), + NULL, + true +FROM hr.skill_types +WHERE name = 'Especialidad Médica' + AND tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; + +-- Certificaciones comunes +INSERT INTO hr.skills (tenant_id, skill_type_id, name, requiere_cedula) +SELECT + current_setting('app.current_tenant_id', true)::UUID, + id, + unnest(ARRAY[ + 'Consejo de Especialidad', + 'COFEPRIS', + 'BLS (Basic Life Support)', + 'ACLS (Advanced Cardiac Life Support)', + 'PALS (Pediatric Advanced Life Support)', + 'NOM-024-SSA3 Expediente Clínico' + ]), + false +FROM hr.skill_types +WHERE name = 'Certificación' + AND tenant_id = current_setting('app.current_tenant_id', true)::UUID +ON CONFLICT DO NOTHING; diff --git a/seeds/fase8/02-clinica-catalogos.sql b/seeds/fase8/02-clinica-catalogos.sql new file mode 100644 index 0000000..00b73ab --- /dev/null +++ b/seeds/fase8/02-clinica-catalogos.sql @@ -0,0 +1,83 @@ +-- ============================================================================ +-- SEED DATA: Catálogos de Clínica +-- FASE-8 ERP-Core - ERP Clínicas +-- ============================================================================ +-- NOTA: Ejecutar después de SET app.current_tenant_id = 'UUID-DEL-TENANT'; +-- ============================================================================ + +-- Categorías de almacén para clínicas +INSERT INTO inventory.storage_categories (tenant_id, name, max_weight, allow_new_product, + requiere_refrigeracion, temperatura_min, temperatura_max, es_controlado, requiere_receta) +SELECT current_setting('app.current_tenant_id', true)::UUID, name, max_weight, allow_new_product, + requiere_refrigeracion, temperatura_min, temperatura_max, es_controlado, requiere_receta +FROM (VALUES + ('Farmacia General', 5000.0, 'mixed', false, NULL, NULL, false, false), + ('Refrigerados', 500.0, 'same', true, 2.0, 8.0, false, true), + ('Medicamentos Controlados', 100.0, 'same', false, NULL, NULL, true, true), + ('Material Quirúrgico', 1000.0, 'mixed', false, NULL, NULL, false, false), + ('Insumos Laboratorio', 500.0, 'mixed', false, NULL, NULL, false, false), + ('Vacunas', 200.0, 'same', true, 2.0, 8.0, false, true) +) AS t(name, max_weight, allow_new_product, requiere_refrigeracion, temperatura_min, temperatura_max, es_controlado, requiere_receta) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT DO NOTHING; + +-- Tipos de paquete para medicamentos +INSERT INTO inventory.package_types (tenant_id, name, height, width, length, base_weight, max_weight, sequence) +SELECT current_setting('app.current_tenant_id', true)::UUID, name, height, width, length, base_weight, max_weight, seq +FROM (VALUES + ('Caja Medicamentos', 20.0, 15.0, 10.0, 0.1, 2.0, 10), + ('Blister', 15.0, 10.0, 1.0, 0.01, 0.1, 20), + ('Frasco', 10.0, 5.0, 5.0, 0.05, 0.5, 30), + ('Ampolleta', 8.0, 2.0, 2.0, 0.01, 0.05, 40), + ('Bolsa Suero', 30.0, 20.0, 5.0, 0.1, 1.0, 50), + ('Jeringa', 15.0, 3.0, 3.0, 0.02, 0.1, 60) +) AS t(name, height, width, length, base_weight, max_weight, seq) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT DO NOTHING; + +-- Métodos de pago para clínicas +INSERT INTO financial.payment_methods (tenant_id, name, code, payment_type, aplica_seguro, requiere_factura, porcentaje_seguro) +SELECT current_setting('app.current_tenant_id', true)::UUID, name, code, payment_type::financial.payment_method_type, aplica_seguro, requiere_factura, porcentaje_seguro +FROM (VALUES + ('Efectivo', 'efectivo', 'inbound', false, false, 0), + ('Tarjeta Débito', 'td', 'inbound', false, false, 0), + ('Tarjeta Crédito', 'tc', 'inbound', false, true, 0), + ('Transferencia', 'transfer', 'inbound', false, true, 0), + ('Seguro GMM', 'seguro_gmm', 'inbound', true, true, 80), + ('Seguro GMA', 'seguro_gma', 'inbound', true, true, 70), + ('Convenio Empresa', 'convenio', 'inbound', false, true, 0) +) AS t(name, code, payment_type, aplica_seguro, requiere_factura, porcentaje_seguro) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT (tenant_id, code) DO NOTHING; + +-- Estructuras de nómina para clínicas +INSERT INTO hr.payslip_structures (tenant_id, name, code, tipo_pago) +SELECT current_setting('app.current_tenant_id', true)::UUID, name, code, tipo_pago +FROM (VALUES + ('Nómina Quincenal', 'NOM-QUIN', 'quincenal'), + ('Nómina Semanal', 'NOM-SEM', 'semanal'), + ('Honorarios Médicos', 'HON-MED', 'honorarios'), + ('Pago por Guardia', 'PAG-GUAR', 'guardia'), + ('Comisión por Consulta', 'COM-CONS', 'comision') +) AS t(name, code, tipo_pago) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT (tenant_id, code) DO NOTHING; + +-- Tipos de consultorio +INSERT INTO hr.work_locations (tenant_id, name, tipo_consultorio, capacidad) +SELECT current_setting('app.current_tenant_id', true)::UUID, name, tipo, capacidad +FROM (VALUES + ('Consultorio 1', 'general', 1), + ('Consultorio 2', 'general', 1), + ('Consultorio Especialidad', 'especialidad', 1), + ('Área de Urgencias', 'urgencias', 3), + ('Laboratorio', 'laboratorio', 5), + ('Farmacia', 'farmacia', 2) +) AS t(name, tipo, capacidad) +WHERE current_setting('app.current_tenant_id', true) IS NOT NULL + AND current_setting('app.current_tenant_id', true) != '' +ON CONFLICT DO NOTHING;