# PLAN DE CORRECCIONES - ERP SUITE (erp-core)

**Version:** 1.2.0
**Fecha:** 2025-12-06
**Autor:** Requirements-Analyst
**Estado:** ✅ FASE 1 Y FASE 2 COMPLETADAS

## REGISTRO DE EJECUCION

| Correccion | Estado | Fecha | Archivos Modificados |
|------------|--------|-------|---------------------|
| CORR-001 | ✅ COMPLETADO | 2025-12-06 | orders.service.ts, sales.controller.ts, status.ts, DashboardPage.tsx |
| CORR-002 | ✅ COMPLETADO | 2025-12-06 | auth.controller.ts, auth.service.ts, users.controller.ts, users.service.ts |
| CORR-003 | ✅ COMPLETADO | 2025-12-06 | DDL originales (07-sales.sql, 06-purchase.sql, 04-financial.sql, 05-inventory.sql, 02-core.sql, 03-analytics.sql) |
| CORR-004 | ✅ COMPLETADO | 2025-12-06 | DDL original (02-core.sql - índices FK en partners) |
| CORR-005 | ✅ COMPLETADO | 2025-12-06 | taxes.service.ts, orders.service.ts, quotations.service.ts, invoices.service.ts |
| CORR-006 | ✅ COMPLETADO | 2025-12-06 | entities.types.ts |
| CORR-007 | ✅ COMPLETADO | 2025-12-06 | shared/services/base.service.ts (NUEVO) |
| CORR-008 | ✅ COMPLETADO | 2025-12-06 | 11-crm.sql, 12-hr.sql (RLS policies agregadas) |

## NOTA IMPORTANTE - DIRECTIVA DE BASE DE DATOS

Por directiva del proyecto, **NO se usan scripts de migración**. Todas las correcciones de base de datos se aplicaron directamente a los archivos DDL originales para garantizar una carga limpia desde cero.

### Archivos de migración eliminados:
- ~~migrations/20251206_001_add_tenant_id_to_lines.sql~~
- ~~migrations/20251206_002_add_rls_to_lines.sql~~
- ~~migrations/20251206_003_add_missing_fk_indexes.sql~~

### Correcciones aplicadas a DDL originales:
- **07-sales.sql**: tenant_id + RLS en sales_order_lines, quotation_lines, pricelist_items
- **06-purchase.sql**: tenant_id + RLS en purchase_order_lines, rfq_lines, purchase_agreement_lines
- **04-financial.sql**: tenant_id + RLS en journal_entry_lines, invoice_lines
- **05-inventory.sql**: tenant_id + RLS en stock_quants, inventory_adjustment_lines
- **02-core.sql**: índices FK en partners (currency_id, payment_term_id, pricelist_id)
- **03-analytics.sql**: analytic_lines ya tenía tenant_id y RLS
- **11-crm.sql**: RLS policies agregadas para todas las tablas
- **12-hr.sql**: RLS policies agregadas para todas las tablas

---

## RESUMEN EJECUTIVO

Este documento detalla el plan de correcciones para resolver las incoherencias criticas identificadas en el proyecto ERP Suite. Cada correccion ha sido analizada por impacto en componentes dependientes.

**Total de correcciones:** 6 criticas + 6 altas
**Tiempo estimado total:** 40-50 horas
**Riesgo de ruptura:** Controlado con orden de ejecucion

---

## MATRIZ DE DEPENDENCIAS

```
                    ┌─────────────────────────────────────────────┐
                    │           ORDEN DE EJECUCION                │
                    └─────────────────────────────────────────────┘

┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│ CORR-001    │────▶│ CORR-002    │────▶│ CORR-005    │────▶│ CORR-006    │
│ Status Enum │     │ firstName   │     │ Taxes Calc  │     │ Types FE    │
│ (DDL→BE→FE) │     │ Transformer │     │ (Depends    │     │ (Depends    │
└─────────────┘     └─────────────┘     │ BE ready)   │     │ all ready)  │
                                        └─────────────┘     └─────────────┘
                          │
                          ▼
                    ┌─────────────┐
                    │ CORR-003    │
                    │ tenant_id   │ ◀──── PARALELO con CORR-002
                    │ (DDL only)  │
                    └─────────────┘
                          │
                          ▼
                    ┌─────────────┐
                    │ CORR-004    │
                    │ FK Indices  │ ◀──── Depende de CORR-003
                    └─────────────┘
```

---

## FASE 1: CORRECCIONES CRITICAS

### CORR-001: Status Enum Sales Orders

**Prioridad:** CRITICA
**Tiempo estimado:** 3-4 horas
**Riesgo de ruptura:** MEDIO

#### Problema
- DDL define: `'draft' | 'sent' | 'sale' | 'done' | 'cancelled'`
- Backend usa: `'draft' | 'confirmed' | 'done' | 'cancelled'`

#### Archivos a Modificar (5 archivos)

