Files
bakery-ia/services/demo_session
Urtzi Alfaro fd657dea02 refactor(demo): Standardize demo account type names across codebase
Standardize demo account type naming from inconsistent variants to clean names:
- individual_bakery, professional_bakery → professional
- central_baker, enterprise_chain → enterprise

This eliminates naming confusion that was causing bugs in the demo session
initialization, particularly for enterprise demo tenants where different
parts of the system used different names for the same concept.

Changes:
- Updated source of truth in demo_session config
- Updated all backend services (middleware, cloning, orchestration)
- Updated frontend types, pages, and stores
- Updated demo session models and schemas
- Removed all backward compatibility code as requested

Related to: Enterprise demo session access fix

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 08:48:56 +01:00
..
2025-10-03 14:09:34 +02:00
2025-11-06 14:10:04 +01:00

Demo Session Service

Overview

The Demo Session Service creates ephemeral, isolated demo environments for sales demonstrations and prospect trials. It provisions temporary tenants with pre-seeded realistic bakery data, allowing prospects to explore the full platform without affecting production data. Demo sessions automatically expire after a configurable period (default: 24 hours) and are completely isolated from real customer tenants, making it safe for prospects to experiment freely.

Key Features

Demo Environment Provisioning

  • One-Click Demo Creation - Create demo tenant in seconds
  • Pre-Seeded Data - Realistic sales, inventory, forecast data
  • Isolated Tenants - Complete separation from production
  • Temporary Credentials - Auto-generated demo user accounts
  • Configurable Duration - 1 hour to 7 days (default: 24 hours)
  • Instant Access - No email verification required

Realistic Demo Data

  • 90 Days Sales History - Realistic transaction patterns
  • Product Catalog - 20+ common bakery products
  • Inventory - Current stock levels and movements
  • Forecasts - Pre-generated 7-day forecasts
  • Production Schedules - Sample production plans
  • Suppliers - 5+ sample supplier profiles
  • Team Members - Sample staff with different roles

Demo Scenarios

  • Standard Bakery - Small neighborhood bakery (1 location)
  • Multi-Location - Bakery chain (3 locations)
  • High-Volume - Large production bakery
  • Custom Scenario - Configurable for specific prospects
  • Spanish Locale - Madrid-based bakery examples
  • Feature Showcase - Highlight specific capabilities

Session Management

  • Auto-Expiration - Automatic cleanup after expiry
  • Session Extension - Extend active demos
  • Session Termination - Manually end demo
  • Session Analytics - Track demo engagement
  • Concurrent Limits - Prevent resource abuse
  • IP-Based Tracking - Monitor demo usage

Sales Enablement

  • Demo Link Generation - Shareable demo URLs
  • Sales Dashboard - Track active demos
  • Usage Analytics - Feature engagement metrics
  • Lead Tracking - Connect demos to CRM
  • Conversion Tracking - Demo to trial to paid
  • Performance Metrics - Demo success rates

Security & Isolation

  • Tenant Isolation - Complete data separation
  • Resource Limits - Prevent abuse
  • Auto-Cleanup - Remove expired demos
  • No Production Access - Isolated database/environment
  • Rate Limiting - Prevent demo spam
  • Audit Logging - Track all demo activities

Business Value

For Sales Team

  • Instant Demos - No setup time, always ready
  • Realistic Experience - Prospects see real functionality
  • Risk-Free - Prospects can't break anything
  • Consistent - Every demo shows same quality data
  • Scalable - Handle 100+ concurrent demos
  • Self-Service - Prospects can explore independently

Quantifiable Impact

  • Sales Cycle: 30-50% shorter with live demos
  • Conversion Rate: 2-3× higher vs. screenshots/videos
  • Demo Setup Time: 0 minutes vs. 15-30 minutes manual
  • Lead Quality: Higher engagement indicates serious interest
  • Sales Efficiency: 5-10× more demos per sales rep
  • Cost Savings: €500-1,500/month (sales time saved)

For Prospects

  • Try Before Buy: Experience platform hands-on
  • No Commitment: No credit card, no sign-up friction
  • Immediate Access: Start exploring in 30 seconds
  • Realistic Data: Understand real-world value
  • Self-Paced: Explore at own speed
  • Safe Environment: Can't break or affect anything

