# DDL SPECIFICATION: Schema Finance

**Version:** 1.0.0
**Fecha:** 2025-12-05
**Schema:** `finance`
**Modulos:** MAE-014 (Finanzas y Controlling)

---

## Resumen

| Metrica | Valor |
|---------|-------|
| Total Tablas | 15 |
| ENUMs | 7 |
| Funciones | 8 |
| Triggers | 5 |
| Indices | 40+ |

---

## 1. ENUMs

```sql
-- Tipos de cuenta contable
CREATE TYPE finance.account_type AS ENUM (
  'asset',           -- Activo
  'liability',       -- Pasivo
  'equity',          -- Capital
  'income',          -- Ingreso
  'expense'          -- Gasto
);

-- Naturaleza de cuenta
CREATE TYPE finance.account_nature AS ENUM (
  'debit',           -- Deudora
  'credit'           -- Acreedora
);

-- Tipos de poliza/entrada contable
CREATE TYPE finance.entry_type AS ENUM (
  'income',          -- Ingreso
  'expense',         -- Egreso
  'journal',         -- Diario
  'transfer',        -- Traspaso
  'adjustment',      -- Ajuste
  'opening',         -- Apertura
  'closing'          -- Cierre
);

-- Estados de pago
CREATE TYPE finance.payment_status AS ENUM (
  'pending',         -- Pendiente
  'partial',         -- Pago parcial
  'paid',            -- Pagado
  'overdue',         -- Vencido
  'cancelled'        -- Cancelado
);

-- Metodos de pago
CREATE TYPE finance.payment_method AS ENUM (
  'cash',            -- Efectivo
  'check',           -- Cheque
  'transfer',        -- Transferencia
  'card',            -- Tarjeta
  'deposit',         -- Deposito
  'other'            -- Otro
);

-- Estados de conciliacion
CREATE TYPE finance.reconciliation_status AS ENUM (
  'pending',         -- Pendiente
  'matched',         -- Conciliado
  'partial',         -- Parcialmente conciliado
  'unmatched'        -- Sin conciliar
);

-- Tipos de movimiento de cash flow
CREATE TYPE finance.cash_flow_type AS ENUM (
  'operating',       -- Operativo
  'investing',       -- Inversion
  'financing'        -- Financiamiento
);
```

---

## 2. Tablas de Catalogo de Cuentas

### 2.1 chart_of_accounts (Catalogo de Cuentas)

```sql
CREATE TABLE finance.chart_of_accounts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  code VARCHAR(30) NOT NULL,
  name VARCHAR(200) NOT NULL,
  description TEXT,

  -- Clasificacion
  account_type finance.account_type NOT NULL,
  nature finance.account_nature NOT NULL,
  level INTEGER NOT NULL DEFAULT 1,

  -- Jerarquia
  parent_id UUID REFERENCES finance.chart_of_accounts(id),
  full_path VARCHAR(500),  -- Ej: "5000/5100/5101"

  -- Configuracion
  cost_center_required BOOLEAN DEFAULT false,
  project_required BOOLEAN DEFAULT false,
  allows_transactions BOOLEAN DEFAULT true,

  -- Codigos externos
  sap_code VARCHAR(50),
  contpaqi_code VARCHAR(50),
  aspel_code VARCHAR(50),

  -- Estado
  is_active BOOLEAN DEFAULT true,

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_accounts_code UNIQUE (tenant_id, code)
);

CREATE INDEX idx_accounts_tenant ON finance.chart_of_accounts(tenant_id);
CREATE INDEX idx_accounts_type ON finance.chart_of_accounts(account_type);
CREATE INDEX idx_accounts_parent ON finance.chart_of_accounts(parent_id);
CREATE INDEX idx_accounts_active ON finance.chart_of_accounts(tenant_id) WHERE is_active = true AND allows_transactions = true;
CREATE INDEX idx_accounts_path ON finance.chart_of_accounts(tenant_id, full_path);
```

### 2.2 cost_centers (Centros de Costo)