| # | Archivo | Linea | Cambio | Dependencias |
|---|---------|-------|--------|--------------|
| 1 | `database/ddl/07-sales.sql` | 15-21 | **VERIFICAR** - Ya correcto | Ninguna |
| 2 | `backend/src/modules/sales/orders.service.ts` | 48 | Cambiar tipo union | CORR-001.3, CORR-001.4 |
| 3 | `backend/src/modules/sales/orders.service.ts` | 526 | `'confirmed'` → `'sent'` | Ninguna |
| 4 | `backend/src/modules/sales/orders.service.ts` | 586 | Validacion `createInvoice()` | CORR-005 |
| 5 | `backend/src/modules/sales/sales.controller.ts` | 204 | Zod enum schema | CORR-001.2 |
| 6 | `frontend/src/shared/constants/status.ts` | 3-26 | Labels y colores | CORR-001.2 |
| 7 | `frontend/src/pages/dashboard/DashboardPage.tsx` | 50-59 | Mock data | CORR-001.6 |

#### Orden de Ejecucion

```
PASO 1: Verificar DDL (07-sales.sql) - ya tiene valores correctos
    │
PASO 2: Actualizar Backend (orders.service.ts)
    │   - Linea 48: tipo interface
    │   - Linea 526: transicion confirm() → 'sent'
    │   - Linea 586: validacion createInvoice()
    │
PASO 3: Actualizar Controller (sales.controller.ts)
    │   - Linea 204: Zod enum
    │
PASO 4: Actualizar Frontend constants (status.ts)
    │   - DOCUMENT_STATUS
    │   - DOCUMENT_STATUS_LABELS
    │   - DOCUMENT_STATUS_COLORS
    │
PASO 5: Actualizar Dashboard mock data (DashboardPage.tsx)
```

#### Cambios Detallados

**Archivo: `orders.service.ts:48`**
```typescript
// ANTES
status: 'draft' | 'confirmed' | 'done' | 'cancelled';

// DESPUES
status: 'draft' | 'sent' | 'sale' | 'done' | 'cancelled';
```

**Archivo: `orders.service.ts:526`**
```typescript
// ANTES (metodo confirm())
status = 'confirmed'

// DESPUES
status = 'sent'
```

**Archivo: `orders.service.ts:586`**
```typescript
// ANTES (validacion createInvoice)
if (!['confirmed', 'done'].includes(order.status))

// DESPUES
if (!['sale', 'done'].includes(order.status))
```

**Archivo: `sales.controller.ts:204`**
```typescript
// ANTES
status: z.enum(['draft', 'confirmed', 'done', 'cancelled']).optional()

// DESPUES
status: z.enum(['draft', 'sent', 'sale', 'done', 'cancelled']).optional()
```

**Archivo: `status.ts`**
```typescript
// ANTES
export const DOCUMENT_STATUS = {
  DRAFT: 'draft',
  SENT: 'sent',
  CONFIRMED: 'confirmed',
  DONE: 'done',
  CANCELLED: 'cancelled',
} as const;

// DESPUES
export const DOCUMENT_STATUS = {
  DRAFT: 'draft',
  SENT: 'sent',
  SALE: 'sale',
  DONE: 'done',
  CANCELLED: 'cancelled',
} as const;

// Labels (actualizar)
export const DOCUMENT_STATUS_LABELS: Record<string, string> = {
  [DOCUMENT_STATUS.DRAFT]: 'Borrador',
  [DOCUMENT_STATUS.SENT]: 'Enviado',
  [DOCUMENT_STATUS.SALE]: 'Venta',  // NUEVO
  [DOCUMENT_STATUS.DONE]: 'Completado',
  [DOCUMENT_STATUS.CANCELLED]: 'Cancelado',
};

// Colors (actualizar)
export const DOCUMENT_STATUS_COLORS: Record<string, string> = {
  [DOCUMENT_STATUS.DRAFT]: 'bg-gray-100 text-gray-800',
  [DOCUMENT_STATUS.SENT]: 'bg-blue-100 text-blue-800',
  [DOCUMENT_STATUS.SALE]: 'bg-cyan-100 text-cyan-800',  // NUEVO
  [DOCUMENT_STATUS.DONE]: 'bg-green-100 text-green-800',
  [DOCUMENT_STATUS.CANCELLED]: 'bg-red-100 text-red-800',
};
```

#### NO Modificar (Otros modulos con 'confirmed')

| Archivo | Razon |
|---------|-------|
| `purchases.service.ts` | Enum separado para Purchase Orders |
| `quotations.service.ts` | Enum separado `quotation_status` |
| `pickings.service.ts` | Enum separado para movimientos |
| `adjustments.service.ts` | Enum separado para ajustes |

#### Validacion Post-Cambio

```bash
# 1. Verificar que el backend compila
npm run build

# 2. Verificar que no hay 'confirmed' en sales orders
grep -r "confirmed" backend/src/modules/sales/orders.service.ts
# Debe retornar 0 resultados

# 3. Test manual: crear orden, confirmar, verificar status = 'sent'
```

---

### CORR-002: firstName/lastName vs full_name

**Prioridad:** CRITICA
**Tiempo estimado:** 2-3 horas
**Riesgo de ruptura:** BAJO (solucion transformador)

#### Problema
- Frontend envia: `firstName` + `lastName`
- Backend espera: `full_name`
- BD almacena: `full_name`

#### Solucion Elegida: TRANSFORMADOR EN FRONTEND

Esta solucion no requiere cambios en BD ni Backend, minimizando riesgo.

#### Archivos a Crear (2 archivos nuevos)

