Migración desde erp-clinicas/database - Estándar multi-repo v2

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-16 08:12:00 -06:00
parent 241058d159
commit cf07a84e26
17 changed files with 2680 additions and 2 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
dist/
coverage/
.env

222
HERENCIA-ERP-CORE.md Normal file
View File

@ -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

View File

@ -1,3 +1,83 @@
# erp-clinicas-database-v2
# Base de Datos - ERP Clínicas
Database de erp-clinicas - Workspace V2
## 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)

25
init/00-extensions.sql Normal file
View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

37
init/02-rls-functions.sql Normal file
View File

@ -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
-- ============================================================================

628
init/03-clinical-tables.sql Normal file
View File

@ -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
-- ============================================================================

34
init/04-seed-data.sql Normal file
View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

View File

@ -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;

View File

@ -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;

View File

@ -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;