# DDL-SPEC: Schema core_financial

## Identificacion

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

---

## Descripcion General

El schema `core_financial` implementa las bases del sistema contable: plan de cuentas jerarquico, manejo multi-moneda con conversion, periodos fiscales, y asientos contables con partida doble. Sirve como fundamento para modulos financieros de verticales.

### RF Cubiertos

| RF | Titulo | Tablas |
|----|--------|--------|
| RF-FIN-001 | Plan de Cuentas | charts_of_accounts, accounts, account_types |
| RF-FIN-002 | Monedas y Cambio | tenant_currencies, exchange_rates |
| RF-FIN-003 | Periodos | fiscal_years, fiscal_periods |
| RF-FIN-004 | Asientos | journal_entries, journal_lines, cost_centers |

---

## Diagrama ER

```mermaid
erDiagram
    charts_of_accounts {
        uuid id PK
        uuid tenant_id FK
        varchar code
        varchar name
        boolean is_active
    }

    account_types {
        uuid id PK
        varchar code UK
        varchar name
        varchar classification
        varchar normal_balance
        int display_order
    }

    accounts {
        uuid id PK
        uuid chart_id FK
        uuid parent_id FK
        uuid account_type_id FK
        varchar code
        varchar name
        int level
        boolean is_detail
        decimal opening_balance
    }

    tenant_currencies {
        uuid id PK
        uuid tenant_id FK
        varchar currency_code FK
        boolean is_base
        boolean is_active
    }

    exchange_rates {
        uuid id PK
        uuid tenant_id FK
        varchar from_currency
        varchar to_currency
        decimal rate
        date effective_date
    }

    fiscal_years {
        uuid id PK
        uuid tenant_id FK
        varchar name
        date start_date
        date end_date
        varchar status
    }

    fiscal_periods {
        uuid id PK
        uuid fiscal_year_id FK
        varchar name
        int period_number
        date start_date
        date end_date
        varchar status
    }

    journal_entries {
        uuid id PK
        uuid tenant_id FK
        uuid fiscal_period_id FK
        varchar entry_number
        date entry_date
        varchar currency_code
        decimal exchange_rate
        varchar status
        text description
    }

    journal_lines {
        uuid id PK
        uuid journal_entry_id FK
        uuid account_id FK
        uuid cost_center_id FK
        decimal debit
        decimal credit
        decimal debit_base
        decimal credit_base
        text description
    }

    cost_centers {
        uuid id PK
        uuid tenant_id FK
        uuid parent_id FK
        varchar code
        varchar name
        boolean is_active
    }

    charts_of_accounts ||--o{ accounts : "contiene"
    accounts ||--o{ accounts : "padre"
    accounts }o--|| account_types : "tipo"
    fiscal_years ||--o{ fiscal_periods : "contiene"
    fiscal_periods ||--o{ journal_entries : "periodo"
    journal_entries ||--o{ journal_lines : "lineas"
    accounts ||--o{ journal_lines : "cuenta"
    cost_centers ||--o{ journal_lines : "centro_costo"
```

---

## Tablas

### 1. account_types

Tipos de cuenta predefinidos (global).

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `code` | VARCHAR(20) | NOT NULL | - | Codigo unico |
| `name` | VARCHAR(100) | NOT NULL | - | Nombre |
| `classification` | VARCHAR(20) | NOT NULL | - | Clasificacion contable |
| `normal_balance` | VARCHAR(10) | NOT NULL | - | Saldo normal |
| `affects_cash_flow` | BOOLEAN | NOT NULL | false | Afecta flujo de caja |
| `display_order` | INTEGER | NOT NULL | 0 | Orden en reportes |

