# DDL-SPEC: Schema core_catalogs

## Identificacion

| Campo | Valor |
|-------|-------|
| **Schema** | core_catalogs |
| **Modulo** | MGN-005 |
| **Version** | 1.0 |
| **Estado** | En Diseno |
| **Autor** | Requirements-Analyst |
| **Fecha** | 2025-12-05 |

---

## Descripcion General

El schema `core_catalogs` contiene los catalogos maestros del ERP: contactos (clientes, proveedores, empleados), paises/estados, monedas, unidades de medida, y categorias/tags. Es un modulo transversal utilizado por todos los demas modulos de negocio.

### Alcance

- Gestion de contactos (partners) con tipos y roles
- Catalogos globales (paises, estados, monedas) - Sin RLS
- Catalogos por tenant (UoM, categorias, tags) - Con RLS
- Tasas de cambio historicas por tenant

### RF Cubiertos

| RF | Titulo | Tablas |
|----|--------|--------|
| RF-CATALOG-001 | Gestion de Contactos | contacts, contact_tags, contact_tag_rel |
| RF-CATALOG-002 | Paises y Estados | countries, states, country_groups |
| RF-CATALOG-003 | Monedas y Tasas | currencies, currency_rates |
| RF-CATALOG-004 | Unidades de Medida | uom_categories, uom |
| RF-CATALOG-005 | Categorias y Tags | categories, contact_tags |

---

## Diagrama Entidad-Relacion

```mermaid
erDiagram
    %% Catalogos Globales (sin tenant_id)
    countries ||--o{ states : "tiene"
    countries ||--o{ country_group_rel : "pertenece"
    country_groups ||--o{ country_group_rel : "incluye"
    countries ||--o| currencies : "moneda_oficial"

    %% Contactos (con tenant_id)
    contacts ||--o{ contacts : "parent/children"
    contacts ||--o{ contact_tag_rel : "tiene"
    contact_tags ||--o{ contact_tag_rel : "asignado_a"
    contacts }o--|| countries : "pais"
    contacts }o--|| states : "estado"
    contacts }o--|| currencies : "moneda_preferida"

    %% Monedas y Tasas
    currencies ||--o{ currency_rates : "tasas"

    %% Unidades de Medida
    uom_categories ||--o{ uom : "contiene"

    %% Categorias Genericas
    categories ||--o{ categories : "jerarquia"

    countries {
        uuid id PK
        varchar name
        char code
        char code3
        int phone_code
        uuid currency_id FK
        boolean is_active
    }

    states {
        uuid id PK
        uuid country_id FK
        varchar name
        varchar code
        boolean is_active
    }

    currencies {
        uuid id PK
        char name
        varchar full_name
        varchar symbol
        int decimal_places
        boolean is_active
    }

    contacts {
        uuid id PK
        uuid tenant_id FK
        uuid parent_id FK
        varchar name
        varchar contact_type
        jsonb roles
        varchar email
        varchar phone
        varchar vat
        uuid country_id FK
        uuid state_id FK
        boolean is_active
    }

    contact_tags {
        uuid id PK
        uuid tenant_id FK
        uuid parent_id FK
        varchar name
        int color
        boolean is_active
    }

    uom_categories {
        uuid id PK
        uuid tenant_id FK
        varchar name
        boolean is_active
    }

    uom {
        uuid id PK
        uuid tenant_id FK
        uuid category_id FK
        varchar name
        varchar uom_type
        decimal factor
        boolean is_active
    }

    categories {
        uuid id PK
        uuid tenant_id FK
        uuid parent_id FK
        varchar entity_type
        varchar name
        varchar code
        varchar parent_path
        boolean is_active
    }
```

---

## Tablas Globales (Sin RLS)

### 1. countries