| # | Archivo | Accion |
|---|---------|--------|
| 1 | `frontend/src/services/api/transformers/user.transformer.ts` | CREAR |
| 2 | `frontend/src/services/api/transformers/index.ts` | CREAR |

#### Archivos a Modificar (2 archivos)

| # | Archivo | Cambio |
|---|---------|--------|
| 3 | `frontend/src/features/users/api/users.api.ts` | Aplicar transformador |
| 4 | `frontend/src/services/api/auth.api.ts` | Aplicar transformador |

#### Orden de Ejecucion

```
PASO 1: Crear transformadores (user.transformer.ts)
    │
PASO 2: Crear index de exportacion (transformers/index.ts)
    │
PASO 3: Aplicar en users.api.ts
    │   - create()
    │   - update()
    │   - getAll() response
    │   - getById() response
    │
PASO 4: Aplicar en auth.api.ts
    │   - register()
    │   - login() response
    │   - getCurrentUser() response
```

#### Cambios Detallados

**Archivo NUEVO: `transformers/user.transformer.ts`**
```typescript
// frontend/src/services/api/transformers/user.transformer.ts

import type { User, CreateUserDto, UpdateUserDto } from '@/features/users/types/user.types';

interface BackendUser {
  id: string;
  email: string;
  full_name: string;
  phone?: string;
  avatar?: string;
  role_id: string;
  status: string;
  tenant_id: string;
  company_id: string;
  last_login_at?: string;
  created_at: string;
  updated_at?: string;
}

interface BackendCreateUserDto {
  email: string;
  password: string;
  full_name: string;
  phone?: string;
  role_id: string;
  company_id?: string;
}

export const userTransformers = {
  /**
   * Transforma datos del frontend al formato del backend
   * firstName + lastName -> full_name
   */
  toBackend: (data: CreateUserDto | UpdateUserDto): BackendCreateUserDto => {
    const { firstName, lastName, roleId, ...rest } = data as any;
    return {
      ...rest,
      full_name: `${firstName || ''} ${lastName || ''}`.trim(),
      role_id: roleId,
    };
  },

  /**
   * Transforma datos del backend al formato del frontend
   * full_name -> firstName + lastName
   */
  fromBackend: (data: BackendUser): User => {
    const nameParts = (data.full_name || '').split(' ');
    const firstName = nameParts[0] || '';
    const lastName = nameParts.slice(1).join(' ') || '';

    return {
      id: data.id,
      email: data.email,
      firstName,
      lastName,
      phone: data.phone,
      avatar: data.avatar,
      roleId: data.role_id,
      status: data.status as any,
      tenantId: data.tenant_id,
      companyId: data.company_id,
      lastLoginAt: data.last_login_at,
      createdAt: data.created_at,
      updatedAt: data.updated_at,
    };
  },

  /**
   * Transforma array de usuarios del backend
   */
  fromBackendList: (data: BackendUser[]): User[] => {
    return data.map(user => userTransformers.fromBackend(user));
  },
};
```

**Archivo NUEVO: `transformers/index.ts`**
```typescript
// frontend/src/services/api/transformers/index.ts

export { userTransformers } from './user.transformer';
```

**Archivo: `users.api.ts` (modificar)**
```typescript
// Agregar import
import { userTransformers } from '@/services/api/transformers';

// Modificar metodos
export const usersApi = {
  getAll: async (filters?: UserFilters): Promise<PaginatedResponse<User>> => {
    const response = await api.get<PaginatedResponse<any>>(BASE_URL, { params: filters });
    return {
      ...response.data,
      data: userTransformers.fromBackendList(response.data.data),
    };
  },

  getById: async (id: string): Promise<User> => {
    const response = await api.get<any>(`${BASE_URL}/${id}`);
    return userTransformers.fromBackend(response.data);
  },

  create: async (data: CreateUserDto): Promise<User> => {
    const backendData = userTransformers.toBackend(data);
    const response = await api.post<any>(BASE_URL, backendData);
    return userTransformers.fromBackend(response.data);
  },

  update: async (id: string, data: UpdateUserDto): Promise<User> => {
    const backendData = userTransformers.toBackend(data);
    const response = await api.put<any>(`${BASE_URL}/${id}`, backendData);
    return userTransformers.fromBackend(response.data);
  },

  // ... resto de metodos sin cambios
};
```

**Archivo: `auth.api.ts` (modificar)**
```typescript
// Agregar import
import { userTransformers } from '@/services/api/transformers';

// Modificar metodos que devuelven User
export const authApi = {
  login: async (credentials: LoginCredentials): Promise<AuthResponse> => {
    const response = await api.post<any>(API_ENDPOINTS.AUTH.LOGIN, credentials);
    return {
      ...response.data,
      user: userTransformers.fromBackend(response.data.user),
    };
  },

  register: async (data: RegisterData): Promise<AuthResponse> => {
    const backendData = {
      email: data.email,
      password: data.password,
      full_name: `${data.firstName} ${data.lastName}`.trim(),
    };
    const response = await api.post<any>(API_ENDPOINTS.AUTH.REGISTER, backendData);
    return {
      ...response.data,
      user: userTransformers.fromBackend(response.data.user),
    };
  },

  getCurrentUser: async (): Promise<User> => {
    const response = await api.get<any>(API_ENDPOINTS.AUTH.ME);
    return userTransformers.fromBackend(response.data);
  },

  // ... resto sin cambios
};
```