```sql
CREATE TABLE core_financial.account_types (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    code VARCHAR(20) NOT NULL UNIQUE,
    name VARCHAR(100) NOT NULL,
    classification VARCHAR(20) NOT NULL,
    normal_balance VARCHAR(10) NOT NULL,
    affects_cash_flow BOOLEAN NOT NULL DEFAULT false,
    display_order INTEGER NOT NULL DEFAULT 0,

    CONSTRAINT chk_classification CHECK (classification IN (
        'asset', 'liability', 'equity', 'revenue', 'expense'
    )),
    CONSTRAINT chk_normal_balance CHECK (normal_balance IN ('debit', 'credit'))
);

-- Seed data
INSERT INTO core_financial.account_types (code, name, classification, normal_balance, display_order) VALUES
('ASSET_CURRENT', 'Activo Circulante', 'asset', 'debit', 1),
('ASSET_FIXED', 'Activo Fijo', 'asset', 'debit', 2),
('ASSET_OTHER', 'Otros Activos', 'asset', 'debit', 3),
('LIABILITY_CURRENT', 'Pasivo Circulante', 'liability', 'credit', 4),
('LIABILITY_LONG', 'Pasivo a Largo Plazo', 'liability', 'credit', 5),
('EQUITY', 'Capital', 'equity', 'credit', 6),
('REVENUE', 'Ingresos', 'revenue', 'credit', 7),
('EXPENSE', 'Gastos', 'expense', 'debit', 8),
('COGS', 'Costo de Ventas', 'expense', 'debit', 9),
('OTHER_INCOME', 'Otros Ingresos', 'revenue', 'credit', 10),
('OTHER_EXPENSE', 'Otros Gastos', 'expense', 'debit', 11);
```

---

### 2. charts_of_accounts

Planes de cuentas por tenant.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `code` | VARCHAR(20) | NOT NULL | - | Codigo |
| `name` | VARCHAR(255) | NOT NULL | - | Nombre del plan |
| `description` | TEXT | NULL | - | Descripcion |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

```sql
CREATE TABLE core_financial.charts_of_accounts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_charts_tenant_code UNIQUE (tenant_id, code)
);

-- RLS
ALTER TABLE core_financial.charts_of_accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.charts_of_accounts
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 3. accounts

Cuentas contables jerarquicas.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `chart_id` | UUID | NOT NULL | - | FK a charts_of_accounts |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants (redundante para RLS) |
| `parent_id` | UUID | NULL | - | Cuenta padre |
| `account_type_id` | UUID | NOT NULL | - | FK a account_types |
| `code` | VARCHAR(20) | NOT NULL | - | Codigo cuenta |
| `name` | VARCHAR(255) | NOT NULL | - | Nombre |
| `description` | TEXT | NULL | - | Descripcion |
| `level` | INTEGER | NOT NULL | 1 | Nivel jerarquico |
| `path` | LTREE | NULL | - | Path materializado |
| `is_detail` | BOOLEAN | NOT NULL | true | Cuenta de detalle |
| `is_bank` | BOOLEAN | NOT NULL | false | Es cuenta bancaria |
| `is_cash` | BOOLEAN | NOT NULL | false | Es cuenta de caja |
| `currency_code` | VARCHAR(3) | NULL | - | Moneda (si especifica) |
| `opening_balance` | DECIMAL(18,4) | NOT NULL | 0 | Saldo inicial |
| `current_balance` | DECIMAL(18,4) | NOT NULL | 0 | Saldo actual |
| `is_active` | BOOLEAN | NOT NULL | true | Activa |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

```sql
CREATE EXTENSION IF NOT EXISTS ltree;

CREATE TABLE core_financial.accounts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    chart_id UUID NOT NULL REFERENCES core_financial.charts_of_accounts(id),
    tenant_id UUID NOT NULL,
    parent_id UUID REFERENCES core_financial.accounts(id),
    account_type_id UUID NOT NULL REFERENCES core_financial.account_types(id),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    level INTEGER NOT NULL DEFAULT 1,
    path LTREE,
    is_detail BOOLEAN NOT NULL DEFAULT true,
    is_bank BOOLEAN NOT NULL DEFAULT false,
    is_cash BOOLEAN NOT NULL DEFAULT false,
    currency_code VARCHAR(3),
    opening_balance DECIMAL(18,4) NOT NULL DEFAULT 0,
    current_balance DECIMAL(18,4) NOT NULL DEFAULT 0,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_accounts_chart_code UNIQUE (chart_id, code),
    CONSTRAINT chk_accounts_level CHECK (level >= 1 AND level <= 10)
);