Catalogo de paises segun ISO 3166-1.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre del pais |
| `code` | CHAR(2) | NOT NULL | - | Codigo ISO 3166-1 alpha-2 |
| `code3` | CHAR(3) | NOT NULL | - | Codigo ISO 3166-1 alpha-3 |
| `numeric_code` | INTEGER | NULL | - | Codigo numerico ISO |
| `phone_code` | INTEGER | NULL | - | Codigo telefonico |
| `currency_id` | UUID | NULL | - | Moneda oficial |
| `address_format` | TEXT | NULL | - | Template de direccion |
| `vat_label` | VARCHAR(20) | NULL | 'VAT' | Etiqueta para identificador fiscal |
| `state_required` | BOOLEAN | NOT NULL | false | Estado obligatorio |
| `zip_required` | BOOLEAN | NOT NULL | false | CP obligatorio |
| `is_active` | BOOLEAN | NOT NULL | true | Disponible para uso |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

#### Constraints

```sql
CONSTRAINT pk_countries PRIMARY KEY (id),
CONSTRAINT uk_countries_code UNIQUE (code),
CONSTRAINT uk_countries_code3 UNIQUE (code3),
CONSTRAINT fk_countries_currency
    FOREIGN KEY (currency_id) REFERENCES core_catalogs.currencies(id)
```

#### Indices

```sql
CREATE INDEX idx_countries_code ON core_catalogs.countries(code);
CREATE INDEX idx_countries_name ON core_catalogs.countries(name);
CREATE INDEX idx_countries_active ON core_catalogs.countries(is_active) WHERE is_active = true;
```

---

### 2. states

Estados/provincias por pais segun ISO 3166-2.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `country_id` | UUID | NOT NULL | - | FK a countries |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre del estado |
| `code` | VARCHAR(10) | NOT NULL | - | Codigo ISO 3166-2 |
| `is_active` | BOOLEAN | NOT NULL | true | Disponible para uso |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

#### Constraints

```sql
CONSTRAINT pk_states PRIMARY KEY (id),
CONSTRAINT uk_states_code UNIQUE (code),
CONSTRAINT fk_states_country
    FOREIGN KEY (country_id) REFERENCES core_catalogs.countries(id) ON DELETE CASCADE
```

#### Indices

```sql
CREATE INDEX idx_states_country_id ON core_catalogs.states(country_id);
CREATE INDEX idx_states_code ON core_catalogs.states(code);
CREATE INDEX idx_states_name ON core_catalogs.states(name);
```

---

### 3. country_groups

Agrupaciones regionales de paises.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre del grupo |
| `code` | VARCHAR(20) | NOT NULL | - | Codigo del grupo |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

#### Constraints

```sql
CONSTRAINT pk_country_groups PRIMARY KEY (id),
CONSTRAINT uk_country_groups_code UNIQUE (code)
```

---

### 4. country_group_rel

Relacion M:N paises-grupos.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `country_id` | UUID | NOT NULL | - | FK a countries |
| `group_id` | UUID | NOT NULL | - | FK a country_groups |

#### Constraints

```sql
CONSTRAINT pk_country_group_rel PRIMARY KEY (country_id, group_id),
CONSTRAINT fk_country_group_rel_country
    FOREIGN KEY (country_id) REFERENCES core_catalogs.countries(id) ON DELETE CASCADE,
CONSTRAINT fk_country_group_rel_group
    FOREIGN KEY (group_id) REFERENCES core_catalogs.country_groups(id) ON DELETE CASCADE
```

---

### 5. currencies

Catalogo de monedas segun ISO 4217.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `name` | CHAR(3) | NOT NULL | - | Codigo ISO 4217 |
| `full_name` | VARCHAR(100) | NOT NULL | - | Nombre completo |
| `symbol` | VARCHAR(10) | NOT NULL | - | Simbolo |
| `iso_numeric` | INTEGER | NULL | - | Codigo numerico ISO |
| `decimal_places` | INTEGER | NOT NULL | 2 | Decimales |
| `rounding` | DECIMAL(10,6) | NOT NULL | 0.01 | Factor redondeo |
| `position` | VARCHAR(10) | NOT NULL | 'before' | Posicion simbolo |
| `is_active` | BOOLEAN | NOT NULL | true | Disponible |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