#### Validacion Post-Cambio

```bash
# 1. Verificar que frontend compila
npm run build

# 2. Test manual:
#    - Crear usuario con firstName="Juan" lastName="Perez Garcia"
#    - Verificar en BD: full_name = "Juan Perez Garcia"
#    - Verificar en UI: muestra firstName="Juan" lastName="Perez Garcia"

# 3. Test registro:
#    - Registrar usuario nuevo
#    - Verificar login funciona
#    - Verificar nombre se muestra correctamente
```

---

### CORR-003: Agregar tenant_id a Tablas de Lineas

**Prioridad:** CRITICA (Seguridad)
**Tiempo estimado:** 4-6 horas
**Riesgo de ruptura:** MEDIO (migracion de datos)

#### Problema
12 tablas de lineas de documentos no tienen `tenant_id`, comprometiendo aislamiento multi-tenant.

#### Tablas Afectadas

| # | Tabla | Tabla Padre | Complejidad |
|---|-------|-------------|-------------|
| 1 | `financial.journal_entry_lines` | journal_entries | Baja |
| 2 | `financial.invoice_lines` | invoices | Baja |
| 3 | `purchase.purchase_order_lines` | purchase_orders | Baja |
| 4 | `sales.sales_order_lines` | sales_orders | Baja |
| 5 | `sales.quotation_lines` | quotations | Baja |
| 6 | `sales.pricelist_items` | pricelists | Media |
| 7 | `inventory.stock_quants` | products + locations | **ALTA** |
| 8 | `billing.invoice_lines` | billing.invoices | Baja |
| 9 | `system.message_followers` | Polimorfica | **ALTA** |
| 10 | `system.dashboard_widgets` | dashboards | Baja |

#### Orden de Ejecucion (4 fases)

```
FASE 1: Agregar columnas NULLABLE (sin downtime)
    │
    ▼
FASE 2: Migrar datos (UPDATE con subquery)
    │
    ▼
FASE 3: Cambiar a NOT NULL + FK + RLS
    │
    ▼
FASE 4: Actualizar backend services
```

#### Script de Migracion DDL