CREATE INDEX idx_accounts_chart ON core_financial.accounts(chart_id);
CREATE INDEX idx_accounts_parent ON core_financial.accounts(parent_id);
CREATE INDEX idx_accounts_type ON core_financial.accounts(account_type_id);
CREATE INDEX idx_accounts_path ON core_financial.accounts USING GIST (path);
CREATE INDEX idx_accounts_detail ON core_financial.accounts(chart_id, is_detail)
    WHERE is_detail = true;

-- RLS
ALTER TABLE core_financial.accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.accounts
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 4. tenant_currencies

Monedas habilitadas por tenant.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `currency_code` | VARCHAR(3) | NOT NULL | - | FK a currencies |
| `is_base` | BOOLEAN | NOT NULL | false | Moneda base |
| `is_active` | BOOLEAN | NOT NULL | true | Activa |
| `decimal_places` | INTEGER | NOT NULL | 2 | Decimales |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

```sql
CREATE TABLE core_financial.tenant_currencies (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    currency_code VARCHAR(3) NOT NULL,
    is_base BOOLEAN NOT NULL DEFAULT false,
    is_active BOOLEAN NOT NULL DEFAULT true,
    decimal_places INTEGER NOT NULL DEFAULT 2,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_tenant_currencies UNIQUE (tenant_id, currency_code),
    CONSTRAINT fk_tenant_currencies_currency
        FOREIGN KEY (currency_code) REFERENCES core_catalogs.currencies(code)
);

-- Solo una moneda base por tenant
CREATE UNIQUE INDEX idx_tenant_currencies_base ON core_financial.tenant_currencies(tenant_id)
    WHERE is_base = true;

-- RLS
ALTER TABLE core_financial.tenant_currencies ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.tenant_currencies
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 5. exchange_rates

Tipos de cambio historicos.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `from_currency` | VARCHAR(3) | NOT NULL | - | Moneda origen |
| `to_currency` | VARCHAR(3) | NOT NULL | - | Moneda destino |
| `rate` | DECIMAL(18,8) | NOT NULL | - | Tasa de cambio |
| `effective_date` | DATE | NOT NULL | - | Fecha efectiva |
| `source` | VARCHAR(50) | NULL | - | Fuente del tipo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `created_by` | UUID | NULL | - | Usuario creador |

```sql
CREATE TABLE core_financial.exchange_rates (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    from_currency VARCHAR(3) NOT NULL,
    to_currency VARCHAR(3) NOT NULL,
    rate DECIMAL(18,8) NOT NULL,
    effective_date DATE NOT NULL,
    source VARCHAR(50),
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by UUID,

    CONSTRAINT uk_exchange_rates UNIQUE (tenant_id, from_currency, to_currency, effective_date),
    CONSTRAINT chk_rate_positive CHECK (rate > 0)
);

CREATE INDEX idx_exchange_rates_lookup ON core_financial.exchange_rates(
    tenant_id, from_currency, to_currency, effective_date DESC
);

-- RLS
ALTER TABLE core_financial.exchange_rates ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.exchange_rates
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 6. fiscal_years

Anos fiscales.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `name` | VARCHAR(50) | NOT NULL | - | Nombre (ej: "2025") |
| `start_date` | DATE | NOT NULL | - | Fecha inicio |
| `end_date` | DATE | NOT NULL | - | Fecha fin |
| `status` | VARCHAR(20) | NOT NULL | 'open' | Estado |
| `closed_at` | TIMESTAMPTZ | NULL | - | Fecha cierre |
| `closed_by` | UUID | NULL | - | Usuario que cerro |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