#### Constraints

```sql
CONSTRAINT pk_currencies PRIMARY KEY (id),
CONSTRAINT uk_currencies_name UNIQUE (name),
CONSTRAINT chk_currencies_position CHECK (position IN ('before', 'after')),
CONSTRAINT chk_currencies_decimal_places CHECK (decimal_places >= 0 AND decimal_places <= 6)
```

#### Indices

```sql
CREATE INDEX idx_currencies_name ON core_catalogs.currencies(name);
CREATE INDEX idx_currencies_active ON core_catalogs.currencies(is_active) WHERE is_active = true;
```

---

## Tablas por Tenant (Con RLS)

### 6. contacts

Contactos: clientes, proveedores, empleados y otros terceros.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `parent_id` | UUID | NULL | - | Empresa padre (para contactos) |
| `name` | VARCHAR(255) | NOT NULL | - | Nombre completo |
| `display_name` | VARCHAR(500) | NULL | - | Computed: parent + name |
| `contact_type` | VARCHAR(20) | NOT NULL | 'company' | company/individual/contact |
| `roles` | JSONB | NOT NULL | '[]' | Roles: customer, vendor, employee |
| `ref` | VARCHAR(50) | NULL | - | Codigo interno |
| `vat` | VARCHAR(50) | NULL | - | RFC/NIT/RUT |
| `email` | VARCHAR(255) | NULL | - | Email principal |
| `phone` | VARCHAR(50) | NULL | - | Telefono fijo |
| `mobile` | VARCHAR(50) | NULL | - | Celular |
| `website` | VARCHAR(255) | NULL | - | Sitio web |
| `street` | VARCHAR(255) | NULL | - | Calle |
| `street2` | VARCHAR(255) | NULL | - | Linea 2 |
| `city` | VARCHAR(100) | NULL | - | Ciudad |
| `zip` | VARCHAR(20) | NULL | - | Codigo postal |
| `state_id` | UUID | NULL | - | FK a states |
| `country_id` | UUID | NULL | - | FK a countries |
| `function` | VARCHAR(100) | NULL | - | Cargo/puesto |
| `user_id` | UUID | NULL | - | FK a users (vinculacion) |
| `currency_id` | UUID | NULL | - | Moneda preferida |
| `payment_term_days` | INTEGER | NULL | - | Dias de credito |
| `credit_limit` | DECIMAL(15,2) | NULL | 0 | Limite de credito |
| `bank_accounts` | JSONB | NULL | '[]' | Cuentas bancarias |
| `notes` | TEXT | NULL | - | Notas |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |
| `created_by` | UUID | NULL | - | Usuario creador |
| `updated_by` | UUID | NULL | - | Usuario modificador |

#### Constraints

```sql
CONSTRAINT pk_contacts PRIMARY KEY (id),
CONSTRAINT fk_contacts_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_contacts_parent
    FOREIGN KEY (parent_id) REFERENCES core_catalogs.contacts(id) ON DELETE CASCADE,
CONSTRAINT fk_contacts_state
    FOREIGN KEY (state_id) REFERENCES core_catalogs.states(id),
CONSTRAINT fk_contacts_country
    FOREIGN KEY (country_id) REFERENCES core_catalogs.countries(id),
CONSTRAINT fk_contacts_currency
    FOREIGN KEY (currency_id) REFERENCES core_catalogs.currencies(id),
CONSTRAINT fk_contacts_user
    FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE SET NULL,
CONSTRAINT uk_contacts_vat_tenant UNIQUE (tenant_id, vat) WHERE vat IS NOT NULL,
CONSTRAINT uk_contacts_ref_tenant UNIQUE (tenant_id, ref) WHERE ref IS NOT NULL,
CONSTRAINT uk_contacts_user UNIQUE (user_id) WHERE user_id IS NOT NULL,
CONSTRAINT chk_contacts_type CHECK (contact_type IN ('company', 'individual', 'contact')),
CONSTRAINT chk_contacts_contact_has_parent
    CHECK (contact_type != 'contact' OR parent_id IS NOT NULL),
CONSTRAINT chk_contacts_credit_limit CHECK (credit_limit >= 0)
```