```sql
CREATE TABLE finance.cost_centers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  code VARCHAR(30) NOT NULL,
  name VARCHAR(200) NOT NULL,
  description TEXT,

  -- Jerarquia
  parent_id UUID REFERENCES finance.cost_centers(id),
  level INTEGER DEFAULT 1,

  -- Vinculacion
  project_id UUID REFERENCES construction.projects(id),
  department_id UUID,

  -- Presupuesto
  budget_amount DECIMAL(18,2),
  spent_amount DECIMAL(18,2) DEFAULT 0,

  -- Estado
  is_active BOOLEAN DEFAULT true,

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_cost_centers_code UNIQUE (tenant_id, code)
);

CREATE INDEX idx_cost_centers_tenant ON finance.cost_centers(tenant_id);
CREATE INDEX idx_cost_centers_project ON finance.cost_centers(project_id);
CREATE INDEX idx_cost_centers_parent ON finance.cost_centers(parent_id);
```

---

## 3. Tablas de Polizas Contables

### 3.1 accounting_entries (Polizas)

```sql
CREATE TABLE finance.accounting_entries (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  entry_number VARCHAR(30) NOT NULL,
  entry_type finance.entry_type NOT NULL,
  entry_date DATE NOT NULL,

  -- Descripcion
  description TEXT NOT NULL,
  reference VARCHAR(100),

  -- Origen
  source_module VARCHAR(50),  -- purchases, estimations, payroll, etc.
  source_id UUID,
  source_reference VARCHAR(100),

  -- Vinculacion
  project_id UUID REFERENCES construction.projects(id),
  cost_center_id UUID REFERENCES finance.cost_centers(id),

  -- Totales
  total_debit DECIMAL(18,2) NOT NULL DEFAULT 0,
  total_credit DECIMAL(18,2) NOT NULL DEFAULT 0,
  is_balanced BOOLEAN DEFAULT false,

  -- Estados
  status VARCHAR(20) DEFAULT 'draft',  -- draft, pending, posted, cancelled
  posted_at TIMESTAMP,
  posted_by UUID REFERENCES core.users(id),

  -- Periodo contable
  fiscal_year INTEGER,
  fiscal_period INTEGER,

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_entries_number UNIQUE (tenant_id, entry_number),
  CONSTRAINT chk_entries_balanced CHECK (total_debit = total_credit OR status = 'draft')
);

CREATE INDEX idx_entries_tenant ON finance.accounting_entries(tenant_id);
CREATE INDEX idx_entries_date ON finance.accounting_entries(entry_date);
CREATE INDEX idx_entries_type ON finance.accounting_entries(entry_type);
CREATE INDEX idx_entries_project ON finance.accounting_entries(project_id);
CREATE INDEX idx_entries_status ON finance.accounting_entries(status);
CREATE INDEX idx_entries_period ON finance.accounting_entries(fiscal_year, fiscal_period);
CREATE INDEX idx_entries_source ON finance.accounting_entries(source_module, source_id);
```

### 3.2 accounting_entry_lines (Detalle de Polizas)

```sql
CREATE TABLE finance.accounting_entry_lines (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  entry_id UUID NOT NULL REFERENCES finance.accounting_entries(id) ON DELETE CASCADE,

  -- Linea
  line_number INTEGER NOT NULL,

  -- Cuenta
  account_id UUID NOT NULL REFERENCES finance.chart_of_accounts(id),
  account_code VARCHAR(30) NOT NULL,

  -- Descripcion
  description TEXT,

  -- Montos
  debit_amount DECIMAL(18,2) DEFAULT 0,
  credit_amount DECIMAL(18,2) DEFAULT 0,

  -- Imputacion
  cost_center_id UUID REFERENCES finance.cost_centers(id),
  project_id UUID REFERENCES construction.projects(id),

  -- Tercero
  partner_id UUID,  -- Proveedor o cliente
  partner_name VARCHAR(200),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),

  CONSTRAINT chk_line_amount CHECK (
    (debit_amount > 0 AND credit_amount = 0) OR
    (credit_amount > 0 AND debit_amount = 0)
  )
);

CREATE INDEX idx_entry_lines_entry ON finance.accounting_entry_lines(entry_id);
CREATE INDEX idx_entry_lines_account ON finance.accounting_entry_lines(account_id);
CREATE INDEX idx_entry_lines_cost_center ON finance.accounting_entry_lines(cost_center_id);
CREATE INDEX idx_entry_lines_project ON finance.accounting_entry_lines(project_id);
```

