commit f92504b11140725c8bb342e5267bd08d70326000 Author: rckrdmrd Date: Sun Jan 4 07:04:02 2026 -0600 Initial commit - erp-retail-database diff --git a/HERENCIA-ERP-CORE.md b/HERENCIA-ERP-CORE.md new file mode 100644 index 0000000..37faa13 --- /dev/null +++ b/HERENCIA-ERP-CORE.md @@ -0,0 +1,196 @@ +# Herencia de Base de Datos - ERP Core -> Retail + +**Fecha:** 2025-12-08 +**Versión:** 1.0 +**Vertical:** Retail +**Nivel:** 2B.2 + +--- + +## RESUMEN + +La vertical de Retail hereda los schemas base del ERP Core y extiende con schemas específicos del dominio de punto de venta y comercio minorista. + +**Ubicación DDL Core:** `apps/erp-core/database/ddl/` + +--- + +## ARQUITECTURA DE HERENCIA + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ ERP CORE (Base) │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ auth │ │ core │ │financial│ │inventory│ │ purchase │ │ +│ │ 26 tbl │ │ 12 tbl │ │ 15 tbl │ │ 15 tbl │ │ 8 tbl │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ sales │ │analytics│ │ system │ │ crm │ │ +│ │ 6 tbl │ │ 5 tbl │ │ 10 tbl │ │ 5 tbl │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ TOTAL: ~102 tablas heredadas │ +└─────────────────────────────────────────────────────────────────┘ + │ + │ HEREDA + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ RETAIL (Extensiones) │ +│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ +│ │ pos │ │ stores │ │ pricing │ │ +│ │ (punto venta) │ │ (sucursales) │ │ (promociones) │ │ +│ └───────────────┘ └───────────────┘ └───────────────┘ │ +│ EXTENSIONES: ~30 tablas (planificadas) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## SCHEMAS HEREDADOS DEL CORE + +| Schema | Tablas | Uso en Retail | +|--------|--------|---------------| +| `auth` | 26 | Autenticación, usuarios por sucursal | +| `core` | 12 | Partners (clientes), catálogos | +| `financial` | 15 | Facturas, cuentas, caja | +| `inventory` | 15 | Inventario multi-sucursal | +| `purchase` | 8 | Compras a proveedores | +| `sales` | 6 | Ventas base | +| `crm` | 5 | Clientes frecuentes | +| `analytics` | 5 | Métricas de venta | +| `system` | 10 | Notificaciones | + +**Total heredado:** ~102 tablas + +--- + +## SCHEMAS ESPECÍFICOS DE RETAIL (Planificados) + +### 1. Schema `pos` (estimado 12+ tablas) + +**Propósito:** Punto de venta y operaciones de caja + +```sql +-- Tablas principales planificadas: +pos.cash_registers -- Cajas registradoras +pos.cash_sessions -- Sesiones de caja +pos.pos_orders -- Tickets/ventas POS +pos.pos_order_lines -- Líneas de ticket +pos.payment_methods -- Métodos de pago +pos.cash_movements -- Movimientos de caja +pos.cash_counts -- Cortes de caja +pos.receipts -- Recibos +``` + +### 2. Schema `stores` (estimado 8+ tablas) + +**Propósito:** Gestión de sucursales + +```sql +-- Tablas principales planificadas: +stores.branches -- Sucursales +stores.branch_inventory -- Inventario por sucursal +stores.transfers -- Transferencias entre sucursales +stores.transfer_lines -- Líneas de transferencia +stores.branch_employees -- Empleados por sucursal +``` + +### 3. Schema `pricing` (estimado 10+ tablas) + +**Propósito:** Precios y promociones + +```sql +-- Extiende: sales schema del core +pricing.price_lists -- Listas de precios +pricing.promotions -- Promociones +pricing.discounts -- Descuentos +pricing.loyalty_programs -- Programas de lealtad +pricing.coupons -- Cupones +pricing.price_history -- Historial de precios +``` + +--- + +## SPECS DEL CORE APLICABLES + +**Documento detallado:** `orchestration/00-guidelines/HERENCIA-SPECS-CORE.md` + +### Correcciones de DDL Core (2025-12-08) + +El DDL del ERP-Core fue corregido para resolver FK inválidas: + +1. **stock_valuation_layers**: Campos `journal_entry_id` y `journal_entry_line_id` (antes `account_move_*`) +2. **stock_move_consume_rel**: Nueva tabla de trazabilidad (antes `move_line_consume_rel`) +3. **category_stock_accounts**: FK corregida a `core.product_categories` +4. **product_categories**: ALTERs ahora apuntan a schema `core` + +### SPECS Obligatorias + +| Spec Core | Aplicación en Retail | SP | Estado | +|-----------|---------------------|----:|--------| +| SPEC-SISTEMA-SECUENCIAS | Foliado de tickets y facturas | 8 | ✅ DDL LISTO | +| SPEC-VALORACION-INVENTARIO | Costeo de mercancía | 21 | ✅ DDL LISTO | +| SPEC-SEGURIDAD-API-KEYS-PERMISOS | Control de acceso por sucursal | 31 | ✅ DDL LISTO | +| SPEC-PRICING-RULES | Precios y promociones | 8 | PENDIENTE | +| SPEC-INVENTARIOS-CICLICOS | Conteos en sucursales | 13 | ✅ DDL LISTO | +| SPEC-TRAZABILIDAD-LOTES-SERIES | Productos con lote/serie | 13 | ✅ DDL LISTO | +| SPEC-MAIL-THREAD-TRACKING | Comunicación con clientes | 13 | PENDIENTE | +| SPEC-WIZARD-TRANSIENT-MODEL | Wizards de cierre de caja | 8 | PENDIENTE | + +### SPECS Opcionales + +| Spec Core | Decisión | Razón | +|-----------|----------|-------| +| SPEC-PORTAL-PROVEEDORES | EVALUAR | Para compras centralizadas | +| SPEC-TAREAS-RECURRENTES | EVALUAR | Para reorden automático | + +### SPECS No Aplican + +| Spec Core | Razón | +|-----------|-------| +| SPEC-INTEGRACION-CALENDAR | No requiere calendario de citas | +| SPEC-PROYECTOS-DEPENDENCIAS-BURNDOWN | No hay proyectos largos | +| SPEC-FIRMA-ELECTRONICA-NOM151 | No aplica para tickets POS | + +--- + +## ORDEN DE EJECUCIÓN DDL (Futuro) + +```bash +# PASO 1: Cargar ERP Core (base) +cd apps/erp-core/database +./scripts/reset-database.sh --force + +# PASO 2: Cargar extensiones de Retail +cd apps/verticales/retail/database +psql $DATABASE_URL -f init/00-extensions.sql +psql $DATABASE_URL -f init/01-create-schemas.sql +psql $DATABASE_URL -f init/02-pos-tables.sql +psql $DATABASE_URL -f init/03-stores-tables.sql +psql $DATABASE_URL -f init/04-pricing-tables.sql +``` + +--- + +## MAPEO DE NOMENCLATURA + +| Core | Retail | +|------|--------| +| `core.partners` | Clientes, proveedores | +| `inventory.products` | Productos de venta | +| `inventory.locations` | Almacenes de sucursal | +| `sales.sale_orders` | Base para POS orders | +| `financial.invoices` | Facturas de venta | + +--- + +## REFERENCIAS + +- ERP Core DDL: `apps/erp-core/database/ddl/` +- ERP Core README: `apps/erp-core/database/README.md` +- Directivas: `orchestration/directivas/` +- Inventarios: `orchestration/inventarios/` + +--- + +**Documento de herencia oficial** +**Última actualización:** 2025-12-08 diff --git a/README.md b/README.md new file mode 100644 index 0000000..dff099d --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# Base de Datos - ERP Retail/POS + +## Resumen + +| Aspecto | Valor | +|---------|-------| +| **Schema principal** | `retail` | +| **Tablas específicas** | 16 | +| **ENUMs** | 6 | +| **Hereda de ERP-Core** | 144 tablas (12 schemas) | + +## Prerequisitos + +1. **ERP-Core instalado** con todos sus schemas +2. **Extensiones PostgreSQL**: pg_trgm + +## Orden de Ejecución DDL + +```bash +# 1. Instalar ERP-Core primero +cd apps/erp-core/database +./scripts/reset-database.sh + +# 2. Instalar extensión Retail +cd apps/verticales/retail/database +psql $DATABASE_URL -f init/00-extensions.sql +psql $DATABASE_URL -f init/01-create-schemas.sql +psql $DATABASE_URL -f init/02-rls-functions.sql +psql $DATABASE_URL -f init/03-retail-tables.sql +``` + +## Tablas Implementadas + +### Schema: retail (16 tablas) + +| Tabla | Módulo | Descripción | +|-------|--------|-------------| +| branches | RT-002 | Sucursales | +| cash_registers | RT-001 | Cajas registradoras | +| pos_sessions | RT-001 | Sesiones de POS | +| pos_orders | RT-001 | Ventas/Órdenes | +| pos_order_lines | RT-001 | Líneas de venta | +| pos_payments | RT-001 | Pagos (mixtos) | +| cash_movements | RT-001 | Entradas/salidas efectivo | +| branch_stock | RT-002 | Stock por sucursal | +| stock_transfers | RT-002 | Transferencias | +| stock_transfer_lines | RT-002 | Líneas de transferencia | +| product_barcodes | RT-003 | Códigos de barras | +| promotions | RT-003 | Promociones | +| promotion_products | RT-003 | Productos en promo | +| loyalty_programs | RT-004 | Programas fidelización | +| loyalty_cards | RT-004 | Tarjetas | +| loyalty_transactions | RT-004 | Transacciones puntos | + +## ENUMs + +| Enum | Valores | +|------|---------| +| pos_session_status | opening, open, closing, closed | +| pos_order_status | draft, paid, done, cancelled, refunded | +| payment_method | cash, card, transfer, credit, mixed | +| cash_movement_type | in, out | +| transfer_status | draft, pending, in_transit, received, cancelled | +| promotion_type | percentage, fixed_amount, buy_x_get_y, bundle | + +## Row Level Security + +Todas las tablas tienen RLS con: +```sql +tenant_id = current_setting('app.current_tenant_id', true)::UUID +``` + +## Consideraciones Especiales + +- **Operación offline**: POS puede operar sin conexión +- **Rendimiento**: <100ms por transacción +- **Hardware**: Integración con impresoras y lectores +- **CFDI 4.0**: Facturación en tiempo real + +## Referencias + +- [HERENCIA-ERP-CORE.md](./HERENCIA-ERP-CORE.md) +- [DATABASE_INVENTORY.yml](../orchestration/inventarios/DATABASE_INVENTORY.yml) diff --git a/init/00-extensions.sql b/init/00-extensions.sql new file mode 100644 index 0000000..87e9d45 --- /dev/null +++ b/init/00-extensions.sql @@ -0,0 +1,22 @@ +-- ============================================================================ +-- EXTENSIONES PostgreSQL - ERP Retail/POS +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- Prerequisito: ERP-Core debe estar instalado +-- ============================================================================ + +-- Verificar que ERP-Core esté instalado +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'auth') THEN + RAISE EXCEPTION 'ERP-Core no instalado. Ejecutar primero DDL de erp-core.'; + END IF; +END $$; + +-- Extensión para búsqueda de texto (productos, códigos) +CREATE EXTENSION IF NOT EXISTS pg_trgm; + +-- ============================================================================ +-- FIN EXTENSIONES +-- ============================================================================ diff --git a/init/01-create-schemas.sql b/init/01-create-schemas.sql new file mode 100644 index 0000000..56518b4 --- /dev/null +++ b/init/01-create-schemas.sql @@ -0,0 +1,15 @@ +-- ============================================================================ +-- SCHEMAS - ERP Retail/POS +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- ============================================================================ + +-- Schema principal para operaciones de punto de venta +CREATE SCHEMA IF NOT EXISTS retail; + +COMMENT ON SCHEMA retail IS 'Schema para operaciones de punto de venta y retail'; + +-- ============================================================================ +-- FIN SCHEMAS +-- ============================================================================ diff --git a/init/02-rls-functions.sql b/init/02-rls-functions.sql new file mode 100644 index 0000000..e3085d4 --- /dev/null +++ b/init/02-rls-functions.sql @@ -0,0 +1,30 @@ +-- ============================================================================ +-- FUNCIONES RLS - ERP Retail/POS +-- ============================================================================ +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- Nota: Usa las funciones de contexto de ERP-Core (auth schema) +-- ============================================================================ + +-- Las funciones principales están en ERP-Core: +-- auth.get_current_tenant_id() +-- auth.get_current_user_id() +-- auth.get_current_company_id() + +-- Función para obtener sucursal actual del usuario (para POS) +CREATE OR REPLACE FUNCTION retail.get_current_branch_id() +RETURNS UUID AS $$ +BEGIN + RETURN current_setting('app.current_branch_id', true)::UUID; +EXCEPTION + WHEN OTHERS THEN + RETURN NULL; +END; +$$ LANGUAGE plpgsql STABLE; + +COMMENT ON FUNCTION retail.get_current_branch_id IS +'Obtiene el ID de la sucursal actual para operaciones POS'; + +-- ============================================================================ +-- FIN FUNCIONES RLS +-- ============================================================================ diff --git a/init/03-retail-tables.sql b/init/03-retail-tables.sql new file mode 100644 index 0000000..d2f8635 --- /dev/null +++ b/init/03-retail-tables.sql @@ -0,0 +1,723 @@ +-- ============================================================================ +-- TABLAS RETAIL/POS - ERP Retail +-- ============================================================================ +-- Módulos: RT-001 (POS), RT-002 (Inventario), RT-003 (Productos), RT-004 (Clientes) +-- Versión: 1.0.0 +-- Fecha: 2025-12-09 +-- ============================================================================ +-- PREREQUISITOS: +-- 1. ERP-Core instalado (auth, core, inventory, sales, financial) +-- 2. Schema retail creado +-- ============================================================================ + +-- ============================================================================ +-- TYPES (ENUMs) +-- ============================================================================ + +DO $$ BEGIN + CREATE TYPE retail.pos_session_status AS ENUM ( + 'opening', 'open', 'closing', 'closed' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE retail.pos_order_status AS ENUM ( + 'draft', 'paid', 'done', 'cancelled', 'refunded' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE retail.payment_method AS ENUM ( + 'cash', 'card', 'transfer', 'credit', 'mixed' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE retail.cash_movement_type AS ENUM ( + 'in', 'out' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE retail.transfer_status AS ENUM ( + 'draft', 'pending', 'in_transit', 'received', 'cancelled' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +DO $$ BEGIN + CREATE TYPE retail.promotion_type AS ENUM ( + 'percentage', 'fixed_amount', 'buy_x_get_y', 'bundle' + ); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================ +-- SUCURSALES Y CONFIGURACIÓN +-- ============================================================================ + +-- Tabla: branches (Sucursales) +CREATE TABLE IF NOT EXISTS retail.branches ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + company_id UUID REFERENCES auth.companies(id), + + -- Identificación + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + + -- Ubicación + address VARCHAR(255), + city VARCHAR(100), + state VARCHAR(100), + zip_code VARCHAR(10), + country VARCHAR(100) DEFAULT 'México', + latitude DECIMAL(10,8), + longitude DECIMAL(11,8), + + -- Contacto + phone VARCHAR(20), + email VARCHAR(255), + manager_id UUID REFERENCES auth.users(id), + + -- Configuración + warehouse_id UUID, -- FK a inventory.warehouses (ERP Core) + default_pricelist_id UUID, + timezone VARCHAR(50) DEFAULT 'America/Mexico_City', + + -- Control + is_active BOOLEAN NOT NULL DEFAULT TRUE, + opening_date DATE, + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + deleted_at TIMESTAMPTZ, + deleted_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_branches_code UNIQUE (tenant_id, code) +); + +-- Tabla: cash_registers (Cajas registradoras) +CREATE TABLE IF NOT EXISTS retail.cash_registers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + branch_id UUID NOT NULL REFERENCES retail.branches(id), + + -- Identificación + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + + -- Configuración + is_active BOOLEAN NOT NULL DEFAULT TRUE, + default_payment_method retail.payment_method DEFAULT 'cash', + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_cash_registers_code UNIQUE (tenant_id, branch_id, code) +); + +-- ============================================================================ +-- PUNTO DE VENTA (RT-001) +-- ============================================================================ + +-- Tabla: pos_sessions (Sesiones de POS) +CREATE TABLE IF NOT EXISTS retail.pos_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + branch_id UUID NOT NULL REFERENCES retail.branches(id), + cash_register_id UUID NOT NULL REFERENCES retail.cash_registers(id), + + -- Usuario + user_id UUID NOT NULL REFERENCES auth.users(id), + + -- Estado + status retail.pos_session_status NOT NULL DEFAULT 'opening', + + -- Apertura + opening_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), + opening_balance DECIMAL(14,2) NOT NULL DEFAULT 0, + + -- Cierre + closing_date TIMESTAMPTZ, + closing_balance DECIMAL(14,2), + closing_notes TEXT, + + -- Totales calculados + total_sales DECIMAL(14,2) DEFAULT 0, + total_refunds DECIMAL(14,2) DEFAULT 0, + total_cash_in DECIMAL(14,2) DEFAULT 0, + total_cash_out DECIMAL(14,2) DEFAULT 0, + total_card DECIMAL(14,2) DEFAULT 0, + total_transfer DECIMAL(14,2) DEFAULT 0, + + -- Diferencia + expected_balance DECIMAL(14,2), + difference DECIMAL(14,2), + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id) +); + +-- Tabla: pos_orders (Órdenes/Ventas de POS) +CREATE TABLE IF NOT EXISTS retail.pos_orders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + session_id UUID NOT NULL REFERENCES retail.pos_sessions(id), + branch_id UUID NOT NULL REFERENCES retail.branches(id), + + -- Número de ticket + order_number VARCHAR(30) NOT NULL, + order_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Cliente (opcional) + customer_id UUID, -- FK a core.partners (ERP Core) + customer_name VARCHAR(200), + + -- Estado + status retail.pos_order_status NOT NULL DEFAULT 'draft', + + -- Totales + subtotal DECIMAL(14,2) NOT NULL DEFAULT 0, + discount_amount DECIMAL(14,2) DEFAULT 0, + tax_amount DECIMAL(14,2) DEFAULT 0, + total DECIMAL(14,2) NOT NULL DEFAULT 0, + + -- Pago + payment_method retail.payment_method, + amount_paid DECIMAL(14,2) DEFAULT 0, + change_amount DECIMAL(14,2) DEFAULT 0, + + -- Facturación + requires_invoice BOOLEAN DEFAULT FALSE, + invoice_id UUID, -- FK a financial.invoices (ERP Core) + + -- Notas + notes TEXT, + + -- Auditoría + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_pos_orders_number UNIQUE (tenant_id, order_number) +); + +-- Tabla: pos_order_lines (Líneas de venta) +CREATE TABLE IF NOT EXISTS retail.pos_order_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + order_id UUID NOT NULL REFERENCES retail.pos_orders(id) ON DELETE CASCADE, + + -- Producto + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + product_name VARCHAR(255) NOT NULL, + barcode VARCHAR(50), + + -- Cantidades + quantity DECIMAL(12,4) NOT NULL, + unit_price DECIMAL(12,4) NOT NULL, + + -- Descuentos + discount_percent DECIMAL(5,2) DEFAULT 0, + discount_amount DECIMAL(12,2) DEFAULT 0, + + -- Totales + subtotal DECIMAL(14,2) GENERATED ALWAYS AS (quantity * unit_price) STORED, + tax_amount DECIMAL(12,2) DEFAULT 0, + total DECIMAL(14,2) NOT NULL, + + -- Orden + sequence INTEGER DEFAULT 1, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: pos_payments (Pagos de orden - para pagos mixtos) +CREATE TABLE IF NOT EXISTS retail.pos_payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + order_id UUID NOT NULL REFERENCES retail.pos_orders(id) ON DELETE CASCADE, + + payment_method retail.payment_method NOT NULL, + amount DECIMAL(14,2) NOT NULL, + + -- Referencia (para tarjeta/transferencia) + reference VARCHAR(100), + card_last_four VARCHAR(4), + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- Tabla: cash_movements (Movimientos de efectivo) +CREATE TABLE IF NOT EXISTS retail.cash_movements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + session_id UUID NOT NULL REFERENCES retail.pos_sessions(id), + + -- Tipo y monto + movement_type retail.cash_movement_type NOT NULL, + amount DECIMAL(14,2) NOT NULL, + + -- Razón + reason VARCHAR(255) NOT NULL, + notes TEXT, + + -- Autorización + authorized_by UUID REFERENCES auth.users(id), + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- INVENTARIO MULTI-SUCURSAL (RT-002) +-- ============================================================================ + +-- Tabla: branch_stock (Stock por sucursal) +CREATE TABLE IF NOT EXISTS retail.branch_stock ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + branch_id UUID NOT NULL REFERENCES retail.branches(id), + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + + -- Cantidades + quantity_on_hand DECIMAL(12,4) NOT NULL DEFAULT 0, + quantity_reserved DECIMAL(12,4) DEFAULT 0, + quantity_available DECIMAL(12,4) GENERATED ALWAYS AS (quantity_on_hand - COALESCE(quantity_reserved, 0)) STORED, + + -- Límites + reorder_point DECIMAL(12,4), + max_stock DECIMAL(12,4), + + -- Control + last_count_date DATE, + last_count_qty DECIMAL(12,4), + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ, + + CONSTRAINT uq_branch_stock UNIQUE (branch_id, product_id) +); + +-- Tabla: stock_transfers (Transferencias entre sucursales) +CREATE TABLE IF NOT EXISTS retail.stock_transfers ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + -- Número + transfer_number VARCHAR(30) NOT NULL, + + -- Origen y destino + source_branch_id UUID NOT NULL REFERENCES retail.branches(id), + destination_branch_id UUID NOT NULL REFERENCES retail.branches(id), + + -- Estado + status retail.transfer_status NOT NULL DEFAULT 'draft', + + -- Fechas + request_date TIMESTAMPTZ NOT NULL DEFAULT NOW(), + ship_date TIMESTAMPTZ, + receive_date TIMESTAMPTZ, + + -- Responsables + requested_by UUID NOT NULL REFERENCES auth.users(id), + shipped_by UUID REFERENCES auth.users(id), + received_by UUID REFERENCES auth.users(id), + + -- Notas + notes TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_stock_transfers_number UNIQUE (tenant_id, transfer_number), + CONSTRAINT chk_different_branches CHECK (source_branch_id != destination_branch_id) +); + +-- Tabla: stock_transfer_lines (Líneas de transferencia) +CREATE TABLE IF NOT EXISTS retail.stock_transfer_lines ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + transfer_id UUID NOT NULL REFERENCES retail.stock_transfers(id) ON DELETE CASCADE, + + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + quantity_requested DECIMAL(12,4) NOT NULL, + quantity_shipped DECIMAL(12,4), + quantity_received DECIMAL(12,4), + + notes TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- PRODUCTOS RETAIL (RT-003) +-- ============================================================================ + +-- Tabla: product_barcodes (Códigos de barras múltiples) +CREATE TABLE IF NOT EXISTS retail.product_barcodes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + + barcode VARCHAR(50) NOT NULL, + barcode_type VARCHAR(20) DEFAULT 'EAN13', -- EAN13, EAN8, UPC, CODE128, etc. + is_primary BOOLEAN DEFAULT FALSE, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_product_barcodes UNIQUE (tenant_id, barcode) +); + +-- Tabla: promotions (Promociones) +CREATE TABLE IF NOT EXISTS retail.promotions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + code VARCHAR(30) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + + -- Tipo de promoción + promotion_type retail.promotion_type NOT NULL, + discount_value DECIMAL(10,2), -- Porcentaje o monto fijo + + -- Vigencia + start_date TIMESTAMPTZ NOT NULL, + end_date TIMESTAMPTZ NOT NULL, + + -- Aplicación + applies_to_all BOOLEAN DEFAULT FALSE, + min_quantity DECIMAL(12,4), + min_amount DECIMAL(14,2), + + -- Sucursales (NULL = todas) + branch_ids UUID[], + + -- Control + is_active BOOLEAN NOT NULL DEFAULT TRUE, + max_uses INTEGER, + current_uses INTEGER DEFAULT 0, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_promotions_code UNIQUE (tenant_id, code), + CONSTRAINT chk_promotion_dates CHECK (end_date > start_date) +); + +-- Tabla: promotion_products (Productos en promoción) +CREATE TABLE IF NOT EXISTS retail.promotion_products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + promotion_id UUID NOT NULL REFERENCES retail.promotions(id) ON DELETE CASCADE, + product_id UUID NOT NULL, -- FK a inventory.products (ERP Core) + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- ============================================================================ +-- CLIENTES Y FIDELIZACIÓN (RT-004) +-- ============================================================================ + +-- Tabla: loyalty_programs (Programas de fidelización) +CREATE TABLE IF NOT EXISTS retail.loyalty_programs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + + code VARCHAR(20) NOT NULL, + name VARCHAR(100) NOT NULL, + description TEXT, + + -- Configuración de puntos + points_per_currency DECIMAL(10,4) DEFAULT 1, -- Puntos por peso gastado + currency_per_point DECIMAL(10,4) DEFAULT 0.01, -- Valor del punto en pesos + min_points_redeem INTEGER DEFAULT 100, + + -- Vigencia + points_expiry_days INTEGER, -- NULL = no expiran + + is_active BOOLEAN NOT NULL DEFAULT TRUE, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_loyalty_programs_code UNIQUE (tenant_id, code) +); + +-- Tabla: loyalty_cards (Tarjetas de fidelización) +CREATE TABLE IF NOT EXISTS retail.loyalty_cards ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + program_id UUID NOT NULL REFERENCES retail.loyalty_programs(id), + customer_id UUID NOT NULL, -- FK a core.partners (ERP Core) + + card_number VARCHAR(30) NOT NULL, + issue_date DATE NOT NULL DEFAULT CURRENT_DATE, + + -- Balance + points_balance INTEGER NOT NULL DEFAULT 0, + points_earned INTEGER NOT NULL DEFAULT 0, + points_redeemed INTEGER NOT NULL DEFAULT 0, + points_expired INTEGER NOT NULL DEFAULT 0, + + is_active BOOLEAN NOT NULL DEFAULT TRUE, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id), + updated_at TIMESTAMPTZ, + updated_by UUID REFERENCES auth.users(id), + + CONSTRAINT uq_loyalty_cards_number UNIQUE (tenant_id, card_number) +); + +-- Tabla: loyalty_transactions (Transacciones de puntos) +CREATE TABLE IF NOT EXISTS retail.loyalty_transactions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, + card_id UUID NOT NULL REFERENCES retail.loyalty_cards(id), + + -- Tipo + transaction_type VARCHAR(20) NOT NULL, -- earn, redeem, expire, adjust + points INTEGER NOT NULL, + + -- Referencia + order_id UUID REFERENCES retail.pos_orders(id), + description TEXT, + + -- Balance después de la transacción + balance_after INTEGER NOT NULL, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_by UUID REFERENCES auth.users(id) +); + +-- ============================================================================ +-- ÍNDICES +-- ============================================================================ + +-- Branches +CREATE INDEX IF NOT EXISTS idx_branches_tenant ON retail.branches(tenant_id); +CREATE INDEX IF NOT EXISTS idx_branches_company ON retail.branches(company_id); + +-- Cash registers +CREATE INDEX IF NOT EXISTS idx_cash_registers_tenant ON retail.cash_registers(tenant_id); +CREATE INDEX IF NOT EXISTS idx_cash_registers_branch ON retail.cash_registers(branch_id); + +-- POS sessions +CREATE INDEX IF NOT EXISTS idx_pos_sessions_tenant ON retail.pos_sessions(tenant_id); +CREATE INDEX IF NOT EXISTS idx_pos_sessions_branch ON retail.pos_sessions(branch_id); +CREATE INDEX IF NOT EXISTS idx_pos_sessions_user ON retail.pos_sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_pos_sessions_status ON retail.pos_sessions(status); +CREATE INDEX IF NOT EXISTS idx_pos_sessions_date ON retail.pos_sessions(opening_date); + +-- POS orders +CREATE INDEX IF NOT EXISTS idx_pos_orders_tenant ON retail.pos_orders(tenant_id); +CREATE INDEX IF NOT EXISTS idx_pos_orders_session ON retail.pos_orders(session_id); +CREATE INDEX IF NOT EXISTS idx_pos_orders_branch ON retail.pos_orders(branch_id); +CREATE INDEX IF NOT EXISTS idx_pos_orders_customer ON retail.pos_orders(customer_id); +CREATE INDEX IF NOT EXISTS idx_pos_orders_date ON retail.pos_orders(order_date); +CREATE INDEX IF NOT EXISTS idx_pos_orders_status ON retail.pos_orders(status); + +-- POS order lines +CREATE INDEX IF NOT EXISTS idx_pos_order_lines_tenant ON retail.pos_order_lines(tenant_id); +CREATE INDEX IF NOT EXISTS idx_pos_order_lines_order ON retail.pos_order_lines(order_id); +CREATE INDEX IF NOT EXISTS idx_pos_order_lines_product ON retail.pos_order_lines(product_id); + +-- POS payments +CREATE INDEX IF NOT EXISTS idx_pos_payments_tenant ON retail.pos_payments(tenant_id); +CREATE INDEX IF NOT EXISTS idx_pos_payments_order ON retail.pos_payments(order_id); + +-- Cash movements +CREATE INDEX IF NOT EXISTS idx_cash_movements_tenant ON retail.cash_movements(tenant_id); +CREATE INDEX IF NOT EXISTS idx_cash_movements_session ON retail.cash_movements(session_id); + +-- Branch stock +CREATE INDEX IF NOT EXISTS idx_branch_stock_tenant ON retail.branch_stock(tenant_id); +CREATE INDEX IF NOT EXISTS idx_branch_stock_branch ON retail.branch_stock(branch_id); +CREATE INDEX IF NOT EXISTS idx_branch_stock_product ON retail.branch_stock(product_id); + +-- Stock transfers +CREATE INDEX IF NOT EXISTS idx_stock_transfers_tenant ON retail.stock_transfers(tenant_id); +CREATE INDEX IF NOT EXISTS idx_stock_transfers_source ON retail.stock_transfers(source_branch_id); +CREATE INDEX IF NOT EXISTS idx_stock_transfers_dest ON retail.stock_transfers(destination_branch_id); +CREATE INDEX IF NOT EXISTS idx_stock_transfers_status ON retail.stock_transfers(status); + +-- Product barcodes +CREATE INDEX IF NOT EXISTS idx_product_barcodes_tenant ON retail.product_barcodes(tenant_id); +CREATE INDEX IF NOT EXISTS idx_product_barcodes_barcode ON retail.product_barcodes(barcode); +CREATE INDEX IF NOT EXISTS idx_product_barcodes_product ON retail.product_barcodes(product_id); + +-- Promotions +CREATE INDEX IF NOT EXISTS idx_promotions_tenant ON retail.promotions(tenant_id); +CREATE INDEX IF NOT EXISTS idx_promotions_dates ON retail.promotions(start_date, end_date); +CREATE INDEX IF NOT EXISTS idx_promotions_active ON retail.promotions(is_active); + +-- Loyalty +CREATE INDEX IF NOT EXISTS idx_loyalty_cards_tenant ON retail.loyalty_cards(tenant_id); +CREATE INDEX IF NOT EXISTS idx_loyalty_cards_customer ON retail.loyalty_cards(customer_id); +CREATE INDEX IF NOT EXISTS idx_loyalty_transactions_tenant ON retail.loyalty_transactions(tenant_id); +CREATE INDEX IF NOT EXISTS idx_loyalty_transactions_card ON retail.loyalty_transactions(card_id); + +-- ============================================================================ +-- ROW LEVEL SECURITY +-- ============================================================================ + +ALTER TABLE retail.branches ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.cash_registers ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.pos_sessions ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.pos_orders ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.pos_order_lines ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.pos_payments ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.cash_movements ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.branch_stock ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.stock_transfers ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.stock_transfer_lines ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.product_barcodes ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.promotions ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.promotion_products ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.loyalty_programs ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.loyalty_cards ENABLE ROW LEVEL SECURITY; +ALTER TABLE retail.loyalty_transactions ENABLE ROW LEVEL SECURITY; + +-- Políticas de aislamiento por tenant +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_branches ON retail.branches; + CREATE POLICY tenant_isolation_branches ON retail.branches + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_cash_registers ON retail.cash_registers; + CREATE POLICY tenant_isolation_cash_registers ON retail.cash_registers + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_pos_sessions ON retail.pos_sessions; + CREATE POLICY tenant_isolation_pos_sessions ON retail.pos_sessions + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_pos_orders ON retail.pos_orders; + CREATE POLICY tenant_isolation_pos_orders ON retail.pos_orders + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_pos_order_lines ON retail.pos_order_lines; + CREATE POLICY tenant_isolation_pos_order_lines ON retail.pos_order_lines + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_pos_payments ON retail.pos_payments; + CREATE POLICY tenant_isolation_pos_payments ON retail.pos_payments + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_cash_movements ON retail.cash_movements; + CREATE POLICY tenant_isolation_cash_movements ON retail.cash_movements + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_branch_stock ON retail.branch_stock; + CREATE POLICY tenant_isolation_branch_stock ON retail.branch_stock + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_stock_transfers ON retail.stock_transfers; + CREATE POLICY tenant_isolation_stock_transfers ON retail.stock_transfers + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_stock_transfer_lines ON retail.stock_transfer_lines; + CREATE POLICY tenant_isolation_stock_transfer_lines ON retail.stock_transfer_lines + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_product_barcodes ON retail.product_barcodes; + CREATE POLICY tenant_isolation_product_barcodes ON retail.product_barcodes + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_promotions ON retail.promotions; + CREATE POLICY tenant_isolation_promotions ON retail.promotions + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_promotion_products ON retail.promotion_products; + CREATE POLICY tenant_isolation_promotion_products ON retail.promotion_products + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_loyalty_programs ON retail.loyalty_programs; + CREATE POLICY tenant_isolation_loyalty_programs ON retail.loyalty_programs + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_loyalty_cards ON retail.loyalty_cards; + CREATE POLICY tenant_isolation_loyalty_cards ON retail.loyalty_cards + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +DO $$ BEGIN + DROP POLICY IF EXISTS tenant_isolation_loyalty_transactions ON retail.loyalty_transactions; + CREATE POLICY tenant_isolation_loyalty_transactions ON retail.loyalty_transactions + FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); +EXCEPTION WHEN undefined_object THEN NULL; END $$; + +-- ============================================================================ +-- COMENTARIOS +-- ============================================================================ + +COMMENT ON TABLE retail.branches IS 'Sucursales de la empresa'; +COMMENT ON TABLE retail.cash_registers IS 'Cajas registradoras por sucursal'; +COMMENT ON TABLE retail.pos_sessions IS 'Sesiones de punto de venta'; +COMMENT ON TABLE retail.pos_orders IS 'Órdenes/Ventas de punto de venta'; +COMMENT ON TABLE retail.pos_order_lines IS 'Líneas de venta'; +COMMENT ON TABLE retail.pos_payments IS 'Pagos de orden (para pagos mixtos)'; +COMMENT ON TABLE retail.cash_movements IS 'Entradas/salidas de efectivo'; +COMMENT ON TABLE retail.branch_stock IS 'Stock por sucursal'; +COMMENT ON TABLE retail.stock_transfers IS 'Transferencias entre sucursales'; +COMMENT ON TABLE retail.stock_transfer_lines IS 'Líneas de transferencia'; +COMMENT ON TABLE retail.product_barcodes IS 'Códigos de barras múltiples por producto'; +COMMENT ON TABLE retail.promotions IS 'Promociones y descuentos'; +COMMENT ON TABLE retail.promotion_products IS 'Productos en promoción'; +COMMENT ON TABLE retail.loyalty_programs IS 'Programas de fidelización'; +COMMENT ON TABLE retail.loyalty_cards IS 'Tarjetas de fidelización'; +COMMENT ON TABLE retail.loyalty_transactions IS 'Transacciones de puntos'; + +-- ============================================================================ +-- FIN TABLAS RETAIL +-- Total: 16 tablas, 6 ENUMs +-- ============================================================================