#### Indices

```sql
CREATE INDEX idx_contacts_tenant_id ON core_catalogs.contacts(tenant_id);
CREATE INDEX idx_contacts_parent_id ON core_catalogs.contacts(parent_id);
CREATE INDEX idx_contacts_type ON core_catalogs.contacts(contact_type);
CREATE INDEX idx_contacts_email ON core_catalogs.contacts(email);
CREATE INDEX idx_contacts_vat ON core_catalogs.contacts(vat) WHERE vat IS NOT NULL;
CREATE INDEX idx_contacts_roles ON core_catalogs.contacts USING gin(roles);
CREATE INDEX idx_contacts_search ON core_catalogs.contacts
    USING gin(to_tsvector('spanish', name || ' ' || COALESCE(email, '') || ' ' || COALESCE(vat, '')));
```

---

### 7. contact_tags

Tags para clasificar contactos.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `parent_id` | UUID | NULL | - | Tag padre |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre del tag |
| `color` | INTEGER | NOT NULL | 1 | Color (1-11) |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

#### Constraints

```sql
CONSTRAINT pk_contact_tags PRIMARY KEY (id),
CONSTRAINT fk_contact_tags_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_contact_tags_parent
    FOREIGN KEY (parent_id) REFERENCES core_catalogs.contact_tags(id) ON DELETE CASCADE,
CONSTRAINT uk_contact_tags_name_parent UNIQUE (tenant_id, parent_id, name),
CONSTRAINT chk_contact_tags_color CHECK (color >= 1 AND color <= 11)
```

---

### 8. contact_tag_rel

Relacion M:N contactos-tags.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `contact_id` | UUID | NOT NULL | - | FK a contacts |
| `tag_id` | UUID | NOT NULL | - | FK a contact_tags |

#### Constraints

```sql
CONSTRAINT pk_contact_tag_rel PRIMARY KEY (contact_id, tag_id),
CONSTRAINT fk_contact_tag_rel_contact
    FOREIGN KEY (contact_id) REFERENCES core_catalogs.contacts(id) ON DELETE CASCADE,
CONSTRAINT fk_contact_tag_rel_tag
    FOREIGN KEY (tag_id) REFERENCES core_catalogs.contact_tags(id) ON DELETE CASCADE
```

---

### 9. currency_rates

Tasas de cambio historicas por tenant.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `currency_id` | UUID | NOT NULL | - | FK a currencies |
| `rate` | DECIMAL(20,10) | NOT NULL | - | Tasa vs moneda base |
| `name` | DATE | NOT NULL | - | Fecha de la tasa |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `created_by` | UUID | NULL | - | Usuario creador |

#### Constraints

```sql
CONSTRAINT pk_currency_rates PRIMARY KEY (id),
CONSTRAINT fk_currency_rates_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_currency_rates_currency
    FOREIGN KEY (currency_id) REFERENCES core_catalogs.currencies(id),
CONSTRAINT uk_currency_rates_tenant_currency_date UNIQUE (tenant_id, currency_id, name),
CONSTRAINT chk_currency_rates_positive CHECK (rate > 0)
```

#### Indices

```sql
CREATE INDEX idx_currency_rates_tenant_id ON core_catalogs.currency_rates(tenant_id);
CREATE INDEX idx_currency_rates_currency_id ON core_catalogs.currency_rates(currency_id);
CREATE INDEX idx_currency_rates_date ON core_catalogs.currency_rates(name DESC);
CREATE INDEX idx_currency_rates_lookup
    ON core_catalogs.currency_rates(tenant_id, currency_id, name DESC);
```

---

### 10. uom_categories