Technology Stack

  • Framework: FastAPI (Python 3.11+) - Async web framework
  • Database: PostgreSQL 17 - Demo session tracking
  • Demo DB: Separate PostgreSQL - Isolated demo data
  • Caching: Redis 7.4 - Session cache, rate limiting
  • Messaging: RabbitMQ 4.1 - Cleanup events
  • Data Seeding: Faker, custom data generators
  • ORM: SQLAlchemy 2.0 (async) - Database abstraction
  • Logging: Structlog - Structured JSON logging
  • Metrics: Prometheus Client - Demo metrics

API Endpoints (Key Routes)

Demo Session Management

  • POST /api/v1/demo-sessions - Create new demo session
  • GET /api/v1/demo-sessions/{session_id} - Get session details
  • POST /api/v1/demo-sessions/{session_id}/extend - Extend session
  • DELETE /api/v1/demo-sessions/{session_id} - Terminate session
  • GET /api/v1/demo-sessions/{session_id}/credentials - Get login credentials
  • GET /api/v1/demo-sessions/active - List active sessions

Demo Scenarios

  • GET /api/v1/demo-sessions/scenarios - List available scenarios
  • GET /api/v1/demo-sessions/scenarios/{scenario_id} - Get scenario details
  • POST /api/v1/demo-sessions/scenarios/{scenario_id}/create - Create session from scenario

Sales Dashboard (Internal)

  • GET /api/v1/demo-sessions/analytics/dashboard - Demo analytics
  • GET /api/v1/demo-sessions/analytics/usage - Usage patterns
  • GET /api/v1/demo-sessions/analytics/conversion - Demo to signup conversion

Health & Monitoring

  • GET /api/v1/demo-sessions/health - Service health
  • GET /api/v1/demo-sessions/cleanup/status - Cleanup job status

Database Schema

Main Tables

demo_sessions

CREATE TABLE demo_sessions (
    id UUID PRIMARY KEY,
    session_token VARCHAR(255) UNIQUE NOT NULL,
    demo_tenant_id UUID NOT NULL,              -- Demo tenant in separate DB

    -- Configuration
    scenario_name VARCHAR(100) NOT NULL,       -- standard_bakery, multi_location, etc.
    duration_hours INTEGER DEFAULT 24,

    -- Status
    status VARCHAR(50) DEFAULT 'active',       -- active, extended, expired, terminated
    created_at TIMESTAMP DEFAULT NOW(),
    expires_at TIMESTAMP NOT NULL,
    extended_count INTEGER DEFAULT 0,
    terminated_at TIMESTAMP,
    termination_reason VARCHAR(255),

    -- Tracking
    created_by_ip INET,
    user_agent TEXT,
    referrer VARCHAR(500),
    utm_source VARCHAR(100),
    utm_campaign VARCHAR(100),
    utm_medium VARCHAR(100),

    -- Usage analytics
    login_count INTEGER DEFAULT 0,
    last_activity_at TIMESTAMP,
    page_views INTEGER DEFAULT 0,
    features_used JSONB,                       -- Array of feature names

    -- Lead info (if provided)
    lead_email VARCHAR(255),
    lead_name VARCHAR(255),
    lead_phone VARCHAR(50),
    lead_company VARCHAR(255),

    INDEX idx_sessions_status ON (status, expires_at),
    INDEX idx_sessions_token ON (session_token)
);

demo_scenarios

CREATE TABLE demo_scenarios (
    id UUID PRIMARY KEY,
    scenario_name VARCHAR(100) UNIQUE NOT NULL,
    display_name VARCHAR(255) NOT NULL,
    description TEXT,

    -- Configuration
    business_name VARCHAR(255),
    location_count INTEGER DEFAULT 1,
    product_count INTEGER DEFAULT 20,
    days_of_history INTEGER DEFAULT 90,

    -- Features to highlight
    featured_capabilities JSONB,

    -- Data generation settings
    seed_data_config JSONB,

    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT NOW()
);

demo_session_events

CREATE TABLE demo_session_events (
    id UUID PRIMARY KEY,
    session_id UUID REFERENCES demo_sessions(id) ON DELETE CASCADE,
    event_type VARCHAR(100) NOT NULL,          -- login, page_view, feature_used, action_performed
    event_data JSONB,
    ip_address INET,
    occurred_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_session_events_session (session_id, occurred_at)
);

demo_session_metrics