---

## 4. Tablas de Cuentas por Pagar (AP)

### 4.1 accounts_payable (Cuentas por Pagar)

```sql
CREATE TABLE finance.accounts_payable (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  document_number VARCHAR(50) NOT NULL,
  document_type VARCHAR(30),  -- invoice, credit_note, debit_note

  -- Proveedor
  supplier_id UUID NOT NULL,
  supplier_name VARCHAR(200) NOT NULL,
  supplier_rfc VARCHAR(20),

  -- Origen
  purchase_order_id UUID,
  contract_id UUID,

  -- Montos
  subtotal DECIMAL(18,2) NOT NULL,
  tax_amount DECIMAL(18,2) DEFAULT 0,
  retention_amount DECIMAL(18,2) DEFAULT 0,
  total_amount DECIMAL(18,2) NOT NULL,
  paid_amount DECIMAL(18,2) DEFAULT 0,
  balance DECIMAL(18,2) GENERATED ALWAYS AS (total_amount - paid_amount) STORED,

  -- Fechas
  document_date DATE NOT NULL,
  due_date DATE NOT NULL,
  received_date DATE,

  -- Proyecto
  project_id UUID REFERENCES construction.projects(id),
  cost_center_id UUID REFERENCES finance.cost_centers(id),

  -- Estado
  status finance.payment_status NOT NULL DEFAULT 'pending',

  -- Poliza
  accounting_entry_id UUID REFERENCES finance.accounting_entries(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_ap_document UNIQUE (tenant_id, supplier_id, document_number)
);

CREATE INDEX idx_ap_tenant ON finance.accounts_payable(tenant_id);
CREATE INDEX idx_ap_supplier ON finance.accounts_payable(supplier_id);
CREATE INDEX idx_ap_project ON finance.accounts_payable(project_id);
CREATE INDEX idx_ap_status ON finance.accounts_payable(status);
CREATE INDEX idx_ap_due_date ON finance.accounts_payable(due_date);
CREATE INDEX idx_ap_overdue ON finance.accounts_payable(tenant_id)
  WHERE status IN ('pending', 'partial') AND due_date < CURRENT_DATE;
```

### 4.2 ap_payments (Pagos a Proveedores)

```sql
CREATE TABLE finance.ap_payments (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  payable_id UUID NOT NULL REFERENCES finance.accounts_payable(id),

  -- Pago
  payment_number VARCHAR(30) NOT NULL,
  payment_date DATE NOT NULL,
  amount DECIMAL(18,2) NOT NULL,

  -- Metodo
  payment_method finance.payment_method NOT NULL,
  bank_account_id UUID REFERENCES finance.bank_accounts(id),
  reference VARCHAR(100),

  -- Estado
  status VARCHAR(20) DEFAULT 'completed',

  -- Poliza
  accounting_entry_id UUID REFERENCES finance.accounting_entries(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id)
);

CREATE INDEX idx_ap_payments_payable ON finance.ap_payments(payable_id);
CREATE INDEX idx_ap_payments_date ON finance.ap_payments(payment_date);
CREATE INDEX idx_ap_payments_bank ON finance.ap_payments(bank_account_id);
```

---

## 5. Tablas de Cuentas por Cobrar (AR)

### 5.1 accounts_receivable (Cuentas por Cobrar)