Categorias de unidades de medida.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre categoria |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

#### Constraints

```sql
CONSTRAINT pk_uom_categories PRIMARY KEY (id),
CONSTRAINT fk_uom_categories_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT uk_uom_categories_name_tenant UNIQUE (tenant_id, name)
```

---

### 11. uom

Unidades de medida.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `category_id` | UUID | NOT NULL | - | FK a uom_categories |
| `name` | VARCHAR(50) | NOT NULL | - | Nombre unidad |
| `uom_type` | VARCHAR(20) | NOT NULL | - | reference/bigger/smaller |
| `factor` | DECIMAL(20,10) | NOT NULL | 1 | Factor de conversion |
| `rounding` | DECIMAL(10,6) | NOT NULL | 0.01 | Precision redondeo |
| `is_protected` | BOOLEAN | NOT NULL | false | Protegida de edicion |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

#### Constraints

```sql
CONSTRAINT pk_uom PRIMARY KEY (id),
CONSTRAINT fk_uom_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_uom_category
    FOREIGN KEY (category_id) REFERENCES core_catalogs.uom_categories(id) ON DELETE CASCADE,
CONSTRAINT uk_uom_name_category UNIQUE (category_id, name),
CONSTRAINT chk_uom_type CHECK (uom_type IN ('reference', 'bigger', 'smaller')),
CONSTRAINT chk_uom_factor CHECK (factor > 0),
CONSTRAINT chk_uom_reference_factor CHECK (uom_type != 'reference' OR factor = 1)
```

#### Indices

```sql
CREATE INDEX idx_uom_tenant_id ON core_catalogs.uom(tenant_id);
CREATE INDEX idx_uom_category_id ON core_catalogs.uom(category_id);
CREATE INDEX idx_uom_type ON core_catalogs.uom(uom_type);
```

---

### 12. categories

Categorias genericas para diferentes entidades.

#### Estructura

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `entity_type` | VARCHAR(50) | NOT NULL | - | Tipo: contact, product, expense |
| `parent_id` | UUID | NULL | - | Categoria padre |
| `parent_path` | VARCHAR(255) | NULL | - | Path materializado |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre categoria |
| `code` | VARCHAR(50) | NULL | - | Codigo unico |
| `sort_order` | INTEGER | NOT NULL | 0 | Orden visualizacion |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

#### Constraints

```sql
CONSTRAINT pk_categories PRIMARY KEY (id),
CONSTRAINT fk_categories_tenant
    FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_categories_parent
    FOREIGN KEY (parent_id) REFERENCES core_catalogs.categories(id) ON DELETE CASCADE,
CONSTRAINT uk_categories_code_type_tenant UNIQUE (tenant_id, entity_type, code) WHERE code IS NOT NULL,
CONSTRAINT chk_categories_entity_type
    CHECK (entity_type IN ('contact', 'product', 'expense', 'document', 'project'))
```

#### Indices

```sql
CREATE INDEX idx_categories_tenant_id ON core_catalogs.categories(tenant_id);
CREATE INDEX idx_categories_parent_id ON core_catalogs.categories(parent_id);
CREATE INDEX idx_categories_entity_type ON core_catalogs.categories(entity_type);
CREATE INDEX idx_categories_parent_path ON core_catalogs.categories
    USING gin (parent_path gin_trgm_ops);
```

---

## Row Level Security (RLS)

### Politicas para Tablas con Tenant