```sql
CREATE TABLE core_financial.fiscal_years (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    name VARCHAR(50) NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    closed_at TIMESTAMPTZ,
    closed_by UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_fiscal_years_tenant_name UNIQUE (tenant_id, name),
    CONSTRAINT chk_fiscal_years_dates CHECK (end_date > start_date),
    CONSTRAINT chk_fiscal_years_status CHECK (status IN ('open', 'closing', 'closed'))
);

-- RLS
ALTER TABLE core_financial.fiscal_years ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.fiscal_years
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 7. fiscal_periods

Periodos contables dentro de ano fiscal.

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `fiscal_year_id` | UUID | NOT NULL | - | FK a fiscal_years |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `name` | VARCHAR(50) | NOT NULL | - | Nombre periodo |
| `period_number` | INTEGER | NOT NULL | - | Numero secuencial |
| `start_date` | DATE | NOT NULL | - | Fecha inicio |
| `end_date` | DATE | NOT NULL | - | Fecha fin |
| `status` | VARCHAR(20) | NOT NULL | 'open' | Estado |
| `is_adjustment` | BOOLEAN | NOT NULL | false | Periodo de ajuste |
| `closed_at` | TIMESTAMPTZ | NULL | - | Fecha cierre |
| `closed_by` | UUID | NULL | - | Usuario que cerro |

```sql
CREATE TABLE core_financial.fiscal_periods (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    fiscal_year_id UUID NOT NULL REFERENCES core_financial.fiscal_years(id),
    tenant_id UUID NOT NULL,
    name VARCHAR(50) NOT NULL,
    period_number INTEGER NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'open',
    is_adjustment BOOLEAN NOT NULL DEFAULT false,
    closed_at TIMESTAMPTZ,
    closed_by UUID,

    CONSTRAINT uk_fiscal_periods_year_number UNIQUE (fiscal_year_id, period_number),
    CONSTRAINT chk_fiscal_periods_dates CHECK (end_date >= start_date),
    CONSTRAINT chk_fiscal_periods_status CHECK (status IN ('open', 'closing', 'closed'))
);

CREATE INDEX idx_fiscal_periods_year ON core_financial.fiscal_periods(fiscal_year_id);
CREATE INDEX idx_fiscal_periods_date ON core_financial.fiscal_periods(tenant_id, start_date, end_date);
CREATE INDEX idx_fiscal_periods_open ON core_financial.fiscal_periods(tenant_id, status)
    WHERE status = 'open';

-- RLS
ALTER TABLE core_financial.fiscal_periods ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.fiscal_periods
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 8. cost_centers

Centros de costo.

| 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 | - | Centro padre |
| `code` | VARCHAR(20) | NOT NULL | - | Codigo |
| `name` | VARCHAR(255) | NOT NULL | - | Nombre |
| `level` | INTEGER | NOT NULL | 1 | Nivel |
| `is_active` | BOOLEAN | NOT NULL | true | Activo |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |

```sql
CREATE TABLE core_financial.cost_centers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    parent_id UUID REFERENCES core_financial.cost_centers(id),
    code VARCHAR(20) NOT NULL,
    name VARCHAR(255) NOT NULL,
    level INTEGER NOT NULL DEFAULT 1,
    is_active BOOLEAN NOT NULL DEFAULT true,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_cost_centers_tenant_code UNIQUE (tenant_id, code)
);

-- RLS
ALTER TABLE core_financial.cost_centers ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.cost_centers
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 9. journal_entries

Asientos contables (cabecera).

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `tenant_id` | UUID | NOT NULL | - | FK a tenants |
| `fiscal_period_id` | UUID | NOT NULL | - | FK a fiscal_periods |
| `entry_number` | VARCHAR(20) | NOT NULL | - | Numero secuencial |
| `entry_date` | DATE | NOT NULL | - | Fecha contable |
| `currency_code` | VARCHAR(3) | NOT NULL | - | Moneda |
| `exchange_rate` | DECIMAL(18,8) | NOT NULL | 1 | Tipo cambio a base |
| `reference` | VARCHAR(100) | NULL | - | Referencia externa |
| `description` | TEXT | NOT NULL | - | Descripcion |
| `source_module` | VARCHAR(50) | NULL | - | Modulo origen |
| `source_document_id` | UUID | NULL | - | Documento origen |
| `status` | VARCHAR(20) | NOT NULL | 'draft' | Estado |
| `total_debit` | DECIMAL(18,4) | NOT NULL | 0 | Total debito |
| `total_credit` | DECIMAL(18,4) | NOT NULL | 0 | Total credito |
| `posted_at` | TIMESTAMPTZ | NULL | - | Fecha contabilizado |
| `posted_by` | UUID | NULL | - | Usuario que contabilizo |
| `reversed_by` | UUID | NULL | - | Asiento reverso |
| `created_by` | UUID | NOT NULL | - | Usuario creador |
| `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha creacion |
| `updated_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha actualizacion |

```sql
CREATE TABLE core_financial.journal_entries (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id UUID NOT NULL,
    fiscal_period_id UUID NOT NULL REFERENCES core_financial.fiscal_periods(id),
    entry_number VARCHAR(20) NOT NULL,
    entry_date DATE NOT NULL,
    currency_code VARCHAR(3) NOT NULL,
    exchange_rate DECIMAL(18,8) NOT NULL DEFAULT 1,
    reference VARCHAR(100),
    description TEXT NOT NULL,
    source_module VARCHAR(50),
    source_document_id UUID,
    status VARCHAR(20) NOT NULL DEFAULT 'draft',
    total_debit DECIMAL(18,4) NOT NULL DEFAULT 0,
    total_credit DECIMAL(18,4) NOT NULL DEFAULT 0,
    posted_at TIMESTAMPTZ,
    posted_by UUID,
    reversed_by UUID REFERENCES core_financial.journal_entries(id),
    created_by UUID NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

    CONSTRAINT uk_journal_entries_number UNIQUE (tenant_id, entry_number),
    CONSTRAINT chk_journal_status CHECK (status IN ('draft', 'posted', 'reversed')),
    CONSTRAINT chk_journal_balance CHECK (
        status = 'draft' OR (total_debit = total_credit AND total_debit > 0)
    )
);