CREATE TABLE demo_session_metrics (
    id UUID PRIMARY KEY,
    metric_date DATE NOT NULL,
    scenario_name VARCHAR(100),

    -- Volume
    sessions_created INTEGER DEFAULT 0,
    sessions_completed INTEGER DEFAULT 0,      -- Not terminated early
    sessions_expired INTEGER DEFAULT 0,
    sessions_terminated INTEGER DEFAULT 0,

    -- Engagement
    avg_duration_minutes INTEGER,
    avg_login_count DECIMAL(5, 2),
    avg_page_views DECIMAL(5, 2),
    avg_features_used DECIMAL(5, 2),

    -- Conversion
    demo_to_signup_count INTEGER DEFAULT 0,
    conversion_rate_percentage DECIMAL(5, 2),

    calculated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(metric_date, scenario_name)
);

Indexes for Performance

CREATE INDEX idx_sessions_expires ON demo_sessions(expires_at) WHERE status = 'active';
CREATE INDEX idx_sessions_scenario ON demo_sessions(scenario_name, created_at DESC);
CREATE INDEX idx_events_session_type ON demo_session_events(session_id, event_type);

Business Logic Examples

Demo Session Creation

async def create_demo_session(
    scenario_name: str = 'standard_bakery',
    duration_hours: int = 24,
    lead_info: dict = None,
    request_info: dict = None
) -> DemoSession:
    """
    Create new demo session with pre-seeded data.
    """
    # Get scenario configuration
    scenario = await db.query(DemoScenario).filter(
        DemoScenario.scenario_name == scenario_name,
        DemoScenario.is_active == True
    ).first()

    if not scenario:
        raise ValueError("Invalid scenario")

    # Check concurrent demo limit
    active_demos = await db.query(DemoSession).filter(
        DemoSession.status == 'active',
        DemoSession.expires_at > datetime.utcnow()
    ).count()

    if active_demos >= MAX_CONCURRENT_DEMOS:
        raise Exception("Maximum concurrent demos reached")

    try:
        # Generate session token
        session_token = secrets.token_urlsafe(32)

        # Create demo tenant in separate database
        demo_tenant = await create_demo_tenant(scenario)

        # Seed demo data
        await seed_demo_data(demo_tenant.id, scenario)

        # Create session record
        session = DemoSession(
            session_token=session_token,
            demo_tenant_id=demo_tenant.id,
            scenario_name=scenario_name,
            duration_hours=duration_hours,
            expires_at=datetime.utcnow() + timedelta(hours=duration_hours),
            created_by_ip=request_info.get('ip'),
            user_agent=request_info.get('user_agent'),
            referrer=request_info.get('referrer'),
            utm_source=request_info.get('utm_source'),
            utm_campaign=request_info.get('utm_campaign'),
            lead_email=lead_info.get('email') if lead_info else None,
            lead_name=lead_info.get('name') if lead_info else None
        )

        db.add(session)

        # Log event
        event = DemoSessionEvent(
            session_id=session.id,
            event_type='session_created',
            event_data={'scenario': scenario_name},
            ip_address=request_info.get('ip')
        )
        db.add(event)

        await db.commit()

        logger.info("Demo session created",
                   session_id=str(session.id),
                   scenario=scenario_name,
                   duration_hours=duration_hours)

        # Publish event
        await publish_event('demo_sessions', 'demo.session_created', {
            'session_id': str(session.id),
            'scenario': scenario_name
        })

        return session

    except Exception as e:
        logger.error("Demo session creation failed",
                    scenario=scenario_name,
                    error=str(e))
        raise

async def create_demo_tenant(scenario: DemoScenario) -> DemoTenant:
    """
    Create isolated demo tenant in demo database.
    """
    # Use separate database connection for demo data
    demo_db = get_demo_database_connection()

    tenant = DemoTenant(
        tenant_name=scenario.business_name or "Demo Bakery",
        email=f"demo_{uuid.uuid4().hex[:8]}@bakery-ia.com",
        status='demo',
        subscription_tier='pro',  # Always show Pro features in demo
        is_demo=True
    )

    demo_db.add(tenant)
    await demo_db.commit()

    return tenant

async def seed_demo_data(tenant_id: UUID, scenario: DemoScenario):
    """
    Seed demo tenant with realistic data.
    """
    demo_db = get_demo_database_connection()

    # Seed configuration
    config = scenario.seed_data_config or {}
    product_count = config.get('product_count', 20)
    days_of_history = config.get('days_of_history', 90)

    # 1. Seed product catalog
    products = await seed_products(demo_db, tenant_id, product_count)

    # 2. Seed suppliers
    suppliers = await seed_suppliers(demo_db, tenant_id, 5)

    # 3. Seed inventory
    await seed_inventory(demo_db, tenant_id, products, suppliers)

    # 4. Seed sales history (90 days)
    await seed_sales_history(demo_db, tenant_id, products, days_of_history)

    # 5. Generate forecasts
    await seed_forecasts(demo_db, tenant_id, products)

    # 6. Seed production schedules
    await seed_production_schedules(demo_db, tenant_id, products)

    # 7. Seed team members
    await seed_team_members(demo_db, tenant_id)

    logger.info("Demo data seeded",
               tenant_id=str(tenant_id),
               products=len(products),
               suppliers=len(suppliers))

