Initial commit - erp-retail-database

This commit is contained in:
rckrdmrd 2026-01-04 07:04:02 -06:00
commit f92504b111
6 changed files with 1069 additions and 0 deletions

196
HERENCIA-ERP-CORE.md Normal file
View File

@ -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

83
README.md Normal file
View File

@ -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)

22
init/00-extensions.sql Normal file
View File

@ -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
-- ============================================================================

View File

@ -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
-- ============================================================================

30
init/02-rls-functions.sql Normal file
View File

@ -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
-- ============================================================================

723
init/03-retail-tables.sql Normal file
View File

@ -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
-- ============================================================================