CREATE INDEX idx_journal_entries_period ON core_financial.journal_entries(fiscal_period_id);
CREATE INDEX idx_journal_entries_date ON core_financial.journal_entries(tenant_id, entry_date);
CREATE INDEX idx_journal_entries_status ON core_financial.journal_entries(status);
CREATE INDEX idx_journal_entries_source ON core_financial.journal_entries(source_module, source_document_id);

-- RLS
ALTER TABLE core_financial.journal_entries ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON core_financial.journal_entries
    FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
```

---

### 10. journal_lines

Lineas de asiento (partida doble).

| Columna | Tipo | Nullable | Default | Descripcion |
|---------|------|----------|---------|-------------|
| `id` | UUID | NOT NULL | gen_random_uuid() | PK |
| `journal_entry_id` | UUID | NOT NULL | - | FK a journal_entries |
| `line_number` | INTEGER | NOT NULL | - | Numero de linea |
| `account_id` | UUID | NOT NULL | - | FK a accounts |
| `cost_center_id` | UUID | NULL | - | FK a cost_centers |
| `debit` | DECIMAL(18,4) | NOT NULL | 0 | Monto debito (moneda doc) |
| `credit` | DECIMAL(18,4) | NOT NULL | 0 | Monto credito (moneda doc) |
| `debit_base` | DECIMAL(18,4) | NOT NULL | 0 | Debito en moneda base |
| `credit_base` | DECIMAL(18,4) | NOT NULL | 0 | Credito en moneda base |
| `description` | TEXT | NULL | - | Descripcion linea |
| `reference` | VARCHAR(100) | NULL | - | Referencia |

```sql
CREATE TABLE core_financial.journal_lines (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    journal_entry_id UUID NOT NULL REFERENCES core_financial.journal_entries(id) ON DELETE CASCADE,
    line_number INTEGER NOT NULL,
    account_id UUID NOT NULL REFERENCES core_financial.accounts(id),
    cost_center_id UUID REFERENCES core_financial.cost_centers(id),
    debit DECIMAL(18,4) NOT NULL DEFAULT 0,
    credit DECIMAL(18,4) NOT NULL DEFAULT 0,
    debit_base DECIMAL(18,4) NOT NULL DEFAULT 0,
    credit_base DECIMAL(18,4) NOT NULL DEFAULT 0,
    description TEXT,
    reference VARCHAR(100),

    CONSTRAINT uk_journal_lines_entry_line UNIQUE (journal_entry_id, line_number),
    CONSTRAINT chk_journal_lines_amounts CHECK (
        (debit > 0 AND credit = 0) OR (debit = 0 AND credit > 0) OR (debit = 0 AND credit = 0)
    )
);