**Archivo: `database/migrations/20251206_add_tenant_id_to_lines.sql`**
```sql
-- ============================================================
-- MIGRACION: Agregar tenant_id a tablas de lineas
-- Fecha: 2025-12-06
-- Autor: Requirements-Analyst
-- ============================================================

BEGIN;

-- ============================================================
-- FASE 1: Agregar columnas NULLABLE
-- ============================================================

-- 1.1 financial.journal_entry_lines
ALTER TABLE financial.journal_entry_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.2 financial.invoice_lines
ALTER TABLE financial.invoice_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.3 purchase.purchase_order_lines
ALTER TABLE purchase.purchase_order_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.4 sales.sales_order_lines
ALTER TABLE sales.sales_order_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.5 sales.quotation_lines
ALTER TABLE sales.quotation_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.6 sales.pricelist_items
ALTER TABLE sales.pricelist_items
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.7 inventory.stock_quants
ALTER TABLE inventory.stock_quants
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.8 billing.invoice_lines
ALTER TABLE billing.invoice_lines
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.9 system.message_followers
ALTER TABLE system.message_followers
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- 1.10 system.dashboard_widgets
ALTER TABLE system.dashboard_widgets
ADD COLUMN IF NOT EXISTS tenant_id UUID;

-- ============================================================
-- FASE 2: Migrar datos (heredar tenant_id de tabla padre)
-- ============================================================

-- 2.1 journal_entry_lines <- journal_entries
UPDATE financial.journal_entry_lines l
SET tenant_id = e.tenant_id
FROM financial.journal_entries e
WHERE l.entry_id = e.id AND l.tenant_id IS NULL;

-- 2.2 invoice_lines <- invoices
UPDATE financial.invoice_lines l
SET tenant_id = i.tenant_id
FROM financial.invoices i
WHERE l.invoice_id = i.id AND l.tenant_id IS NULL;

-- 2.3 purchase_order_lines <- purchase_orders
UPDATE purchase.purchase_order_lines l
SET tenant_id = o.tenant_id
FROM purchase.purchase_orders o
WHERE l.order_id = o.id AND l.tenant_id IS NULL;

-- 2.4 sales_order_lines <- sales_orders
UPDATE sales.sales_order_lines l
SET tenant_id = o.tenant_id
FROM sales.sales_orders o
WHERE l.order_id = o.id AND l.tenant_id IS NULL;

-- 2.5 quotation_lines <- quotations
UPDATE sales.quotation_lines l
SET tenant_id = q.tenant_id
FROM sales.quotations q
WHERE l.quotation_id = q.id AND l.tenant_id IS NULL;

-- 2.6 pricelist_items <- pricelists
UPDATE sales.pricelist_items l
SET tenant_id = p.tenant_id
FROM sales.pricelists p
WHERE l.pricelist_id = p.id AND l.tenant_id IS NULL;

-- 2.7 stock_quants <- products (usa product.tenant_id)
UPDATE inventory.stock_quants q
SET tenant_id = p.tenant_id
FROM inventory.products p
WHERE q.product_id = p.id AND q.tenant_id IS NULL;

-- 2.8 billing.invoice_lines <- billing.invoices
UPDATE billing.invoice_lines l
SET tenant_id = i.tenant_id
FROM billing.invoices i
WHERE l.invoice_id = i.id AND l.tenant_id IS NULL;

-- 2.9 message_followers: usar user.tenant_id o partner.tenant_id
UPDATE system.message_followers f
SET tenant_id = COALESCE(
  (SELECT u.tenant_id FROM auth.users u WHERE u.id = f.user_id),
  (SELECT p.tenant_id FROM core.partners p WHERE p.id = f.partner_id)
)
WHERE f.tenant_id IS NULL;

-- 2.10 dashboard_widgets <- dashboards
UPDATE system.dashboard_widgets w
SET tenant_id = d.tenant_id
FROM system.dashboards d
WHERE w.dashboard_id = d.id AND w.tenant_id IS NULL;

-- ============================================================
-- FASE 3: Cambiar a NOT NULL + Foreign Keys
-- ============================================================

-- 3.1 Constraints NOT NULL
ALTER TABLE financial.journal_entry_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE financial.invoice_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE purchase.purchase_order_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE sales.sales_order_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE sales.quotation_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE sales.pricelist_items
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE inventory.stock_quants
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE billing.invoice_lines
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE system.message_followers
ALTER COLUMN tenant_id SET NOT NULL;

ALTER TABLE system.dashboard_widgets
ALTER COLUMN tenant_id SET NOT NULL;

-- 3.2 Foreign Keys a auth.tenants
ALTER TABLE financial.journal_entry_lines
ADD CONSTRAINT fk_journal_entry_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE financial.invoice_lines
ADD CONSTRAINT fk_invoice_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE purchase.purchase_order_lines
ADD CONSTRAINT fk_po_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE sales.sales_order_lines
ADD CONSTRAINT fk_so_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE sales.quotation_lines
ADD CONSTRAINT fk_quotation_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE sales.pricelist_items
ADD CONSTRAINT fk_pricelist_items_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE inventory.stock_quants
ADD CONSTRAINT fk_stock_quants_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE billing.invoice_lines
ADD CONSTRAINT fk_billing_invoice_lines_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE system.message_followers
ADD CONSTRAINT fk_message_followers_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

ALTER TABLE system.dashboard_widgets
ADD CONSTRAINT fk_dashboard_widgets_tenant
FOREIGN KEY (tenant_id) REFERENCES auth.tenants(id);

-- ============================================================
-- FASE 4: Indices para performance
-- ============================================================

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_journal_entry_lines_tenant ON financial.journal_entry_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_invoice_lines_tenant ON financial.invoice_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_po_lines_tenant ON purchase.purchase_order_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_so_lines_tenant ON sales.sales_order_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_quotation_lines_tenant ON sales.quotation_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_pricelist_items_tenant ON sales.pricelist_items(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_stock_quants_tenant ON inventory.stock_quants(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_billing_invoice_lines_tenant ON billing.invoice_lines(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_message_followers_tenant ON system.message_followers(tenant_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_dashboard_widgets_tenant ON system.dashboard_widgets(tenant_id);

-- ============================================================
-- FASE 5: RLS Policies
-- ============================================================

-- Habilitar RLS
ALTER TABLE financial.journal_entry_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE financial.invoice_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE purchase.purchase_order_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales.sales_order_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales.quotation_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE sales.pricelist_items ENABLE ROW LEVEL SECURITY;
ALTER TABLE inventory.stock_quants ENABLE ROW LEVEL SECURITY;
ALTER TABLE billing.invoice_lines ENABLE ROW LEVEL SECURITY;
ALTER TABLE system.message_followers ENABLE ROW LEVEL SECURITY;
ALTER TABLE system.dashboard_widgets ENABLE ROW LEVEL SECURITY;

-- Policies de aislamiento
CREATE POLICY tenant_isolation_journal_entry_lines
ON financial.journal_entry_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_invoice_lines
ON financial.invoice_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_po_lines
ON purchase.purchase_order_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_so_lines
ON sales.sales_order_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_quotation_lines
ON sales.quotation_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_pricelist_items
ON sales.pricelist_items
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_stock_quants
ON inventory.stock_quants
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_billing_invoice_lines
ON billing.invoice_lines
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_message_followers
ON system.message_followers
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_dashboard_widgets
ON system.dashboard_widgets
USING (tenant_id = auth.get_current_tenant_id());

COMMIT;

-- ============================================================
-- VERIFICACION
-- ============================================================

-- Verificar que todas las columnas existen
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE column_name = 'tenant_id'
AND table_schema IN ('financial', 'purchase', 'sales', 'inventory', 'billing', 'system')
ORDER BY table_schema, table_name;

-- Verificar que no hay NULLs
SELECT 'financial.journal_entry_lines' as tabla, COUNT(*) as nulls
FROM financial.journal_entry_lines WHERE tenant_id IS NULL
UNION ALL
SELECT 'financial.invoice_lines', COUNT(*)
FROM financial.invoice_lines WHERE tenant_id IS NULL
UNION ALL
SELECT 'sales.sales_order_lines', COUNT(*)
FROM sales.sales_order_lines WHERE tenant_id IS NULL;
-- Debe retornar 0 para todas
```

