# 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 = { [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 = { [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> => { const response = await api.get>(BASE_URL, { params: filters }); return { ...response.data, data: userTransformers.fromBackendList(response.data.data), }; }, getById: async (id: string): Promise => { const response = await api.get(`${BASE_URL}/${id}`); return userTransformers.fromBackend(response.data); }, create: async (data: CreateUserDto): Promise => { const backendData = userTransformers.toBackend(data); const response = await api.post(BASE_URL, backendData); return userTransformers.fromBackend(response.data); }, update: async (id: string, data: UpdateUserDto): Promise => { const backendData = userTransformers.toBackend(data); const response = await api.put(`${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 => { const response = await api.post(API_ENDPOINTS.AUTH.LOGIN, credentials); return { ...response.data, user: userTransformers.fromBackend(response.data.user), }; }, register: async (data: RegisterData): Promise => { const backendData = { email: data.email, password: data.password, full_name: `${data.firstName} ${data.lastName}`.trim(), }; const response = await api.post(API_ENDPOINTS.AUTH.REGISTER, backendData); return { ...response.data, user: userTransformers.fromBackend(response.data.user), }; }, getCurrentUser: async (): Promise => { const response = await api.get(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 { // 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 { if (taxIds.length === 0) return []; const result = await query( `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` con logica comun. **Archivo: `backend/src/shared/services/base.service.ts`** ```typescript export abstract class BaseService { protected abstract tableName: string; protected abstract selectFields: string; async findAll( tenantId: string, filters: PaginationFilters = {} ): Promise> { 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( `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