CREATE INDEX idx_journal_lines_entry ON core_financial.journal_lines(journal_entry_id);
CREATE INDEX idx_journal_lines_account ON core_financial.journal_lines(account_id);
CREATE INDEX idx_journal_lines_cost_center ON core_financial.journal_lines(cost_center_id);
```

---

## Funciones de Utilidad

### Obtener Tipo de Cambio

```sql
CREATE OR REPLACE FUNCTION core_financial.get_exchange_rate(
    p_tenant_id UUID,
    p_from_currency VARCHAR,
    p_to_currency VARCHAR,
    p_date DATE DEFAULT CURRENT_DATE
) RETURNS DECIMAL(18,8) AS $$
DECLARE
    v_rate DECIMAL(18,8);
BEGIN
    IF p_from_currency = p_to_currency THEN
        RETURN 1;
    END IF;

    -- Buscar tasa directa
    SELECT rate INTO v_rate
    FROM core_financial.exchange_rates
    WHERE tenant_id = p_tenant_id
    AND from_currency = p_from_currency
    AND to_currency = p_to_currency
    AND effective_date <= p_date
    ORDER BY effective_date DESC
    LIMIT 1;

    IF v_rate IS NOT NULL THEN
        RETURN v_rate;
    END IF;

    -- Buscar tasa inversa
    SELECT 1.0 / rate INTO v_rate
    FROM core_financial.exchange_rates
    WHERE tenant_id = p_tenant_id
    AND from_currency = p_to_currency
    AND to_currency = p_from_currency
    AND effective_date <= p_date
    ORDER BY effective_date DESC
    LIMIT 1;

    IF v_rate IS NOT NULL THEN
        RETURN v_rate;
    END IF;

    RAISE EXCEPTION 'Exchange rate not found for % to % on %', p_from_currency, p_to_currency, p_date;
END;
$$ LANGUAGE plpgsql STABLE;
```

### Obtener Periodo para Fecha

```sql
CREATE OR REPLACE FUNCTION core_financial.get_period_for_date(
    p_tenant_id UUID,
    p_date DATE
) RETURNS UUID AS $$
DECLARE
    v_period_id UUID;
BEGIN
    SELECT id INTO v_period_id
    FROM core_financial.fiscal_periods
    WHERE tenant_id = p_tenant_id
    AND p_date BETWEEN start_date AND end_date
    AND status = 'open'
    LIMIT 1;

    IF v_period_id IS NULL THEN
        RAISE EXCEPTION 'No open period found for date %', p_date;
    END IF;

    RETURN v_period_id;
END;
$$ LANGUAGE plpgsql STABLE;
```

### Contabilizar Asiento

```sql
CREATE OR REPLACE FUNCTION core_financial.post_journal_entry(
    p_entry_id UUID,
    p_user_id UUID
) RETURNS BOOLEAN AS $$
DECLARE
    v_entry RECORD;
    v_line RECORD;
    v_period_status VARCHAR;