```sql
CREATE TABLE finance.accounts_receivable (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  document_number VARCHAR(50) NOT NULL,
  document_type VARCHAR(30),  -- invoice, credit_note, estimation

  -- Cliente
  customer_id UUID NOT NULL,
  customer_name VARCHAR(200) NOT NULL,
  customer_rfc VARCHAR(20),

  -- Origen
  estimation_id UUID,
  sale_order_id UUID,
  contract_id UUID,

  -- Montos
  subtotal DECIMAL(18,2) NOT NULL,
  tax_amount DECIMAL(18,2) DEFAULT 0,
  retention_amount DECIMAL(18,2) DEFAULT 0,
  total_amount DECIMAL(18,2) NOT NULL,
  collected_amount DECIMAL(18,2) DEFAULT 0,
  balance DECIMAL(18,2) GENERATED ALWAYS AS (total_amount - collected_amount) STORED,

  -- Fechas
  document_date DATE NOT NULL,
  due_date DATE NOT NULL,

  -- Proyecto
  project_id UUID REFERENCES construction.projects(id),

  -- Estado
  status finance.payment_status NOT NULL DEFAULT 'pending',

  -- Poliza
  accounting_entry_id UUID REFERENCES finance.accounting_entries(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_ar_document UNIQUE (tenant_id, document_number)
);

CREATE INDEX idx_ar_tenant ON finance.accounts_receivable(tenant_id);
CREATE INDEX idx_ar_customer ON finance.accounts_receivable(customer_id);
CREATE INDEX idx_ar_project ON finance.accounts_receivable(project_id);
CREATE INDEX idx_ar_status ON finance.accounts_receivable(status);
CREATE INDEX idx_ar_due_date ON finance.accounts_receivable(due_date);
CREATE INDEX idx_ar_overdue ON finance.accounts_receivable(tenant_id)
  WHERE status IN ('pending', 'partial') AND due_date < CURRENT_DATE;
```

### 5.2 ar_collections (Cobros)

```sql
CREATE TABLE finance.ar_collections (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  receivable_id UUID NOT NULL REFERENCES finance.accounts_receivable(id),

  -- Cobro
  collection_number VARCHAR(30) NOT NULL,
  collection_date DATE NOT NULL,
  amount DECIMAL(18,2) NOT NULL,

  -- Metodo
  payment_method finance.payment_method NOT NULL,
  bank_account_id UUID REFERENCES finance.bank_accounts(id),
  reference VARCHAR(100),

  -- Estado
  status VARCHAR(20) DEFAULT 'completed',

  -- Poliza
  accounting_entry_id UUID REFERENCES finance.accounting_entries(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id)
);

CREATE INDEX idx_ar_collections_receivable ON finance.ar_collections(receivable_id);
CREATE INDEX idx_ar_collections_date ON finance.ar_collections(collection_date);
CREATE INDEX idx_ar_collections_bank ON finance.ar_collections(bank_account_id);
```

---

## 6. Tablas Bancarias y Conciliacion

### 6.1 bank_accounts (Cuentas Bancarias)

```sql
CREATE TABLE finance.bank_accounts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),

  -- Identificacion
  account_number VARCHAR(30) NOT NULL,
  clabe VARCHAR(18),
  name VARCHAR(200) NOT NULL,

  -- Banco
  bank_name VARCHAR(100) NOT NULL,
  bank_code VARCHAR(10),

  -- Tipo
  account_type VARCHAR(30),  -- checking, savings, credit

  -- Moneda
  currency VARCHAR(3) DEFAULT 'MXN',

  -- Saldos
  current_balance DECIMAL(18,2) DEFAULT 0,
  available_balance DECIMAL(18,2) DEFAULT 0,
  last_reconciled_balance DECIMAL(18,2),
  last_reconciled_date DATE,

  -- Cuenta contable
  ledger_account_id UUID REFERENCES finance.chart_of_accounts(id),

  -- Proyecto (si es cuenta dedicada)
  project_id UUID REFERENCES construction.projects(id),

  -- Estado
  is_active BOOLEAN DEFAULT true,

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),
  deleted_at TIMESTAMP,

  CONSTRAINT uq_bank_accounts UNIQUE (tenant_id, account_number)
);

CREATE INDEX idx_bank_accounts_tenant ON finance.bank_accounts(tenant_id);
CREATE INDEX idx_bank_accounts_project ON finance.bank_accounts(project_id);
CREATE INDEX idx_bank_accounts_active ON finance.bank_accounts(tenant_id) WHERE is_active = true;
```