async def seed_sales_history(
    demo_db,
    tenant_id: UUID,
    products: list,
    days: int = 90
) -> list:
    """
    Generate realistic sales history using patterns.
    """
    from faker import Faker
    fake = Faker('es_ES')  # Spanish locale

    sales_records = []
    start_date = date.today() - timedelta(days=days)

    for day_offset in range(days):
        current_date = start_date + timedelta(days=day_offset)
        is_weekend = current_date.weekday() >= 5
        is_holiday = await is_spanish_holiday(current_date)

        # Adjust volume based on day type
        base_transactions = 50
        if is_weekend:
            base_transactions = int(base_transactions * 1.4)  # 40% more on weekends
        if is_holiday:
            base_transactions = int(base_transactions * 0.7)  # 30% less on holidays

        # Add randomness
        daily_transactions = int(base_transactions * random.uniform(0.8, 1.2))

        for _ in range(daily_transactions):
            # Random product
            product = random.choice(products)

            # Realistic quantity (most orders are 1-5 units)
            quantity = random.choices([1, 2, 3, 4, 5, 6, 10], weights=[40, 25, 15, 10, 5, 3, 2])[0]

            # Calculate price with small variance
            unit_price = product.price * random.uniform(0.95, 1.05)

            sale = DemoSale(
                tenant_id=tenant_id,
                sale_date=current_date,
                sale_time=fake.time(),
                product_id=product.id,
                product_name=product.name,
                quantity=quantity,
                unit_price=unit_price,
                total_amount=quantity * unit_price,
                channel='pos'
            )

            sales_records.append(sale)

    # Bulk insert
    demo_db.bulk_save_objects(sales_records)
    await demo_db.commit()

    return sales_records

Auto-Cleanup Job

async def cleanup_expired_demos():
    """
    Background job to cleanup expired demo sessions.
    Runs every hour.
    """
    # Find expired sessions
    expired_sessions = await db.query(DemoSession).filter(
        DemoSession.status == 'active',
        DemoSession.expires_at <= datetime.utcnow()
    ).all()

    for session in expired_sessions:
        try:
            # Mark session as expired
            session.status = 'expired'
            session.terminated_at = datetime.utcnow()

            # Delete demo tenant and all data
            await delete_demo_tenant(session.demo_tenant_id)

            # Log event
            event = DemoSessionEvent(
                session_id=session.id,
                event_type='session_expired',
                occurred_at=datetime.utcnow()
            )
            db.add(event)

            logger.info("Demo session cleaned up",
                       session_id=str(session.id),
                       duration_hours=(session.terminated_at - session.created_at).total_seconds() / 3600)

        except Exception as e:
            logger.error("Demo cleanup failed",
                        session_id=str(session.id),
                        error=str(e))
            continue

    await db.commit()

    logger.info("Demo cleanup completed",
               expired_count=len(expired_sessions))

Events & Messaging

Published Events (RabbitMQ)

Exchange: demo_sessions Routing Keys: demo.session_created, demo.session_converted

Demo Session Created Event

{
    "event_type": "demo_session_created",
    "session_id": "uuid",
    "scenario": "standard_bakery",
    "duration_hours": 24,
    "lead_email": "prospect@example.com",
    "utm_source": "google_ads",
    "timestamp": "2025-11-06T10:00:00Z"
}

Demo Converted to Signup

{
    "event_type": "demo_session_converted",
    "session_id": "uuid",
    "tenant_id": "uuid",
    "scenario": "standard_bakery",
    "demo_duration_hours": 2.5,
    "features_used": ["forecasting", "inventory", "production"],
    "timestamp": "2025-11-06T12:30:00Z"
}

Custom Metrics (Prometheus)

# Demo session metrics
demo_sessions_created_total = Counter(
    'demo_sessions_created_total',
    'Total demo sessions created',
    ['scenario']
)

demo_sessions_active = Gauge(
    'demo_sessions_active',
    'Current active demo sessions',
    []
)