#### Cambios en Backend Services

Cada servicio que inserta en estas tablas debe agregar `tenant_id`:

**Ejemplo: `orders.service.ts` - addLine()**
```typescript
// ANTES (linea ~400)
INSERT INTO sales.sales_order_lines
(order_id, product_id, description, ...)
VALUES ($1, $2, $3, ...)

// DESPUES
INSERT INTO sales.sales_order_lines
(order_id, product_id, description, tenant_id, ...)
VALUES ($1, $2, $3, $4, ...)
// Agregar tenantId como parametro
```

#### Validacion Post-Cambio

```sql
-- 1. Verificar columnas creadas
SELECT table_name, column_name
FROM information_schema.columns
WHERE column_name = 'tenant_id'
AND table_schema IN ('financial', 'sales', 'purchase', 'inventory');

-- 2. Verificar no hay NULLs
SELECT COUNT(*) FROM sales.sales_order_lines WHERE tenant_id IS NULL;
-- Debe ser 0

-- 3. Verificar RLS funciona
SET app.current_tenant_id = 'tenant-uuid-1';
SELECT COUNT(*) FROM sales.sales_order_lines;
-- Solo debe mostrar datos del tenant-1
```

---

### CORR-004: Crear Indices FK Faltantes

**Prioridad:** ALTA (Performance)
**Tiempo estimado:** 1 hora
**Riesgo de ruptura:** NINGUNO

#### Foreign Keys sin Indice

| # | Tabla | Columna FK | Indice a crear |
|---|-------|-----------|----------------|
| 1 | `core.partners` | `currency_id` | `idx_partners_currency_id` |
| 2 | `core.partners` | `payment_term_id` | `idx_partners_payment_term_id` |
| 3 | `core.partners` | `pricelist_id` | `idx_partners_pricelist_id` |
| 4 | `financial.invoice_lines` | `product_id` | `idx_invoice_lines_product_id` |

#### Script DDL

**Archivo: `database/migrations/20251206_add_missing_fk_indexes.sql`**
```sql
-- Indices para FK sin indice (CONCURRENTLY para no bloquear)

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_partners_currency_id ON core.partners(currency_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_partners_payment_term_id ON core.partners(payment_term_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_partners_pricelist_id ON core.partners(pricelist_id);

CREATE INDEX CONCURRENTLY IF NOT EXISTS
idx_invoice_lines_product_id ON financial.invoice_lines(product_id);

-- Verificacion
SELECT indexname, tablename
FROM pg_indexes
WHERE indexname LIKE 'idx_partners_%' OR indexname LIKE 'idx_invoice_lines_%';
```

---

### CORR-005: Implementar Calculo de Impuestos

**Prioridad:** CRITICA
**Tiempo estimado:** 8-10 horas
**Riesgo de ruptura:** BAJO (nueva funcionalidad)

#### Problema
Todos los documentos tienen `amount_tax = 0` debido a TODOs sin implementar.

#### Archivos a Modificar

| # | Archivo | Linea | Cambio |
|---|---------|-------|--------|
| 1 | `backend/src/modules/financial/taxes.service.ts` | N/A | Agregar `calculateTaxes()` |
| 2 | `backend/src/modules/sales/orders.service.ts` | 386 | Usar `calculateTaxes()` |
| 3 | `backend/src/modules/sales/orders.service.ts` | 462 | Usar en `updateLine()` |
| 4 | `backend/src/modules/sales/quotations.service.ts` | 337 | Usar `calculateTaxes()` |
| 5 | `backend/src/modules/sales/quotations.service.ts` | 408 | Usar en `updateLine()` |
| 6 | `backend/src/modules/financial/invoices.service.ts` | 326 | Usar `calculateTaxes()` |
| 7 | `backend/src/modules/financial/invoices.service.ts` | 398 | Usar en `updateLine()` |

#### Implementacion