### 6.2 bank_movements (Movimientos Bancarios)

```sql
CREATE TABLE finance.bank_movements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  bank_account_id UUID NOT NULL REFERENCES finance.bank_accounts(id),

  -- Movimiento
  movement_date DATE NOT NULL,
  value_date DATE,
  reference VARCHAR(100),
  description TEXT,

  -- Montos
  debit_amount DECIMAL(18,2) DEFAULT 0,
  credit_amount DECIMAL(18,2) DEFAULT 0,
  balance_after DECIMAL(18,2),

  -- Origen (del estado de cuenta)
  bank_reference VARCHAR(100),
  import_batch_id UUID,

  -- Conciliacion
  reconciliation_status finance.reconciliation_status DEFAULT 'pending',
  reconciled_with_id UUID,  -- ID de pago/cobro conciliado
  reconciled_with_type VARCHAR(30),  -- ap_payment, ar_collection, entry_line
  reconciled_at TIMESTAMP,
  reconciled_by UUID REFERENCES core.users(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id)
);

CREATE INDEX idx_bank_movements_account ON finance.bank_movements(bank_account_id);
CREATE INDEX idx_bank_movements_date ON finance.bank_movements(movement_date);
CREATE INDEX idx_bank_movements_status ON finance.bank_movements(reconciliation_status);
CREATE INDEX idx_bank_movements_pending ON finance.bank_movements(bank_account_id)
  WHERE reconciliation_status = 'pending';
```

### 6.3 bank_reconciliations (Conciliaciones)

```sql
CREATE TABLE finance.bank_reconciliations (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  bank_account_id UUID NOT NULL REFERENCES finance.bank_accounts(id),

  -- Periodo
  period_start DATE NOT NULL,
  period_end DATE NOT NULL,

  -- Saldos
  bank_opening_balance DECIMAL(18,2) NOT NULL,
  bank_closing_balance DECIMAL(18,2) NOT NULL,
  book_opening_balance DECIMAL(18,2) NOT NULL,
  book_closing_balance DECIMAL(18,2) NOT NULL,

  -- Partidas en conciliacion
  deposits_in_transit DECIMAL(18,2) DEFAULT 0,
  checks_in_transit DECIMAL(18,2) DEFAULT 0,
  bank_errors DECIMAL(18,2) DEFAULT 0,
  book_errors DECIMAL(18,2) DEFAULT 0,

  -- Resultado
  reconciled_balance DECIMAL(18,2),
  difference DECIMAL(18,2),
  is_reconciled BOOLEAN DEFAULT false,

  -- Estado
  status VARCHAR(20) DEFAULT 'draft',  -- draft, in_progress, completed, approved

  -- Aprobacion
  approved_at TIMESTAMP,
  approved_by UUID REFERENCES core.users(id),

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id)
);

CREATE INDEX idx_reconciliations_account ON finance.bank_reconciliations(bank_account_id);
CREATE INDEX idx_reconciliations_period ON finance.bank_reconciliations(period_end);
```

---

## 7. Tablas de Cash Flow

### 7.1 cash_flow_projections (Proyecciones)

```sql
CREATE TABLE finance.cash_flow_projections (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  project_id UUID REFERENCES construction.projects(id),

  -- Periodo
  period_date DATE NOT NULL,
  period_type VARCHAR(20) NOT NULL,  -- daily, weekly, monthly

  -- Ingresos proyectados
  projected_income DECIMAL(18,2) DEFAULT 0,
  projected_collections DECIMAL(18,2) DEFAULT 0,

  -- Egresos proyectados
  projected_expenses DECIMAL(18,2) DEFAULT 0,
  projected_payments DECIMAL(18,2) DEFAULT 0,

  -- Reales (actualizados)
  actual_income DECIMAL(18,2) DEFAULT 0,
  actual_expenses DECIMAL(18,2) DEFAULT 0,

  -- Saldos
  opening_balance DECIMAL(18,2) DEFAULT 0,
  projected_closing DECIMAL(18,2) DEFAULT 0,
  actual_closing DECIMAL(18,2),

  -- Clasificacion
  cash_flow_type finance.cash_flow_type DEFAULT 'operating',

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id),

  CONSTRAINT uq_cash_flow_projection UNIQUE (tenant_id, project_id, period_date, period_type)
);

CREATE INDEX idx_cash_flow_tenant ON finance.cash_flow_projections(tenant_id);
CREATE INDEX idx_cash_flow_project ON finance.cash_flow_projections(project_id);
CREATE INDEX idx_cash_flow_date ON finance.cash_flow_projections(period_date);
```