demo_session_duration_hours = Histogram(
    'demo_session_duration_hours',
    'Demo session duration',
    ['scenario'],
    buckets=[0.5, 1, 2, 4, 8, 12, 24, 48]
)

demo_to_signup_conversions_total = Counter(
    'demo_to_signup_conversions_total',
    'Demo sessions that converted to signup',
    ['scenario']
)

demo_feature_usage_total = Counter(
    'demo_feature_usage_total',
    'Feature usage in demos',
    ['feature_name']
)

Configuration

Environment Variables

Service Configuration:

  • PORT - Service port (default: 8019)
  • DATABASE_URL - Main PostgreSQL connection
  • DEMO_DATABASE_URL - Isolated demo database
  • REDIS_URL - Redis connection string
  • RABBITMQ_URL - RabbitMQ connection string

Demo Configuration:

  • DEFAULT_DEMO_DURATION_HOURS - Default duration (default: 24)
  • MAX_DEMO_DURATION_HOURS - Maximum duration (default: 168/7 days)
  • MAX_CONCURRENT_DEMOS - Concurrent limit (default: 100)
  • CLEANUP_INTERVAL_MINUTES - Cleanup frequency (default: 60)

Data Seeding:

  • DEMO_SALES_HISTORY_DAYS - Sales history length (default: 90)
  • DEMO_PRODUCT_COUNT - Number of products (default: 20)
  • DEMO_SUPPLIER_COUNT - Number of suppliers (default: 5)

Development Setup

Prerequisites

  • Python 3.11+
  • PostgreSQL 17 (2 databases: main + demo)
  • Redis 7.4
  • RabbitMQ 4.1

Local Development

cd services/demo_session
python -m venv venv
source venv/bin/activate

pip install -r requirements.txt

export DATABASE_URL=postgresql://user:pass@localhost:5432/demo_session
export DEMO_DATABASE_URL=postgresql://user:pass@localhost:5432/demo_data
export REDIS_URL=redis://localhost:6379/0
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/

alembic upgrade head
python main.py

Integration Points

Dependencies

  • Separate Demo Database - Isolated demo tenant data
  • Auth Service - Demo user credentials
  • Data Generators - Realistic data seeding
  • PostgreSQL - Session tracking
  • Redis - Rate limiting, caching
  • RabbitMQ - Event publishing

Dependents

  • Sales Team - Demo creation
  • Marketing - Landing page demos
  • Frontend - Demo UI access
  • Analytics - Demo conversion tracking

Business Value for VUE Madrid

Problem Statement

Traditional sales demos are difficult:

  • Time-consuming setup (15-30 minutes per demo)
  • Risk of breaking things in front of prospects
  • Inconsistent demo quality
  • No self-service for prospects
  • Hard to track engagement
  • Limited by sales rep availability

Solution

Bakery-IA Demo Session Service provides:

  • Instant Demos: Ready in 30 seconds
  • Risk-Free: Isolated environments
  • Self-Service: Prospects explore independently
  • Consistent Quality: Same data every time
  • Engagement Tracking: Know what prospects care about
  • Scalable: Unlimited concurrent demos

Quantifiable Impact

Sales Efficiency:

  • 30-50% shorter sales cycle with live demos
  • 2-3× conversion rate vs. static presentations
  • 5-10× more demos per sales rep
  • 0 minutes setup time vs. 15-30 minutes
  • €500-1,500/month sales time saved

Lead Quality:

  • Higher engagement = more qualified leads
  • Feature usage indicates specific needs
  • Demo-to-trial conversion: 35-45%
  • Trial-to-paid conversion: 25-35%
  • Overall demo-to-paid: 12-16%

Marketing Value:

  • Self-service demos on landing page
  • 24/7 availability for global prospects
  • Viral potential (shareable demo links)
  • Lower customer acquisition cost
  • Better understanding of product-market fit

Target Market Fit (Spanish Bakeries)

  • Visual Learners: Spanish business culture values demonstrations
  • Trust Building: Try-before-buy reduces risk perception
  • Language: Demo data in Spanish increases resonance
  • Realistic: Spanish products, Madrid locations feel authentic

ROI for Platform

Investment: €100-300/month (compute + storage for demos) Value Generated:

  • 50+ demos/month → 20 trials → 6 paid customers
  • 6 customers × €66 avg MRR = €396/month
  • Payback: 1-3 months
  • ROI: 30-400% depending on conversion rates

Copyright © 2025 Bakery-IA. All rights reserved.