trading-platform/docs/02-definicion-modulos/OQI-009-marketplace/especificaciones/ET-MKT-001-database.md
rckrdmrd a7cca885f0 feat: Major platform documentation and architecture updates
Changes include:
- Updated architecture documentation
- Enhanced module definitions (OQI-001 to OQI-008)
- ML integration documentation updates
- Trading strategies documentation
- Orchestration and inventory updates
- Docker configuration updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 05:33:35 -06:00

28 KiB

id title type status priority epic project version dates tags
ET-MKT-001 Especificacion de Base de Datos - Marketplace technical-spec Draft High OQI-009 trading-platform 1.0.0
created updated
2026-01-04 2026-01-04
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

CREATE SCHEMA IF NOT EXISTS marketplace;

Tablas

1. product_categories

Categorias de productos del marketplace.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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)

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

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

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