### 7.2 cash_flow_items (Detalle de Proyeccion)

```sql
CREATE TABLE finance.cash_flow_items (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL REFERENCES core.tenants(id),
  projection_id UUID NOT NULL REFERENCES finance.cash_flow_projections(id),

  -- Tipo
  item_type VARCHAR(30) NOT NULL,  -- income, expense
  category VARCHAR(50),

  -- Descripcion
  description TEXT NOT NULL,

  -- Origen
  source_type VARCHAR(30),  -- estimation, purchase_order, contract, payroll
  source_id UUID,

  -- Montos
  projected_amount DECIMAL(18,2) NOT NULL,
  actual_amount DECIMAL(18,2),

  -- Fecha esperada
  expected_date DATE,
  actual_date DATE,

  -- Auditoria
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  created_by UUID REFERENCES core.users(id),
  updated_at TIMESTAMP,
  updated_by UUID REFERENCES core.users(id)
);

CREATE INDEX idx_cash_flow_items_projection ON finance.cash_flow_items(projection_id);
CREATE INDEX idx_cash_flow_items_type ON finance.cash_flow_items(item_type);
CREATE INDEX idx_cash_flow_items_source ON finance.cash_flow_items(source_type, source_id);
```

---

## 8. Funciones

### 8.1 Calcular Totales de Poliza

```sql
CREATE OR REPLACE FUNCTION finance.calculate_entry_totals()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE finance.accounting_entries ae
  SET
    total_debit = (
      SELECT COALESCE(SUM(debit_amount), 0)
      FROM finance.accounting_entry_lines
      WHERE entry_id = ae.id
    ),
    total_credit = (
      SELECT COALESCE(SUM(credit_amount), 0)
      FROM finance.accounting_entry_lines
      WHERE entry_id = ae.id
    ),
    is_balanced = (
      SELECT COALESCE(SUM(debit_amount), 0) = COALESCE(SUM(credit_amount), 0)
      FROM finance.accounting_entry_lines
      WHERE entry_id = ae.id
    ),
    updated_at = CURRENT_TIMESTAMP
  WHERE id = COALESCE(NEW.entry_id, OLD.entry_id);

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_calculate_entry_totals
AFTER INSERT OR UPDATE OR DELETE ON finance.accounting_entry_lines
FOR EACH ROW
EXECUTE FUNCTION finance.calculate_entry_totals();
```

### 8.2 Actualizar Saldo de Cuenta por Pagar

```sql
CREATE OR REPLACE FUNCTION finance.update_ap_balance()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE finance.accounts_payable ap
  SET
    paid_amount = (
      SELECT COALESCE(SUM(amount), 0)
      FROM finance.ap_payments
      WHERE payable_id = ap.id AND status = 'completed'
    ),
    status = CASE
      WHEN paid_amount >= total_amount THEN 'paid'
      WHEN paid_amount > 0 THEN 'partial'
      WHEN due_date < CURRENT_DATE THEN 'overdue'
      ELSE 'pending'
    END,
    updated_at = CURRENT_TIMESTAMP
  WHERE id = COALESCE(NEW.payable_id, OLD.payable_id);

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_ap_balance
AFTER INSERT OR UPDATE OR DELETE ON finance.ap_payments
FOR EACH ROW
EXECUTE FUNCTION finance.update_ap_balance();
```

### 8.3 Actualizar Saldo de Cuenta por Cobrar

