erp-core-database-v2/ddl/03-core-branches.sql
rckrdmrd 5043a640e4 refactor: Restructure DDL with numbered schema files
- Replace old DDL structure with new numbered files (01-24)
- Update migrations and seeds for new schema
- Clean up deprecated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 00:40:32 -06:00

367 lines
13 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 03-core-branches.sql
-- DESCRIPCION: Sucursales, jerarquia y asignaciones de usuarios
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- =============================================================
-- =====================
-- EXTENSIONES REQUERIDAS
-- =====================
CREATE EXTENSION IF NOT EXISTS cube;
CREATE EXTENSION IF NOT EXISTS earthdistance;
-- =====================
-- SCHEMA: core (si no existe)
-- =====================
CREATE SCHEMA IF NOT EXISTS core;
-- =====================
-- TABLA: branches
-- Sucursales/ubicaciones del negocio
-- =====================
CREATE TABLE IF NOT EXISTS core.branches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
parent_id UUID REFERENCES core.branches(id) ON DELETE SET NULL,
-- Identificacion
code VARCHAR(20) NOT NULL,
name VARCHAR(100) NOT NULL,
short_name VARCHAR(50),
-- Tipo
branch_type VARCHAR(30) NOT NULL DEFAULT 'store', -- headquarters, regional, store, warehouse, office, factory
-- Contacto
phone VARCHAR(20),
email VARCHAR(255),
manager_id UUID REFERENCES auth.users(id),
-- Direccion
address_line1 VARCHAR(200),
address_line2 VARCHAR(200),
city VARCHAR(100),
state VARCHAR(100),
postal_code VARCHAR(20),
country VARCHAR(3) DEFAULT 'MEX',
-- Geolocalizacion
latitude DECIMAL(10, 8),
longitude DECIMAL(11, 8),
geofence_radius INTEGER DEFAULT 100, -- Radio en metros para validacion de ubicacion
geofence_enabled BOOLEAN DEFAULT TRUE,
-- Configuracion
timezone VARCHAR(50) DEFAULT 'America/Mexico_City',
currency VARCHAR(3) DEFAULT 'MXN',
is_active BOOLEAN DEFAULT TRUE,
is_main BOOLEAN DEFAULT FALSE, -- Sucursal principal/matriz
-- Horarios de operacion
operating_hours JSONB DEFAULT '{}',
-- Ejemplo: {"monday": {"open": "09:00", "close": "18:00"}, ...}
-- Configuraciones especificas
settings JSONB DEFAULT '{}',
-- Ejemplo: {"allow_pos": true, "allow_warehouse": true, ...}
-- Jerarquia (path materializado para consultas eficientes)
hierarchy_path TEXT, -- Ejemplo: /root/regional-norte/sucursal-01
hierarchy_level INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_by UUID REFERENCES auth.users(id),
deleted_at TIMESTAMPTZ,
UNIQUE(tenant_id, code)
);
-- Indices para branches
CREATE INDEX IF NOT EXISTS idx_branches_tenant ON core.branches(tenant_id);
CREATE INDEX IF NOT EXISTS idx_branches_parent ON core.branches(parent_id);
CREATE INDEX IF NOT EXISTS idx_branches_code ON core.branches(code);
CREATE INDEX IF NOT EXISTS idx_branches_type ON core.branches(branch_type);
CREATE INDEX IF NOT EXISTS idx_branches_active ON core.branches(is_active) WHERE is_active = TRUE;
CREATE INDEX IF NOT EXISTS idx_branches_hierarchy ON core.branches(hierarchy_path);
CREATE INDEX IF NOT EXISTS idx_branches_location ON core.branches USING gist (
ll_to_earth(latitude, longitude)
) WHERE latitude IS NOT NULL AND longitude IS NOT NULL;
-- =====================
-- TABLA: user_branch_assignments
-- Asignacion de usuarios a sucursales
-- =====================
CREATE TABLE IF NOT EXISTS core.user_branch_assignments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
branch_id UUID NOT NULL REFERENCES core.branches(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Tipo de asignacion
assignment_type VARCHAR(30) NOT NULL DEFAULT 'primary', -- primary, secondary, temporary, floating
-- Rol en la sucursal
branch_role VARCHAR(50), -- manager, supervisor, staff
-- Permisos especificos
permissions JSONB DEFAULT '[]',
-- Vigencia (para asignaciones temporales)
valid_from TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
valid_until TIMESTAMPTZ,
-- Estado
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
created_by UUID REFERENCES auth.users(id),
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(user_id, branch_id, assignment_type)
);
-- Indices para user_branch_assignments
CREATE INDEX IF NOT EXISTS idx_user_branch_user ON core.user_branch_assignments(user_id);
CREATE INDEX IF NOT EXISTS idx_user_branch_branch ON core.user_branch_assignments(branch_id);
CREATE INDEX IF NOT EXISTS idx_user_branch_tenant ON core.user_branch_assignments(tenant_id);
CREATE INDEX IF NOT EXISTS idx_user_branch_active ON core.user_branch_assignments(is_active) WHERE is_active = TRUE;
-- =====================
-- TABLA: branch_schedules
-- Horarios de trabajo por sucursal
-- =====================
CREATE TABLE IF NOT EXISTS core.branch_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
branch_id UUID NOT NULL REFERENCES core.branches(id) ON DELETE CASCADE,
-- Identificacion
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tipo
schedule_type VARCHAR(30) NOT NULL DEFAULT 'regular', -- regular, holiday, special
-- Dia de la semana (0=domingo, 1=lunes, ..., 6=sabado) o fecha especifica
day_of_week INTEGER, -- NULL para fechas especificas
specific_date DATE, -- Para dias festivos o especiales
-- Horarios
open_time TIME NOT NULL,
close_time TIME NOT NULL,
-- Turnos (si aplica)
shifts JSONB DEFAULT '[]',
-- Ejemplo: [{"name": "Matutino", "start": "08:00", "end": "14:00"}, ...]
-- Estado
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para branch_schedules
CREATE INDEX IF NOT EXISTS idx_branch_schedules_branch ON core.branch_schedules(branch_id);
CREATE INDEX IF NOT EXISTS idx_branch_schedules_day ON core.branch_schedules(day_of_week);
CREATE INDEX IF NOT EXISTS idx_branch_schedules_date ON core.branch_schedules(specific_date);
-- =====================
-- TABLA: branch_inventory_settings
-- Configuracion de inventario por sucursal
-- =====================
CREATE TABLE IF NOT EXISTS core.branch_inventory_settings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
branch_id UUID NOT NULL REFERENCES core.branches(id) ON DELETE CASCADE,
-- Almacen asociado
warehouse_id UUID, -- Referencia a inventory.warehouses
-- Configuracion de stock
default_stock_min INTEGER DEFAULT 0,
default_stock_max INTEGER DEFAULT 1000,
auto_reorder_enabled BOOLEAN DEFAULT FALSE,
-- Configuracion de precios
price_list_id UUID, -- Referencia a sales.price_lists
allow_price_override BOOLEAN DEFAULT FALSE,
max_discount_percent DECIMAL(5,2) DEFAULT 0,
-- Configuracion de impuestos
tax_config JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(branch_id)
);
-- Indices para branch_inventory_settings
CREATE INDEX IF NOT EXISTS idx_branch_inventory_branch ON core.branch_inventory_settings(branch_id);
-- =====================
-- TABLA: branch_payment_terminals
-- Terminales de pago asociadas a sucursal
-- =====================
CREATE TABLE IF NOT EXISTS core.branch_payment_terminals (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
branch_id UUID NOT NULL REFERENCES core.branches(id) ON DELETE CASCADE,
-- Terminal
terminal_provider VARCHAR(30) NOT NULL, -- clip, mercadopago, stripe
terminal_id VARCHAR(100) NOT NULL,
terminal_name VARCHAR(100),
-- Credenciales (encriptadas)
credentials JSONB NOT NULL DEFAULT '{}',
-- Configuracion
is_primary BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
-- Limites
daily_limit DECIMAL(12,2),
transaction_limit DECIMAL(12,2),
-- Ultima actividad
last_transaction_at TIMESTAMPTZ,
last_health_check_at TIMESTAMPTZ,
health_status VARCHAR(20) DEFAULT 'unknown', -- healthy, degraded, offline, unknown
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(branch_id, terminal_provider, terminal_id)
);
-- Indices para branch_payment_terminals
CREATE INDEX IF NOT EXISTS idx_branch_terminals_branch ON core.branch_payment_terminals(branch_id);
CREATE INDEX IF NOT EXISTS idx_branch_terminals_provider ON core.branch_payment_terminals(terminal_provider);
CREATE INDEX IF NOT EXISTS idx_branch_terminals_active ON core.branch_payment_terminals(is_active) WHERE is_active = TRUE;
-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE core.branches ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_branches ON core.branches
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE core.user_branch_assignments ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_branch_assignments ON core.user_branch_assignments
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE core.branch_schedules ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_branch_schedules ON core.branch_schedules
USING (branch_id IN (
SELECT id FROM core.branches
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
ALTER TABLE core.branch_inventory_settings ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_branch_inventory ON core.branch_inventory_settings
USING (branch_id IN (
SELECT id FROM core.branches
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
ALTER TABLE core.branch_payment_terminals ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_branch_terminals ON core.branch_payment_terminals
USING (branch_id IN (
SELECT id FROM core.branches
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
-- =====================
-- FUNCIONES
-- =====================
-- Funcion para actualizar hierarchy_path
CREATE OR REPLACE FUNCTION core.update_branch_hierarchy()
RETURNS TRIGGER AS $$
DECLARE
parent_path TEXT;
BEGIN
IF NEW.parent_id IS NULL THEN
NEW.hierarchy_path := '/' || NEW.code;
NEW.hierarchy_level := 0;
ELSE
SELECT hierarchy_path, hierarchy_level + 1
INTO parent_path, NEW.hierarchy_level
FROM core.branches
WHERE id = NEW.parent_id;
NEW.hierarchy_path := parent_path || '/' || NEW.code;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger para actualizar hierarchy_path automaticamente
CREATE TRIGGER trg_update_branch_hierarchy
BEFORE INSERT OR UPDATE OF parent_id, code ON core.branches
FOR EACH ROW
EXECUTE FUNCTION core.update_branch_hierarchy();
-- Funcion para obtener todas las sucursales hijas
CREATE OR REPLACE FUNCTION core.get_branch_children(parent_branch_id UUID)
RETURNS SETOF core.branches AS $$
BEGIN
RETURN QUERY
WITH RECURSIVE branch_tree AS (
SELECT * FROM core.branches WHERE id = parent_branch_id
UNION ALL
SELECT b.* FROM core.branches b
JOIN branch_tree bt ON b.parent_id = bt.id
)
SELECT * FROM branch_tree WHERE id != parent_branch_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para validar si usuario esta en rango de geofence
CREATE OR REPLACE FUNCTION core.is_within_geofence(
branch_id UUID,
user_lat DECIMAL(10, 8),
user_lon DECIMAL(11, 8)
)
RETURNS BOOLEAN AS $$
DECLARE
branch_record RECORD;
distance_meters FLOAT;
BEGIN
SELECT latitude, longitude, geofence_radius, geofence_enabled
INTO branch_record
FROM core.branches
WHERE id = branch_id;
IF NOT branch_record.geofence_enabled THEN
RETURN TRUE;
END IF;
IF branch_record.latitude IS NULL OR branch_record.longitude IS NULL THEN
RETURN TRUE;
END IF;
-- Calcular distancia usando formula Haversine (aproximada)
distance_meters := 6371000 * acos(
cos(radians(user_lat)) * cos(radians(branch_record.latitude)) *
cos(radians(branch_record.longitude) - radians(user_lon)) +
sin(radians(user_lat)) * sin(radians(branch_record.latitude))
);
RETURN distance_meters <= branch_record.geofence_radius;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- COMENTARIOS DE TABLAS
-- =====================
COMMENT ON TABLE core.branches IS 'Sucursales/ubicaciones del negocio con soporte para jerarquia';
COMMENT ON TABLE core.user_branch_assignments IS 'Asignacion de usuarios a sucursales';
COMMENT ON TABLE core.branch_schedules IS 'Horarios de operacion por sucursal';
COMMENT ON TABLE core.branch_inventory_settings IS 'Configuracion de inventario especifica por sucursal';
COMMENT ON TABLE core.branch_payment_terminals IS 'Terminales de pago asociadas a cada sucursal';