BEGIN
    -- Obtener asiento
    SELECT * INTO v_entry
    FROM core_financial.journal_entries
    WHERE id = p_entry_id FOR UPDATE;

    IF v_entry IS NULL THEN
        RAISE EXCEPTION 'Journal entry not found';
    END IF;

    IF v_entry.status != 'draft' THEN
        RAISE EXCEPTION 'Entry is not in draft status';
    END IF;

    -- Verificar periodo abierto
    SELECT status INTO v_period_status
    FROM core_financial.fiscal_periods
    WHERE id = v_entry.fiscal_period_id;

    IF v_period_status != 'open' THEN
        RAISE EXCEPTION 'Fiscal period is not open';
    END IF;

    -- Verificar balance
    IF v_entry.total_debit != v_entry.total_credit THEN
        RAISE EXCEPTION 'Entry is not balanced: debit=% credit=%',
            v_entry.total_debit, v_entry.total_credit;
    END IF;

    -- Actualizar saldos de cuentas
    FOR v_line IN
        SELECT jl.*, a.current_balance, at.normal_balance
        FROM core_financial.journal_lines jl
        JOIN core_financial.accounts a ON a.id = jl.account_id
        JOIN core_financial.account_types at ON at.id = a.account_type_id
        WHERE jl.journal_entry_id = p_entry_id
    LOOP
        UPDATE core_financial.accounts
        SET current_balance = current_balance +
            CASE WHEN v_line.normal_balance = 'debit'
                THEN v_line.debit_base - v_line.credit_base
                ELSE v_line.credit_base - v_line.debit_base
            END,
            updated_at = NOW()
        WHERE id = v_line.account_id;
    END LOOP;

    -- Marcar como contabilizado
    UPDATE core_financial.journal_entries
    SET status = 'posted',
        posted_at = NOW(),
        posted_by = p_user_id
    WHERE id = p_entry_id;

    RETURN true;
END;
$$ LANGUAGE plpgsql;
```

### Obtener Saldo de Cuenta por Periodo

```sql
CREATE OR REPLACE FUNCTION core_financial.get_account_balance(
    p_account_id UUID,
    p_as_of_date DATE DEFAULT NULL
) RETURNS TABLE (
    debit_total DECIMAL(18,4),
    credit_total DECIMAL(18,4),
    balance DECIMAL(18,4)
) AS $$
BEGIN
    RETURN QUERY
    SELECT
        COALESCE(SUM(jl.debit_base), 0) as debit_total,
        COALESCE(SUM(jl.credit_base), 0) as credit_total,
        COALESCE(SUM(jl.debit_base - jl.credit_base), 0) as balance
    FROM core_financial.journal_lines jl
    JOIN core_financial.journal_entries je ON je.id = jl.journal_entry_id
    WHERE jl.account_id = p_account_id
    AND je.status = 'posted'
    AND (p_as_of_date IS NULL OR je.entry_date <= p_as_of_date);
END;
$$ LANGUAGE plpgsql STABLE;
```

---

## Seed Data

### Crear Plan de Cuentas Base

```sql
CREATE OR REPLACE FUNCTION core_financial.create_default_chart(
    p_tenant_id UUID
) RETURNS UUID AS $$
DECLARE
    v_chart_id UUID;
    v_type_id UUID;
BEGIN
    -- Crear chart
    INSERT INTO core_financial.charts_of_accounts (tenant_id, code, name)
    VALUES (p_tenant_id, 'PRINCIPAL', 'Plan de Cuentas Principal')
    RETURNING id INTO v_chart_id;

    -- Cuentas principales
    -- Activos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'ASSET_CURRENT';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '1', 'ACTIVO', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1', 'Activo Circulante', 2, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1', 'Caja y Bancos', 3, false),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1.1', 'Caja General', 4, true),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.1.2', 'Bancos', 4, true),
    (v_chart_id, p_tenant_id, v_type_id, '1.1.2', 'Cuentas por Cobrar', 3, true);

    -- Pasivos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'LIABILITY_CURRENT';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '2', 'PASIVO', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '2.1', 'Pasivo Circulante', 2, false),
    (v_chart_id, p_tenant_id, v_type_id, '2.1.1', 'Cuentas por Pagar', 3, true);

    -- Capital
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'EQUITY';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '3', 'CAPITAL', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '3.1', 'Capital Social', 2, true);

    -- Ingresos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'REVENUE';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '4', 'INGRESOS', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '4.1', 'Ingresos Operativos', 2, true);

    -- Gastos
    SELECT id INTO v_type_id FROM core_financial.account_types WHERE code = 'EXPENSE';
    INSERT INTO core_financial.accounts (chart_id, tenant_id, account_type_id, code, name, level, is_detail)
    VALUES
    (v_chart_id, p_tenant_id, v_type_id, '5', 'GASTOS', 1, false),
    (v_chart_id, p_tenant_id, v_type_id, '5.1', 'Gastos Operativos', 2, true);

    RETURN v_chart_id;
END;
$$ LANGUAGE plpgsql;
```

---

## Historial

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