```sql
CREATE OR REPLACE FUNCTION finance.update_ar_balance()
RETURNS TRIGGER AS $$
BEGIN
  UPDATE finance.accounts_receivable ar
  SET
    collected_amount = (
      SELECT COALESCE(SUM(amount), 0)
      FROM finance.ar_collections
      WHERE receivable_id = ar.id AND status = 'completed'
    ),
    status = CASE
      WHEN collected_amount >= total_amount THEN 'paid'
      WHEN collected_amount > 0 THEN 'partial'
      WHEN due_date < CURRENT_DATE THEN 'overdue'
      ELSE 'pending'
    END,
    updated_at = CURRENT_TIMESTAMP
  WHERE id = COALESCE(NEW.receivable_id, OLD.receivable_id);

  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_update_ar_balance
AFTER INSERT OR UPDATE OR DELETE ON finance.ar_collections
FOR EACH ROW
EXECUTE FUNCTION finance.update_ar_balance();
```

### 8.4 Generar Poliza desde Compra

```sql
CREATE OR REPLACE FUNCTION finance.create_entry_from_purchase(
  p_tenant_id UUID,
  p_purchase_id UUID,
  p_created_by UUID
)
RETURNS UUID AS $$
DECLARE
  v_entry_id UUID;
  v_purchase RECORD;
  v_entry_number VARCHAR(30);
BEGIN
  -- Obtener datos de la compra
  SELECT * INTO v_purchase
  FROM purchasing.purchase_orders
  WHERE id = p_purchase_id;

  -- Generar numero de poliza
  SELECT 'POL-' || TO_CHAR(CURRENT_DATE, 'YYYY') || '-' ||
         LPAD((COUNT(*) + 1)::TEXT, 6, '0')
  INTO v_entry_number
  FROM finance.accounting_entries
  WHERE tenant_id = p_tenant_id
    AND fiscal_year = EXTRACT(YEAR FROM CURRENT_DATE);

  -- Crear poliza
  INSERT INTO finance.accounting_entries (
    tenant_id, entry_number, entry_type, entry_date,
    description, reference, source_module, source_id,
    project_id, fiscal_year, fiscal_period, created_by
  )
  VALUES (
    p_tenant_id,
    v_entry_number,
    'expense',
    CURRENT_DATE,
    'Compra: ' || v_purchase.po_number,
    v_purchase.po_number,
    'purchases',
    p_purchase_id,
    v_purchase.project_id,
    EXTRACT(YEAR FROM CURRENT_DATE),
    EXTRACT(MONTH FROM CURRENT_DATE),
    p_created_by
  )
  RETURNING id INTO v_entry_id;

  -- TODO: Crear lineas de poliza segun configuracion de cuentas

  RETURN v_entry_id;
END;
$$ LANGUAGE plpgsql;
```

### 8.5 Calcular Aging de Cuentas

```sql
CREATE OR REPLACE FUNCTION finance.calculate_ap_aging(p_tenant_id UUID, p_as_of_date DATE DEFAULT CURRENT_DATE)
RETURNS TABLE (
  supplier_id UUID,
  supplier_name VARCHAR,
  current_amount DECIMAL,
  days_1_30 DECIMAL,
  days_31_60 DECIMAL,
  days_61_90 DECIMAL,
  days_over_90 DECIMAL,
  total_amount DECIMAL
) AS $$
BEGIN
  RETURN QUERY
  SELECT
    ap.supplier_id,
    ap.supplier_name,
    SUM(CASE WHEN ap.due_date >= p_as_of_date THEN ap.balance ELSE 0 END) as current_amount,
    SUM(CASE WHEN ap.due_date BETWEEN p_as_of_date - 30 AND p_as_of_date - 1 THEN ap.balance ELSE 0 END) as days_1_30,
    SUM(CASE WHEN ap.due_date BETWEEN p_as_of_date - 60 AND p_as_of_date - 31 THEN ap.balance ELSE 0 END) as days_31_60,
    SUM(CASE WHEN ap.due_date BETWEEN p_as_of_date - 90 AND p_as_of_date - 61 THEN ap.balance ELSE 0 END) as days_61_90,
    SUM(CASE WHEN ap.due_date < p_as_of_date - 90 THEN ap.balance ELSE 0 END) as days_over_90,
    SUM(ap.balance) as total_amount
  FROM finance.accounts_payable ap
  WHERE ap.tenant_id = p_tenant_id
    AND ap.status IN ('pending', 'partial', 'overdue')
    AND ap.deleted_at IS NULL
  GROUP BY ap.supplier_id, ap.supplier_name;
END;
$$ LANGUAGE plpgsql;
```