```sql
-- Habilitar RLS
ALTER TABLE core_catalogs.contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.contact_tags ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.contact_tag_rel ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.currency_rates ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.uom_categories ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.uom ENABLE ROW LEVEL SECURITY;
ALTER TABLE core_catalogs.categories ENABLE ROW LEVEL SECURITY;

-- Politica de aislamiento por tenant
CREATE POLICY tenant_isolation_contacts ON core_catalogs.contacts
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_contact_tags ON core_catalogs.contact_tags
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_currency_rates ON core_catalogs.currency_rates
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_uom_categories ON core_catalogs.uom_categories
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_uom ON core_catalogs.uom
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

CREATE POLICY tenant_isolation_categories ON core_catalogs.categories
    FOR ALL
    USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

### Tablas Globales (Sin RLS)

```sql
-- Catalogos globales NO tienen RLS
-- countries, states, country_groups, currencies
-- Son de solo lectura para tenants
```

---

## Funciones de Utilidad

### 1. Conversion de Moneda

```sql
CREATE OR REPLACE FUNCTION core_catalogs.convert_currency(
    p_amount DECIMAL,
    p_from_currency UUID,
    p_to_currency UUID,
    p_tenant_id UUID,
    p_date DATE DEFAULT CURRENT_DATE
) RETURNS DECIMAL AS $$
DECLARE
    v_from_rate DECIMAL;
    v_to_rate DECIMAL;
BEGIN
    -- Si son la misma moneda, retornar el monto
    IF p_from_currency = p_to_currency THEN
        RETURN p_amount;
    END IF;

    -- Obtener tasa de origen (ultima disponible hasta la fecha)
    SELECT rate INTO v_from_rate
    FROM core_catalogs.currency_rates
    WHERE tenant_id = p_tenant_id
      AND currency_id = p_from_currency
      AND name <= p_date
    ORDER BY name DESC LIMIT 1;

    -- Obtener tasa de destino
    SELECT rate INTO v_to_rate
    FROM core_catalogs.currency_rates
    WHERE tenant_id = p_tenant_id
      AND currency_id = p_to_currency
      AND name <= p_date
    ORDER BY name DESC LIMIT 1;

    -- Convertir (asumiendo rates vs moneda base)
    RETURN p_amount * (COALESCE(v_to_rate, 1) / COALESCE(v_from_rate, 1));
END;
$$ LANGUAGE plpgsql STABLE;
```

### 2. Conversion de UoM

```sql
CREATE OR REPLACE FUNCTION core_catalogs.convert_uom(
    p_quantity DECIMAL,
    p_from_uom UUID,
    p_to_uom UUID
) RETURNS DECIMAL AS $$
DECLARE
    v_from_factor DECIMAL;
    v_to_factor DECIMAL;
    v_from_category UUID;
    v_to_category UUID;
BEGIN
    -- Si son la misma UoM, retornar la cantidad
    IF p_from_uom = p_to_uom THEN
        RETURN p_quantity;
    END IF;

    -- Obtener datos de origen
    SELECT factor, category_id INTO v_from_factor, v_from_category
    FROM core_catalogs.uom WHERE id = p_from_uom;

    -- Obtener datos de destino
    SELECT factor, category_id INTO v_to_factor, v_to_category
    FROM core_catalogs.uom WHERE id = p_to_uom;

    -- Validar misma categoria
    IF v_from_category != v_to_category THEN
        RAISE EXCEPTION 'Cannot convert between different UoM categories';
    END IF;

    -- Convertir
    RETURN p_quantity * (v_from_factor / v_to_factor);
END;
$$ LANGUAGE plpgsql STABLE;
```

### 3. Display Name de Categoria

```sql
CREATE OR REPLACE FUNCTION core_catalogs.get_category_display_name(p_id UUID)
RETURNS TEXT AS $$
WITH RECURSIVE ancestors AS (
    SELECT id, name, parent_id, 1 as level
    FROM core_catalogs.categories WHERE id = p_id
    UNION ALL
    SELECT c.id, c.name, c.parent_id, a.level + 1
    FROM core_catalogs.categories c
    JOIN ancestors a ON c.id = a.parent_id
)
SELECT string_agg(name, ' / ' ORDER BY level DESC)
FROM ancestors;
$$ LANGUAGE SQL STABLE;
```

### 4. Actualizar Parent Path

```sql
CREATE OR REPLACE FUNCTION core_catalogs.update_category_parent_path()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.parent_id IS NULL THEN
        NEW.parent_path := NEW.id::text || '/';
    ELSE
        SELECT parent_path || NEW.id::text || '/'
        INTO NEW.parent_path
        FROM core_catalogs.categories
        WHERE id = NEW.parent_id;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_category_parent_path
