--- id: ET-MKT-001 title: Especificacion de Base de Datos - Marketplace type: technical-spec status: Draft priority: High epic: OQI-009 project: trading-platform version: 1.0.0 dates: created: 2026-01-04 updated: 2026-01-04 tags: - marketplace - database - postgresql - schema --- # ET-MKT-001: Especificacion de Base de Datos ## Resumen Este documento define el schema de base de datos para el modulo de Marketplace (OQI-009), incluyendo tablas para productos, compras, suscripciones, asesoria y visualizacion premium. ## Schema **Schema Name**: `marketplace` ```sql CREATE SCHEMA IF NOT EXISTS marketplace; ``` ## Tablas ### 1. product_categories Categorias de productos del marketplace. ```sql CREATE TABLE marketplace.product_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL, slug VARCHAR(100) NOT NULL UNIQUE, description TEXT, icon VARCHAR(50), sort_order INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_product_categories_slug ON marketplace.product_categories(slug); CREATE INDEX idx_product_categories_active ON marketplace.product_categories(is_active); -- Datos iniciales INSERT INTO marketplace.product_categories (name, slug, description, icon, sort_order) VALUES ('Senales', 'signals', 'Paquetes de senales ML premium', 'signal', 1), ('Asesoria', 'advisory', 'Sesiones de consultoria 1:1', 'users', 2), ('Visualizacion', 'visualization', 'Herramientas graficas avanzadas', 'chart-bar', 3), ('Educacion', 'education', 'Cursos y materiales educativos', 'academic-cap', 4); ``` ### 2. products Catalogo de productos del marketplace. ```sql CREATE TYPE marketplace.product_type AS ENUM ( 'signal_pack', 'advisory_session', 'visualization_addon', 'course', 'ebook' ); CREATE TYPE marketplace.billing_type AS ENUM ( 'one_time', 'subscription' ); CREATE TABLE marketplace.products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, description TEXT, short_description VARCHAR(500), type marketplace.product_type NOT NULL, price DECIMAL(10, 2) NOT NULL CHECK (price > 0), currency VARCHAR(3) DEFAULT 'USD', billing_type marketplace.billing_type DEFAULT 'one_time', subscription_interval VARCHAR(20), -- 'monthly', 'yearly' category_id UUID REFERENCES marketplace.product_categories(id), metadata JSONB DEFAULT '{}', is_active BOOLEAN DEFAULT true, is_featured BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_products_slug ON marketplace.products(slug); CREATE INDEX idx_products_type ON marketplace.products(type); CREATE INDEX idx_products_category ON marketplace.products(category_id); CREATE INDEX idx_products_active ON marketplace.products(is_active); CREATE INDEX idx_products_featured ON marketplace.products(is_featured) WHERE is_featured = true; CREATE INDEX idx_products_price ON marketplace.products(price); -- Busqueda full-text CREATE INDEX idx_products_search ON marketplace.products USING gin(to_tsvector('spanish', coalesce(name, '') || ' ' || coalesce(description, ''))); -- Productos iniciales INSERT INTO marketplace.products (name, slug, type, price, billing_type, category_id, metadata, short_description) VALUES ('Basic Signal Pack', 'basic-signal-pack', 'signal_pack', 9.00, 'one_time', (SELECT id FROM marketplace.product_categories WHERE slug = 'signals'), '{"credits": 50, "validity_days": 30, "min_confidence": 80}', '50 senales ML de alta confianza'), ('Pro Signal Pack', 'pro-signal-pack', 'signal_pack', 29.00, 'one_time', (SELECT id FROM marketplace.product_categories WHERE slug = 'signals'), '{"credits": 200, "validity_days": 60, "min_confidence": 80}', '200 senales ML de alta confianza'), ('Unlimited Signals', 'unlimited-signals', 'signal_pack', 49.00, 'subscription', (SELECT id FROM marketplace.product_categories WHERE slug = 'signals'), '{"credits": -1, "min_confidence": 75}', 'Senales ML ilimitadas'), ('Asesoria 30 min', 'advisory-30', 'advisory_session', 49.00, 'one_time', (SELECT id FROM marketplace.product_categories WHERE slug = 'advisory'), '{"duration_minutes": 30}', 'Sesion de 30 minutos con asesor certificado'), ('Asesoria 60 min', 'advisory-60', 'advisory_session', 89.00, 'one_time', (SELECT id FROM marketplace.product_categories WHERE slug = 'advisory'), '{"duration_minutes": 60}', 'Sesion de 60 minutos + plan de accion'), ('Asesoria 90 min', 'advisory-90', 'advisory_session', 119.00, 'one_time', (SELECT id FROM marketplace.product_categories WHERE slug = 'advisory'), '{"duration_minutes": 90}', 'Sesion de 90 minutos + seguimiento'), ('Visualizacion Premium', 'visualization-premium', 'visualization_addon', 19.00, 'subscription', (SELECT id FROM marketplace.product_categories WHERE slug = 'visualization'), '{"features": ["ml_indicators", "unlimited_backtest", "unlimited_alerts", "multi_chart"]}', 'Indicadores ML exclusivos y herramientas avanzadas'); ``` ### 3. purchases Registro de compras realizadas. ```sql CREATE TYPE marketplace.purchase_status AS ENUM ( 'pending', 'completed', 'failed', 'refunded', 'partially_refunded' ); CREATE TABLE marketplace.purchases ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), product_id UUID NOT NULL REFERENCES marketplace.products(id), quantity INTEGER DEFAULT 1, unit_price DECIMAL(10, 2) NOT NULL, total_price DECIMAL(10, 2) NOT NULL, currency VARCHAR(3) DEFAULT 'USD', status marketplace.purchase_status DEFAULT 'pending', payment_id VARCHAR(255), -- ID de transaccion externa (Stripe) payment_method VARCHAR(50), metadata JSONB DEFAULT '{}', completed_at TIMESTAMP WITH TIME ZONE, refunded_at TIMESTAMP WITH TIME ZONE, refund_amount DECIMAL(10, 2), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_purchases_user ON marketplace.purchases(user_id); CREATE INDEX idx_purchases_product ON marketplace.purchases(product_id); CREATE INDEX idx_purchases_status ON marketplace.purchases(status); CREATE INDEX idx_purchases_payment ON marketplace.purchases(payment_id); CREATE INDEX idx_purchases_created ON marketplace.purchases(created_at DESC); ``` ### 4. subscriptions Suscripciones activas de usuarios. ```sql CREATE TYPE marketplace.subscription_status AS ENUM ( 'active', 'canceled', 'past_due', 'paused', 'expired' ); CREATE TABLE marketplace.subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), product_id UUID NOT NULL REFERENCES marketplace.products(id), purchase_id UUID REFERENCES marketplace.purchases(id), status marketplace.subscription_status DEFAULT 'active', current_period_start TIMESTAMP WITH TIME ZONE NOT NULL, current_period_end TIMESTAMP WITH TIME ZONE NOT NULL, cancel_at_period_end BOOLEAN DEFAULT false, canceled_at TIMESTAMP WITH TIME ZONE, cancellation_reason TEXT, stripe_subscription_id VARCHAR(255), metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), -- Solo una suscripcion activa por producto por usuario CONSTRAINT unique_active_subscription UNIQUE (user_id, product_id) ); -- Indices CREATE INDEX idx_subscriptions_user ON marketplace.subscriptions(user_id); CREATE INDEX idx_subscriptions_product ON marketplace.subscriptions(product_id); CREATE INDEX idx_subscriptions_status ON marketplace.subscriptions(status); CREATE INDEX idx_subscriptions_period_end ON marketplace.subscriptions(current_period_end); CREATE INDEX idx_subscriptions_stripe ON marketplace.subscriptions(stripe_subscription_id); ``` ### 5. signal_credits Creditos de senales de usuarios. ```sql CREATE TABLE marketplace.signal_credits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), product_id UUID NOT NULL REFERENCES marketplace.products(id), purchase_id UUID REFERENCES marketplace.purchases(id), subscription_id UUID REFERENCES marketplace.subscriptions(id), initial_amount INTEGER NOT NULL, remaining_amount INTEGER NOT NULL, expires_at TIMESTAMP WITH TIME ZONE, is_unlimited BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_signal_credits_user ON marketplace.signal_credits(user_id); CREATE INDEX idx_signal_credits_remaining ON marketplace.signal_credits(remaining_amount) WHERE remaining_amount > 0; CREATE INDEX idx_signal_credits_expires ON marketplace.signal_credits(expires_at); ``` ### 6. signal_deliveries Registro de senales entregadas a usuarios. ```sql CREATE TYPE marketplace.delivery_channel AS ENUM ( 'push', 'email', 'both', 'in_app' ); CREATE TABLE marketplace.signal_deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), signal_id UUID NOT NULL, -- FK a ml_signals credit_id UUID REFERENCES marketplace.signal_credits(id), delivery_channel marketplace.delivery_channel NOT NULL, delivered_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), read_at TIMESTAMP WITH TIME ZONE, metadata JSONB DEFAULT '{}' ); -- Indices CREATE INDEX idx_signal_deliveries_user ON marketplace.signal_deliveries(user_id); CREATE INDEX idx_signal_deliveries_signal ON marketplace.signal_deliveries(signal_id); CREATE INDEX idx_signal_deliveries_date ON marketplace.signal_deliveries(delivered_at DESC); ``` ### 7. advisors Asesores financieros registrados. ```sql CREATE TABLE marketplace.advisors ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) UNIQUE, display_name VARCHAR(255) NOT NULL, title VARCHAR(100), bio TEXT, short_bio VARCHAR(500), specialties JSONB DEFAULT '[]', experience_years INTEGER DEFAULT 0, hourly_rate DECIMAL(10, 2), languages JSONB DEFAULT '["es"]', cal_username VARCHAR(100), cal_event_type_id INTEGER, profile_image_url VARCHAR(500), rating DECIMAL(3, 2) DEFAULT 0.00, review_count INTEGER DEFAULT 0, completed_sessions INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT true, is_verified BOOLEAN DEFAULT false, verified_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_advisors_user ON marketplace.advisors(user_id); CREATE INDEX idx_advisors_active ON marketplace.advisors(is_active); CREATE INDEX idx_advisors_rating ON marketplace.advisors(rating DESC); CREATE INDEX idx_advisors_specialties ON marketplace.advisors USING gin(specialties); ``` ### 8. advisory_sessions Sesiones de asesoria agendadas. ```sql CREATE TYPE marketplace.session_status AS ENUM ( 'scheduled', 'in_progress', 'completed', 'cancelled', 'no_show_client', 'no_show_advisor' ); CREATE TABLE marketplace.advisory_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), advisor_id UUID NOT NULL REFERENCES marketplace.advisors(id), product_id UUID NOT NULL REFERENCES marketplace.products(id), purchase_id UUID REFERENCES marketplace.purchases(id), duration_minutes INTEGER NOT NULL, scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL, ended_at TIMESTAMP WITH TIME ZONE, status marketplace.session_status DEFAULT 'scheduled', cal_event_id VARCHAR(255), cal_booking_uid VARCHAR(255), daily_room_name VARCHAR(255), daily_room_url VARCHAR(500), recording_url VARCHAR(500), cancelled_at TIMESTAMP WITH TIME ZONE, cancellation_reason TEXT, refund_amount DECIMAL(10, 2), metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_advisory_sessions_user ON marketplace.advisory_sessions(user_id); CREATE INDEX idx_advisory_sessions_advisor ON marketplace.advisory_sessions(advisor_id); CREATE INDEX idx_advisory_sessions_scheduled ON marketplace.advisory_sessions(scheduled_at); CREATE INDEX idx_advisory_sessions_status ON marketplace.advisory_sessions(status); CREATE INDEX idx_advisory_sessions_cal ON marketplace.advisory_sessions(cal_booking_uid); ``` ### 9. session_notes Notas post-sesion de asesoria. ```sql CREATE TABLE marketplace.session_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES marketplace.advisory_sessions(id) UNIQUE, advisor_id UUID NOT NULL REFERENCES marketplace.advisors(id), summary TEXT, recommendations JSONB DEFAULT '[]', resources JSONB DEFAULT '[]', follow_up_actions JSONB DEFAULT '[]', private_notes TEXT, -- Solo visible para el asesor created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_session_notes_session ON marketplace.session_notes(session_id); CREATE INDEX idx_session_notes_advisor ON marketplace.session_notes(advisor_id); ``` ### 10. advisor_reviews Reviews de usuarios sobre asesores. ```sql CREATE TABLE marketplace.advisor_reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES marketplace.advisory_sessions(id) UNIQUE, user_id UUID NOT NULL REFERENCES auth.users(id), advisor_id UUID NOT NULL REFERENCES marketplace.advisors(id), rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5), comment TEXT, is_public BOOLEAN DEFAULT true, advisor_response TEXT, advisor_responded_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_advisor_reviews_advisor ON marketplace.advisor_reviews(advisor_id); CREATE INDEX idx_advisor_reviews_user ON marketplace.advisor_reviews(user_id); CREATE INDEX idx_advisor_reviews_rating ON marketplace.advisor_reviews(rating); CREATE INDEX idx_advisor_reviews_public ON marketplace.advisor_reviews(is_public) WHERE is_public = true; ``` ### 11. visualization_subscriptions Vista materializada para suscripciones de visualizacion. ```sql CREATE TABLE marketplace.visualization_subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) UNIQUE, subscription_id UUID NOT NULL REFERENCES marketplace.subscriptions(id), features JSONB DEFAULT '[]', started_at TIMESTAMP WITH TIME ZONE NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_vis_sub_user ON marketplace.visualization_subscriptions(user_id); CREATE INDEX idx_vis_sub_active ON marketplace.visualization_subscriptions(is_active); CREATE INDEX idx_vis_sub_expires ON marketplace.visualization_subscriptions(expires_at); ``` ### 12. user_chart_layouts Layouts de charts guardados por usuarios. ```sql CREATE TABLE marketplace.user_chart_layouts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), name VARCHAR(100) NOT NULL, layout_config JSONB NOT NULL, indicators JSONB DEFAULT '[]', is_default BOOLEAN DEFAULT false, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_chart_layouts_user ON marketplace.user_chart_layouts(user_id); CREATE INDEX idx_chart_layouts_default ON marketplace.user_chart_layouts(is_default) WHERE is_default = true; -- Trigger para asegurar solo un default por usuario CREATE OR REPLACE FUNCTION marketplace.ensure_single_default_layout() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_default = true THEN UPDATE marketplace.user_chart_layouts SET is_default = false WHERE user_id = NEW.user_id AND id != NEW.id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_single_default_layout BEFORE INSERT OR UPDATE ON marketplace.user_chart_layouts FOR EACH ROW EXECUTE FUNCTION marketplace.ensure_single_default_layout(); ``` ### 13. indicator_alerts Alertas de indicadores configuradas por usuarios. ```sql CREATE TABLE marketplace.indicator_alerts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), indicator_id VARCHAR(50) NOT NULL, symbol VARCHAR(20) NOT NULL, conditions JSONB NOT NULL, notification_channels JSONB DEFAULT '["push", "email"]', is_active BOOLEAN DEFAULT true, trigger_count INTEGER DEFAULT 0, last_triggered_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_indicator_alerts_user ON marketplace.indicator_alerts(user_id); CREATE INDEX idx_indicator_alerts_active ON marketplace.indicator_alerts(is_active); CREATE INDEX idx_indicator_alerts_symbol ON marketplace.indicator_alerts(symbol); ``` ### 14. backtest_results Resultados de backtesting guardados. ```sql CREATE TABLE marketplace.backtest_results ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), symbol VARCHAR(20) NOT NULL, strategy_name VARCHAR(100), strategy_config JSONB NOT NULL, period_start TIMESTAMP WITH TIME ZONE NOT NULL, period_end TIMESTAMP WITH TIME ZONE NOT NULL, initial_capital DECIMAL(15, 2) NOT NULL, final_capital DECIMAL(15, 2) NOT NULL, total_return DECIMAL(10, 4), total_trades INTEGER DEFAULT 0, winning_trades INTEGER DEFAULT 0, losing_trades INTEGER DEFAULT 0, win_rate DECIMAL(5, 2), profit_factor DECIMAL(5, 2), max_drawdown DECIMAL(5, 2), sharpe_ratio DECIMAL(5, 2), trades JSONB DEFAULT '[]', created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); -- Indices CREATE INDEX idx_backtest_user ON marketplace.backtest_results(user_id); CREATE INDEX idx_backtest_symbol ON marketplace.backtest_results(symbol); CREATE INDEX idx_backtest_created ON marketplace.backtest_results(created_at DESC); ``` ## Row Level Security (RLS) ```sql -- Habilitar RLS en todas las tablas ALTER TABLE marketplace.purchases ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.subscriptions ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.signal_credits ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.signal_deliveries ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.advisory_sessions ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.session_notes ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.advisor_reviews ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.user_chart_layouts ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.indicator_alerts ENABLE ROW LEVEL SECURITY; ALTER TABLE marketplace.backtest_results ENABLE ROW LEVEL SECURITY; -- Politicas para purchases CREATE POLICY purchases_select_own ON marketplace.purchases FOR SELECT USING (user_id = auth.uid()); CREATE POLICY purchases_insert_own ON marketplace.purchases FOR INSERT WITH CHECK (user_id = auth.uid()); -- Politicas para subscriptions CREATE POLICY subscriptions_select_own ON marketplace.subscriptions FOR SELECT USING (user_id = auth.uid()); -- Politicas para signal_credits CREATE POLICY signal_credits_select_own ON marketplace.signal_credits FOR SELECT USING (user_id = auth.uid()); -- Politicas para advisory_sessions (usuarios ven las suyas, asesores ven donde son asesores) CREATE POLICY advisory_sessions_select ON marketplace.advisory_sessions FOR SELECT USING ( user_id = auth.uid() OR advisor_id IN (SELECT id FROM marketplace.advisors WHERE user_id = auth.uid()) ); -- Politicas para session_notes CREATE POLICY session_notes_select ON marketplace.session_notes FOR SELECT USING ( session_id IN (SELECT id FROM marketplace.advisory_sessions WHERE user_id = auth.uid()) OR advisor_id IN (SELECT id FROM marketplace.advisors WHERE user_id = auth.uid()) ); -- Politicas para layouts y alertas CREATE POLICY chart_layouts_all_own ON marketplace.user_chart_layouts FOR ALL USING (user_id = auth.uid()); CREATE POLICY indicator_alerts_all_own ON marketplace.indicator_alerts FOR ALL USING (user_id = auth.uid()); CREATE POLICY backtest_results_all_own ON marketplace.backtest_results FOR ALL USING (user_id = auth.uid()); ``` ## Funciones y Triggers ### Actualizar rating de asesor ```sql CREATE OR REPLACE FUNCTION marketplace.update_advisor_rating() RETURNS TRIGGER AS $$ BEGIN UPDATE marketplace.advisors SET rating = ( SELECT COALESCE(AVG(rating), 0) FROM marketplace.advisor_reviews WHERE advisor_id = NEW.advisor_id ), review_count = ( SELECT COUNT(*) FROM marketplace.advisor_reviews WHERE advisor_id = NEW.advisor_id ), updated_at = NOW() WHERE id = NEW.advisor_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_update_advisor_rating AFTER INSERT OR UPDATE ON marketplace.advisor_reviews FOR EACH ROW EXECUTE FUNCTION marketplace.update_advisor_rating(); ``` ### Decrementar creditos de senales ```sql CREATE OR REPLACE FUNCTION marketplace.decrement_signal_credit( p_user_id UUID, p_signal_id UUID ) RETURNS BOOLEAN AS $$ DECLARE v_credit_id UUID; v_remaining INTEGER; BEGIN -- Buscar credito con saldo disponible (primero los que expiran antes) SELECT id, remaining_amount INTO v_credit_id, v_remaining FROM marketplace.signal_credits WHERE user_id = p_user_id AND (remaining_amount > 0 OR is_unlimited = true) AND (expires_at IS NULL OR expires_at > NOW()) ORDER BY is_unlimited ASC, expires_at ASC NULLS LAST LIMIT 1 FOR UPDATE; IF v_credit_id IS NULL THEN RETURN FALSE; END IF; -- Decrementar si no es unlimited IF v_remaining > 0 THEN UPDATE marketplace.signal_credits SET remaining_amount = remaining_amount - 1, updated_at = NOW() WHERE id = v_credit_id; END IF; -- Registrar delivery INSERT INTO marketplace.signal_deliveries (user_id, signal_id, credit_id, delivery_channel) VALUES (p_user_id, p_signal_id, v_credit_id, 'both'); RETURN TRUE; END; $$ LANGUAGE plpgsql; ``` ## Diagrama ER ``` ┌─────────────────────┐ ┌─────────────────────┐ │ product_categories │ │ products │ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │◄────┤ category_id (FK) │ │ name │ │ id (PK) │ │ slug │ │ name, slug │ │ description │ │ type, price │ └─────────────────────┘ │ billing_type │ └──────────┬──────────┘ │ ┌──────────────────────────┼──────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ purchases │ │ subscriptions │ │ signal_credits │ ├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │ │ id (PK) │ │ id (PK) │ │ user_id (FK) │ │ user_id (FK) │ │ user_id (FK) │ │ product_id (FK) │ │ product_id (FK) │ │ product_id (FK) │ │ status, total_price │ │ status, period_* │ │ remaining_amount │ └─────────────────────┘ └─────────────────────┘ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ signal_deliveries │ ├─────────────────────┤ │ id (PK) │ │ user_id, signal_id │ │ credit_id (FK) │ └─────────────────────┘ ┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ │ advisors │ │ advisory_sessions │ │ session_notes │ ├─────────────────────┤ ├─────────────────────┤ ├─────────────────────┤ │ id (PK) │◄────┤ advisor_id (FK) │◄────┤ session_id (FK) │ │ user_id (FK) │ │ id (PK) │ │ id (PK) │ │ display_name │ │ user_id (FK) │ │ summary │ │ rating │ │ scheduled_at │ │ recommendations │ └─────────┬───────────┘ └─────────────────────┘ └─────────────────────┘ │ ▼ ┌─────────────────────┐ │ advisor_reviews │ ├─────────────────────┤ │ id (PK) │ │ advisor_id (FK) │ │ session_id (FK) │ │ rating, comment │ └─────────────────────┘ ``` ## Referencias - [RF-MKT-001: Catalogo](../requerimientos/RF-MKT-001-catalogo.md) - [RF-MKT-002: Senales Premium](../requerimientos/RF-MKT-002-senales-premium.md) - [RF-MKT-003: Asesoria](../requerimientos/RF-MKT-003-asesoria.md) - [RF-MKT-004: Visualizacion](../requerimientos/RF-MKT-004-visualizacion.md)