**Archivo: `taxes.service.ts` (agregar metodos)**
```typescript
// Agregar al final de TaxesService

interface TaxCalculationResult {
  amountUntaxed: number;
  amountTax: number;
  amountTotal: number;
  taxBreakdown: {
    taxId: string;
    taxName: string;
    taxRate: number;
    base: number;
    taxAmount: number;
  }[];
}

interface LineData {
  quantity: number;
  priceUnit: number;
  discount: number;
  taxIds: string[];
}

/**
 * Calcula impuestos para una linea de documento
 */
async calculateTaxes(
  lineData: LineData,
  tenantId: string,
  transactionType: 'sales' | 'purchase' = 'sales'
): Promise<TaxCalculationResult> {

  // 1. Validar inputs
  if (lineData.quantity <= 0) {
    throw new ValidationError('Quantity must be greater than 0');
  }
  if (lineData.priceUnit < 0) {
    throw new ValidationError('Price must be >= 0');
  }
  if (lineData.discount < 0 || lineData.discount > 100) {
    throw new ValidationError('Discount must be between 0 and 100');
  }

  // 2. Calcular base imponible
  const subtotal = lineData.quantity * lineData.priceUnit;
  const discountAmount = subtotal * (lineData.discount / 100);
  const amountUntaxed = this.roundToDecimals(subtotal - discountAmount, 2);

  // 3. Si no hay impuestos, retornar solo base
  if (!lineData.taxIds || lineData.taxIds.length === 0) {
    return {
      amountUntaxed,
      amountTax: 0,
      amountTotal: amountUntaxed,
      taxBreakdown: [],
    };
  }

  // 4. Obtener impuestos de la BD
  const taxes = await this.getTaxesByIds(lineData.taxIds, tenantId);

  // 5. Validar tipo de impuesto
  const validTypes = transactionType === 'sales'
    ? ['sales', 'all']
    : ['purchase', 'all'];

  for (const tax of taxes) {
    if (!validTypes.includes(tax.tax_type)) {
      throw new ValidationError(
        `Tax ${tax.code} is not applicable to ${transactionType} transactions`
      );
    }
  }

  // 6. Calcular impuestos
  const taxBreakdown = taxes
    .filter(tax => tax.active)
    .map(tax => {
      const taxAmount = this.roundToDecimals(amountUntaxed * tax.rate, 2);
      return {
        taxId: tax.id,
        taxName: tax.name,
        taxRate: tax.rate * 100, // Convertir a porcentaje
        base: amountUntaxed,
        taxAmount,
      };
    });

  // 7. Sumar impuestos
  const amountTax = this.roundToDecimals(
    taxBreakdown.reduce((sum, t) => sum + t.taxAmount, 0),
    2
  );

  // 8. Total
  const amountTotal = this.roundToDecimals(amountUntaxed + amountTax, 2);

  return {
    amountUntaxed,
    amountTax,
    amountTotal,
    taxBreakdown,
  };
}

/**
 * Obtiene multiples impuestos por IDs
 */
private async getTaxesByIds(taxIds: string[], tenantId: string): Promise<Tax[]> {
  if (taxIds.length === 0) return [];

  const result = await query<Tax>(
    `SELECT * FROM financial.taxes
     WHERE id = ANY($1) AND tenant_id = $2`,
    [taxIds, tenantId]
  );

  return result.rows;
}

/**
 * Redondea a N decimales
 */
private roundToDecimals(value: number, decimals: number): number {
  const factor = Math.pow(10, decimals);
  return Math.round(value * factor) / factor;
}
```

**Archivo: `orders.service.ts:386` (modificar)**
```typescript
// ANTES
const subtotal = dto.quantity * dto.price_unit;
const discountAmount = subtotal * (dto.discount || 0) / 100;
const amountUntaxed = subtotal - discountAmount;
// TODO: Calculate taxes properly based on tax_ids
const amountTax = 0;
const amountTotal = amountUntaxed + amountTax;

// DESPUES
const taxResult = await this.taxesService.calculateTaxes(
  {
    quantity: dto.quantity,
    priceUnit: dto.price_unit,
    discount: dto.discount || 0,
    taxIds: dto.tax_ids || [],
  },
  tenantId,
  'sales'
);

const amountUntaxed = taxResult.amountUntaxed;
const amountTax = taxResult.amountTax;
const amountTotal = taxResult.amountTotal;
```

#### Dependencias

- Requiere que `TaxesService` este inyectado en `OrdersService`, `QuotationsService`, `InvoicesService`
- Agregar import y constructor injection

---

### CORR-006: Consolidar Tipos Frontend

**Prioridad:** ALTA
**Tiempo estimado:** 2-3 horas
**Riesgo de ruptura:** BAJO

#### Problema
Hay 2 definiciones conflictivas de `User`:
- `shared/types/entities.types.ts` - define `roles: string[]`
- `features/users/types/user.types.ts` - define `roleId: string`

#### Solucion
Consolidar en `features/*/types/` como fuente de verdad y hacer que `shared/types/` solo re-exporte.

#### Archivos a Modificar

| # | Archivo | Cambio |
|---|---------|--------|
| 1 | `shared/types/entities.types.ts` | Eliminar User, Partner, Company duplicados |
| 2 | `shared/types/entities.types.ts` | Re-exportar desde features/ |
| 3 | `shared/stores/useAuthStore.ts` | Actualizar import |

#### Cambios Detallados

**Archivo: `shared/types/entities.types.ts`**
```typescript
// ANTES (conflicto)
export interface User extends BaseEntity {
  email: string;
  firstName: string;
  lastName: string;
  isActive: boolean;
  roles: string[];  // CONFLICTO con roleId
  tenantId: string;
}

// DESPUES (re-exportar)
// Mantener BaseEntity local
export interface BaseEntity {
  id: string;
  createdAt: string;
  updatedAt?: string;
}

// Re-exportar tipos de features
export type { User, CreateUserDto, UpdateUserDto } from '@/features/users/types/user.types';
export type { Partner, CreatePartnerDto, UpdatePartnerDto } from '@/features/partners/types/partner.types';
export type { Company, CreateCompanyDto, UpdateCompanyDto } from '@/features/companies/types/company.types';

// Eliminar interfaces duplicadas Product, etc.
```

---

## FASE 2: CORRECCIONES ALTAS

### CORR-007: Crear BaseService Reutilizable

**Prioridad:** ALTA
**Tiempo estimado:** 4-6 horas

#### Problema
~5,000 lineas de codigo duplicado en metodos `findAll()`, `update()` entre 28+ servicios.

