erp-core-database-v2/ddl/04-mobile.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

394 lines
15 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 04-mobile.sql
-- DESCRIPCION: Sesiones moviles, sincronizacion offline, push tokens
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- =============================================================
-- =====================
-- SCHEMA: mobile
-- =====================
CREATE SCHEMA IF NOT EXISTS mobile;
-- =====================
-- TABLA: mobile_sessions
-- Sesiones activas de la aplicacion movil
-- =====================
CREATE TABLE IF NOT EXISTS mobile.mobile_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
device_id UUID NOT NULL REFERENCES auth.devices(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
branch_id UUID REFERENCES core.branches(id),
-- Estado de la sesion
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, paused, expired, terminated
-- Perfil activo
active_profile_id UUID REFERENCES auth.user_profiles(id),
active_profile_code VARCHAR(10),
-- Modo de operacion
is_offline_mode BOOLEAN DEFAULT FALSE,
offline_since TIMESTAMPTZ,
-- Sincronizacion
last_sync_at TIMESTAMPTZ,
pending_sync_count INTEGER DEFAULT 0,
-- Ubicacion
last_latitude DECIMAL(10, 8),
last_longitude DECIMAL(11, 8),
last_location_at TIMESTAMPTZ,
-- Metadata
app_version VARCHAR(20),
platform VARCHAR(20), -- ios, android
os_version VARCHAR(20),
-- Tiempos
started_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
last_activity_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ,
ended_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para mobile_sessions
CREATE INDEX IF NOT EXISTS idx_mobile_sessions_user ON mobile.mobile_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_mobile_sessions_device ON mobile.mobile_sessions(device_id);
CREATE INDEX IF NOT EXISTS idx_mobile_sessions_tenant ON mobile.mobile_sessions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_mobile_sessions_branch ON mobile.mobile_sessions(branch_id);
CREATE INDEX IF NOT EXISTS idx_mobile_sessions_active ON mobile.mobile_sessions(status) WHERE status = 'active';
-- =====================
-- TABLA: offline_sync_queue
-- Cola de operaciones pendientes de sincronizar
-- =====================
CREATE TABLE IF NOT EXISTS mobile.offline_sync_queue (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
device_id UUID NOT NULL REFERENCES auth.devices(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
session_id UUID REFERENCES mobile.mobile_sessions(id),
-- Operacion
entity_type VARCHAR(50) NOT NULL, -- sale, attendance, inventory_count, etc.
entity_id UUID, -- ID local del registro
operation VARCHAR(20) NOT NULL, -- create, update, delete
-- Datos
payload JSONB NOT NULL,
metadata JSONB DEFAULT '{}',
-- Orden y dependencias
sequence_number BIGINT NOT NULL,
depends_on UUID, -- ID de otra operacion que debe procesarse primero
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed, conflict
-- Procesamiento
retry_count INTEGER DEFAULT 0,
max_retries INTEGER DEFAULT 3,
last_error TEXT,
processed_at TIMESTAMPTZ,
-- Conflicto
conflict_data JSONB,
conflict_resolved_at TIMESTAMPTZ,
conflict_resolution VARCHAR(20), -- local_wins, server_wins, merged, manual
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para offline_sync_queue
CREATE INDEX IF NOT EXISTS idx_offline_sync_user ON mobile.offline_sync_queue(user_id);
CREATE INDEX IF NOT EXISTS idx_offline_sync_device ON mobile.offline_sync_queue(device_id);
CREATE INDEX IF NOT EXISTS idx_offline_sync_tenant ON mobile.offline_sync_queue(tenant_id);
CREATE INDEX IF NOT EXISTS idx_offline_sync_status ON mobile.offline_sync_queue(status);
CREATE INDEX IF NOT EXISTS idx_offline_sync_sequence ON mobile.offline_sync_queue(device_id, sequence_number);
CREATE INDEX IF NOT EXISTS idx_offline_sync_pending ON mobile.offline_sync_queue(status, created_at) WHERE status = 'pending';
-- =====================
-- TABLA: sync_conflicts
-- Registro de conflictos de sincronizacion
-- =====================
CREATE TABLE IF NOT EXISTS mobile.sync_conflicts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sync_queue_id UUID NOT NULL REFERENCES mobile.offline_sync_queue(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id),
-- Tipo de conflicto
conflict_type VARCHAR(30) NOT NULL, -- version_mismatch, deleted_on_server, concurrent_edit
-- Datos en conflicto
local_data JSONB NOT NULL,
server_data JSONB NOT NULL,
-- Resolucion
resolution VARCHAR(20), -- local_wins, server_wins, merged, manual
merged_data JSONB,
resolved_by UUID REFERENCES auth.users(id),
resolved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para sync_conflicts
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_queue ON mobile.sync_conflicts(sync_queue_id);
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_user ON mobile.sync_conflicts(user_id);
CREATE INDEX IF NOT EXISTS idx_sync_conflicts_unresolved ON mobile.sync_conflicts(resolved_at) WHERE resolved_at IS NULL;
-- =====================
-- TABLA: push_tokens
-- Tokens de notificaciones push
-- =====================
CREATE TABLE IF NOT EXISTS mobile.push_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
device_id UUID NOT NULL REFERENCES auth.devices(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Token
token TEXT NOT NULL,
platform VARCHAR(20) NOT NULL, -- ios, android
provider VARCHAR(30) NOT NULL DEFAULT 'firebase', -- firebase, apns, fcm
-- Estado
is_active BOOLEAN DEFAULT TRUE,
is_valid BOOLEAN DEFAULT TRUE,
invalid_reason TEXT,
-- Topics suscritos
subscribed_topics TEXT[] DEFAULT '{}',
-- Ultima actividad
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(device_id, platform)
);
-- Indices para push_tokens
CREATE INDEX IF NOT EXISTS idx_push_tokens_user ON mobile.push_tokens(user_id);
CREATE INDEX IF NOT EXISTS idx_push_tokens_device ON mobile.push_tokens(device_id);
CREATE INDEX IF NOT EXISTS idx_push_tokens_tenant ON mobile.push_tokens(tenant_id);
CREATE INDEX IF NOT EXISTS idx_push_tokens_active ON mobile.push_tokens(is_active, is_valid) WHERE is_active = TRUE AND is_valid = TRUE;
-- =====================
-- TABLA: push_notifications_log
-- Log de notificaciones enviadas
-- =====================
CREATE TABLE IF NOT EXISTS mobile.push_notifications_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Destino
user_id UUID REFERENCES auth.users(id),
device_id UUID REFERENCES auth.devices(id),
push_token_id UUID REFERENCES mobile.push_tokens(id),
-- Notificacion
title VARCHAR(200) NOT NULL,
body TEXT,
data JSONB DEFAULT '{}',
category VARCHAR(50), -- attendance, sale, inventory, alert, system
-- Envio
sent_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
provider_message_id VARCHAR(255),
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'sent', -- sent, delivered, failed, read
delivered_at TIMESTAMPTZ,
read_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para push_notifications_log
CREATE INDEX IF NOT EXISTS idx_push_log_tenant ON mobile.push_notifications_log(tenant_id);
CREATE INDEX IF NOT EXISTS idx_push_log_user ON mobile.push_notifications_log(user_id);
CREATE INDEX IF NOT EXISTS idx_push_log_device ON mobile.push_notifications_log(device_id);
CREATE INDEX IF NOT EXISTS idx_push_log_created ON mobile.push_notifications_log(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_push_log_category ON mobile.push_notifications_log(category);
-- =====================
-- TABLA: payment_transactions
-- Transacciones de pago desde terminales moviles
-- =====================
CREATE TABLE IF NOT EXISTS mobile.payment_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
branch_id UUID REFERENCES core.branches(id),
user_id UUID NOT NULL REFERENCES auth.users(id),
device_id UUID REFERENCES auth.devices(id),
-- Referencia al documento origen
source_type VARCHAR(30) NOT NULL, -- sale, invoice, subscription
source_id UUID NOT NULL,
-- Terminal de pago
terminal_provider VARCHAR(30) NOT NULL, -- clip, mercadopago, stripe
terminal_id VARCHAR(100),
-- Transaccion
external_transaction_id VARCHAR(255),
amount DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'MXN',
tip_amount DECIMAL(12,2) DEFAULT 0,
total_amount DECIMAL(12,2) NOT NULL,
-- Metodo de pago
payment_method VARCHAR(30) NOT NULL, -- card, contactless, qr, link
card_brand VARCHAR(20), -- visa, mastercard, amex
card_last_four VARCHAR(4),
card_type VARCHAR(20), -- credit, debit
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending, processing, completed, failed, refunded, cancelled
failure_reason TEXT,
-- Tiempos
initiated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ,
-- Metadata del proveedor
provider_response JSONB DEFAULT '{}',
-- Recibo
receipt_url TEXT,
receipt_sent BOOLEAN DEFAULT FALSE,
receipt_sent_to VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para payment_transactions
CREATE INDEX IF NOT EXISTS idx_payment_tx_tenant ON mobile.payment_transactions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_branch ON mobile.payment_transactions(branch_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_user ON mobile.payment_transactions(user_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_source ON mobile.payment_transactions(source_type, source_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_external ON mobile.payment_transactions(external_transaction_id);
CREATE INDEX IF NOT EXISTS idx_payment_tx_status ON mobile.payment_transactions(status);
CREATE INDEX IF NOT EXISTS idx_payment_tx_created ON mobile.payment_transactions(created_at DESC);
-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE mobile.mobile_sessions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_mobile_sessions ON mobile.mobile_sessions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE mobile.offline_sync_queue ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_sync_queue ON mobile.offline_sync_queue
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE mobile.sync_conflicts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_sync_conflicts ON mobile.sync_conflicts
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE mobile.push_tokens ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_push_tokens ON mobile.push_tokens
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE mobile.push_notifications_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_push_log ON mobile.push_notifications_log
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE mobile.payment_transactions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_payment_tx ON mobile.payment_transactions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- =====================
-- FUNCIONES
-- =====================
-- Funcion para obtener siguiente numero de secuencia
CREATE OR REPLACE FUNCTION mobile.get_next_sync_sequence(p_device_id UUID)
RETURNS BIGINT AS $$
DECLARE
next_seq BIGINT;
BEGIN
SELECT COALESCE(MAX(sequence_number), 0) + 1
INTO next_seq
FROM mobile.offline_sync_queue
WHERE device_id = p_device_id;
RETURN next_seq;
END;
$$ LANGUAGE plpgsql;
-- Funcion para procesar cola de sincronizacion
CREATE OR REPLACE FUNCTION mobile.process_sync_queue(p_device_id UUID, p_batch_size INTEGER DEFAULT 100)
RETURNS TABLE (
processed_count INTEGER,
failed_count INTEGER,
conflict_count INTEGER
) AS $$
DECLARE
v_processed INTEGER := 0;
v_failed INTEGER := 0;
v_conflicts INTEGER := 0;
BEGIN
-- Marcar items como processing (usando subquery para ORDER BY y LIMIT en PostgreSQL)
UPDATE mobile.offline_sync_queue
SET status = 'processing', updated_at = CURRENT_TIMESTAMP
WHERE id IN (
SELECT osq.id FROM mobile.offline_sync_queue osq
WHERE osq.device_id = p_device_id
AND osq.status = 'pending'
AND (osq.depends_on IS NULL OR osq.depends_on IN (
SELECT id FROM mobile.offline_sync_queue WHERE status = 'completed'
))
ORDER BY osq.sequence_number
LIMIT p_batch_size
);
-- Aqui iria la logica de procesamiento real
-- Por ahora solo retornamos conteos
SELECT COUNT(*) INTO v_processed
FROM mobile.offline_sync_queue
WHERE device_id = p_device_id AND status = 'processing';
RETURN QUERY SELECT v_processed, v_failed, v_conflicts;
END;
$$ LANGUAGE plpgsql;
-- Funcion para limpiar sesiones inactivas
CREATE OR REPLACE FUNCTION mobile.cleanup_inactive_sessions(p_hours INTEGER DEFAULT 24)
RETURNS INTEGER AS $$
DECLARE
cleaned_count INTEGER;
BEGIN
UPDATE mobile.mobile_sessions
SET status = 'expired', ended_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE status = 'active'
AND last_activity_at < CURRENT_TIMESTAMP - (p_hours || ' hours')::INTERVAL;
GET DIAGNOSTICS cleaned_count = ROW_COUNT;
RETURN cleaned_count;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- COMENTARIOS DE TABLAS
-- =====================
COMMENT ON TABLE mobile.mobile_sessions IS 'Sesiones activas de la aplicacion movil';
COMMENT ON TABLE mobile.offline_sync_queue IS 'Cola de operaciones pendientes de sincronizar desde modo offline';
COMMENT ON TABLE mobile.sync_conflicts IS 'Registro de conflictos de sincronizacion detectados';
COMMENT ON TABLE mobile.push_tokens IS 'Tokens de notificaciones push por dispositivo';
COMMENT ON TABLE mobile.push_notifications_log IS 'Log de notificaciones push enviadas';
COMMENT ON TABLE mobile.payment_transactions IS 'Transacciones de pago desde terminales moviles';