BEFORE INSERT OR UPDATE OF parent_id ON core_catalogs.categories
FOR EACH ROW
EXECUTE FUNCTION core_catalogs.update_category_parent_path();
```

---

## Triggers

### 1. Validar Una Sola UoM Reference por Categoria

```sql
CREATE OR REPLACE FUNCTION core_catalogs.validate_uom_reference()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.uom_type = 'reference' THEN
        IF EXISTS (
            SELECT 1 FROM core_catalogs.uom
            WHERE category_id = NEW.category_id
              AND uom_type = 'reference'
              AND id != NEW.id
        ) THEN
            RAISE EXCEPTION 'Category already has a reference UoM';
        END IF;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_validate_uom_reference
BEFORE INSERT OR UPDATE ON core_catalogs.uom
FOR EACH ROW
EXECUTE FUNCTION core_catalogs.validate_uom_reference();
```

### 2. Actualizar Display Name de Contacto

```sql
CREATE OR REPLACE FUNCTION core_catalogs.update_contact_display_name()
RETURNS TRIGGER AS $$
BEGIN
    IF NEW.parent_id IS NOT NULL THEN
        SELECT name || ', ' || NEW.name
        INTO NEW.display_name
        FROM core_catalogs.contacts
        WHERE id = NEW.parent_id;
    ELSE
        NEW.display_name := NEW.name;
    END IF;
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_contact_display_name
BEFORE INSERT OR UPDATE OF name, parent_id ON core_catalogs.contacts
FOR EACH ROW
EXECUTE FUNCTION core_catalogs.update_contact_display_name();
```

---

## Seed Data

### Paises Principales

```sql
INSERT INTO core_catalogs.countries (name, code, code3, phone_code, vat_label, state_required) VALUES
('Mexico', 'MX', 'MEX', 52, 'RFC', true),
('United States', 'US', 'USA', 1, 'Tax ID', true),
('Spain', 'ES', 'ESP', 34, 'NIF', true),
('Colombia', 'CO', 'COL', 57, 'NIT', true),
('Argentina', 'AR', 'ARG', 54, 'CUIT', true),
('Chile', 'CL', 'CHL', 56, 'RUT', true),
('Peru', 'PE', 'PER', 51, 'RUC', true);
```

### Monedas Principales

```sql
INSERT INTO core_catalogs.currencies (name, full_name, symbol, decimal_places, position) VALUES
('MXN', 'Mexican Peso', '$', 2, 'before'),
('USD', 'US Dollar', '$', 2, 'before'),
('EUR', 'Euro', '€', 2, 'before'),
('COP', 'Colombian Peso', '$', 0, 'before'),
('ARS', 'Argentine Peso', '$', 2, 'before'),
('CLP', 'Chilean Peso', '$', 0, 'before'),
('PEN', 'Peruvian Sol', 'S/', 2, 'before');
```

---

## Consideraciones de Performance

| Tabla | Volumen Esperado | Estrategia |
|-------|------------------|------------|
| countries | ~250 | Cache agresivo |
| states | ~4000 | Cache por pais |
| currencies | ~180 | Cache agresivo |
| contacts | Alto (miles) | Indices GIN, paginacion |
| currency_rates | Medio | Indice compuesto fecha |
| uom | Bajo (~50/tenant) | Cache por tenant |
| categories | Medio | Parent path para consultas |

---

## Historial de Cambios

| Version | Fecha | Autor | Cambios |
|---------|-------|-------|---------|
| 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |

---

## Aprobaciones

| Rol | Nombre | Fecha | Firma |
|-----|--------|-------|-------|
| DBA | - | - | [ ] |
| Tech Lead | - | - | [ ] |
| Architect | - | - | [ ] |