#### Solucion
Crear clase abstracta `BaseService<T>` con logica comun.

**Archivo: `backend/src/shared/services/base.service.ts`**
```typescript
export abstract class BaseService<T, CreateDto, UpdateDto> {
  protected abstract tableName: string;
  protected abstract selectFields: string;

  async findAll(
    tenantId: string,
    filters: PaginationFilters = {}
  ): Promise<PaginatedResult<T>> {
    const { page = 1, limit = 20, ...customFilters } = filters;
    const offset = (page - 1) * limit;

    let whereClause = 'WHERE tenant_id = $1';
    const params: any[] = [tenantId];
    let paramIndex = 2;

    // Aplicar filtros custom
    for (const [key, value] of Object.entries(customFilters)) {
      if (value !== undefined && value !== null) {
        whereClause += ` AND ${key} = $${paramIndex++}`;
        params.push(value);
      }
    }

    // Count total
    const countResult = await queryOne<{ count: number }>(
      `SELECT COUNT(*) as count FROM ${this.tableName} ${whereClause}`,
      params
    );

    // Get data
    const dataResult = await query<T>(
      `SELECT ${this.selectFields}
       FROM ${this.tableName}
       ${whereClause}
       ORDER BY created_at DESC
       LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
      [...params, limit, offset]
    );

    return {
      data: dataResult.rows,
      total: countResult?.count || 0,
      page,
      limit,
      totalPages: Math.ceil((countResult?.count || 0) / limit),
    };
  }

  // ... otros metodos comunes
}
```

---

### CORR-008: Implementar RLS en CRM y HR

**Prioridad:** ALTA
**Tiempo estimado:** 2-3 horas

#### Problema
Schemas `crm` y `hr` no tienen RLS policies.

#### Script DDL
```sql
-- CRM
ALTER TABLE crm.leads ENABLE ROW LEVEL SECURITY;
ALTER TABLE crm.opportunities ENABLE ROW LEVEL SECURITY;
ALTER TABLE crm.activities ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_leads ON crm.leads
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_opportunities ON crm.opportunities
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_crm_activities ON crm.activities
USING (tenant_id = auth.get_current_tenant_id());

-- HR
ALTER TABLE hr.employees ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.departments ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.contracts ENABLE ROW LEVEL SECURITY;
ALTER TABLE hr.leaves ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation_employees ON hr.employees
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_departments ON hr.departments
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_contracts ON hr.contracts
USING (tenant_id = auth.get_current_tenant_id());

CREATE POLICY tenant_isolation_leaves ON hr.leaves
USING (tenant_id = auth.get_current_tenant_id());
```

---

## CRONOGRAMA DE EJECUCION

```
SEMANA 1
├── Dia 1-2: CORR-001 (Status Enum) - 4h
├── Dia 2-3: CORR-002 (firstName Transformer) - 3h
├── Dia 3-4: CORR-004 (FK Indices) - 1h
└── Dia 4-5: CORR-006 (Types Frontend) - 3h

SEMANA 2
├── Dia 1-3: CORR-003 (tenant_id Migration) - 6h
├── Dia 3-5: CORR-005 (Taxes Calculation) - 10h
└── Buffer para testing - 4h

SEMANA 3
├── Dia 1-2: CORR-007 (BaseService) - 6h
├── Dia 3: CORR-008 (RLS CRM/HR) - 3h
└── Dia 4-5: Testing integral + Documentacion
```

---

## ROLLBACK PLAN

### CORR-001: Revertir Status Enum
```bash
# Revertir cambios en Git
git checkout HEAD~1 -- backend/src/modules/sales/orders.service.ts
git checkout HEAD~1 -- backend/src/modules/sales/sales.controller.ts
git checkout HEAD~1 -- frontend/src/shared/constants/status.ts
```

### CORR-003: Revertir tenant_id
```sql
-- ROLLBACK (solo si es necesario)
ALTER TABLE financial.journal_entry_lines DROP COLUMN tenant_id;
ALTER TABLE financial.invoice_lines DROP COLUMN tenant_id;
-- ... etc para cada tabla
```

### CORR-005: Revertir Taxes
```typescript
// Revertir a:
const amountTax = 0; // TODO: Calculate taxes
```

---

## CHECKLIST DE VALIDACION FINAL

- [ ] CORR-001: Orders pueden transicionar draft → sent → sale → done
- [ ] CORR-001: Frontend muestra labels correctos
- [ ] CORR-002: Crear usuario funciona con firstName/lastName
- [ ] CORR-002: Login retorna firstName/lastName parseados
- [ ] CORR-003: Todas las tablas tienen tenant_id NOT NULL
- [ ] CORR-003: RLS policies funcionan (test cross-tenant)
- [ ] CORR-004: Indices creados y usados (EXPLAIN ANALYZE)
- [ ] CORR-005: amount_tax calculado correctamente
- [ ] CORR-005: Multiples impuestos suman correctamente
- [ ] CORR-006: No hay errores de tipos en frontend
- [ ] CORR-007: BaseService reduce duplicacion
- [ ] CORR-008: CRM y HR aislados por tenant

---

**Documento generado por Requirements-Analyst**
**Fecha:** 2025-12-06
**Pendiente:** Aprobacion del equipo antes de ejecutar