---

## 9. Row Level Security

```sql
-- Habilitar RLS en todas las tablas
ALTER TABLE finance.chart_of_accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.cost_centers ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.accounting_entries ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.accounting_entry_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.accounts_payable ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.ap_payments ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.accounts_receivable ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.ar_collections ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.bank_accounts ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.bank_movements ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.bank_reconciliations ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.cash_flow_projections ENABLE ROW LEVEL SECURITY;
ALTER TABLE finance.cash_flow_items ENABLE ROW LEVEL SECURITY;

-- Crear politicas de aislamiento por tenant
DO $$
DECLARE
  t TEXT;
BEGIN
  FOR t IN SELECT tablename FROM pg_tables WHERE schemaname = 'finance'
  LOOP
    EXECUTE format('
      CREATE POLICY tenant_isolation ON finance.%I
      USING (tenant_id = current_setting(''app.current_tenant_id'')::uuid)
    ', t);
  END LOOP;
END $$;
```

---

## 10. Seeds Iniciales

```sql
-- Catalogo de cuentas base
INSERT INTO finance.chart_of_accounts (tenant_id, code, name, account_type, nature, level)
VALUES
  -- Activos
  ('{{TENANT_ID}}', '1000', 'ACTIVO', 'asset', 'debit', 1),
  ('{{TENANT_ID}}', '1100', 'Activo Circulante', 'asset', 'debit', 2),
  ('{{TENANT_ID}}', '1101', 'Bancos', 'asset', 'debit', 3),
  ('{{TENANT_ID}}', '1102', 'Cuentas por Cobrar', 'asset', 'debit', 3),
  ('{{TENANT_ID}}', '1103', 'Inventarios', 'asset', 'debit', 3),
  -- Pasivos
  ('{{TENANT_ID}}', '2000', 'PASIVO', 'liability', 'credit', 1),
  ('{{TENANT_ID}}', '2100', 'Pasivo a Corto Plazo', 'liability', 'credit', 2),
  ('{{TENANT_ID}}', '2101', 'Proveedores', 'liability', 'credit', 3),
  ('{{TENANT_ID}}', '2102', 'Acreedores Diversos', 'liability', 'credit', 3),
  -- Capital
  ('{{TENANT_ID}}', '3000', 'CAPITAL', 'equity', 'credit', 1),
  ('{{TENANT_ID}}', '3101', 'Capital Social', 'equity', 'credit', 2),
  -- Ingresos
  ('{{TENANT_ID}}', '4000', 'INGRESOS', 'income', 'credit', 1),
  ('{{TENANT_ID}}', '4101', 'Ingresos por Obra', 'income', 'credit', 2),
  -- Gastos
  ('{{TENANT_ID}}', '5000', 'GASTOS', 'expense', 'debit', 1),
  ('{{TENANT_ID}}', '5100', 'Costo de Ventas', 'expense', 'debit', 2),
  ('{{TENANT_ID}}', '5101', 'Materiales', 'expense', 'debit', 3),
  ('{{TENANT_ID}}', '5102', 'Mano de Obra', 'expense', 'debit', 3),
  ('{{TENANT_ID}}', '5103', 'Subcontratos', 'expense', 'debit', 3);
```

---

## Referencias

- [MAE-014: Finanzas y Controlling](../../02-definicion-modulos/MAE-014-finanzas-controlling/)
- [EPIC-MAE-014](../../08-epicas/EPIC-MAE-014-finanzas.md)
- [ADR-007: Database Design](../../97-adr/ADR-007-database-design.md)

---

*Ultima actualizacion: 2025-12-05*
