Files
bakery-ia/services/tenant
2025-11-06 14:10:04 +01:00
..
2025-11-05 13:34:56 +01:00
2025-11-06 11:04:50 +01:00
2025-11-05 13:34:56 +01:00
2025-09-30 08:12:45 +02:00
2025-11-06 14:10:04 +01:00

Tenant Service

Overview

The Tenant Service manages the multi-tenant SaaS architecture, handling tenant (bakery) registration, subscription management via Stripe, team member administration, and billing. It provides tenant isolation, subscription tier enforcement, usage tracking, and automated billing workflows. This service is the foundation for scaling Bakery-IA to thousands of Spanish bakeries with a sustainable SaaS revenue model.

Key Features

Tenant Management

  • Tenant Registration - New bakery signup and onboarding
  • Tenant Profiles - Business information, settings, preferences
  • Multi-Location Support - Multiple stores per tenant
  • Tenant Isolation - Complete data separation between tenants
  • Tenant Settings - Configurable features per tenant
  • Tenant Branding - Custom logos, colors (Enterprise tier)
  • Tenant Status - Active, trial, suspended, cancelled

Subscription Management

  • Stripe Integration - Full Stripe API integration
  • Subscription Tiers - Free, Pro, Enterprise plans
  • Trial Management - 14-day free trials
  • Upgrade/Downgrade - Self-service plan changes
  • Proration - Automatic prorated billing
  • Payment Methods - Credit cards, SEPA Direct Debit (Europe)
  • Invoicing - Automatic invoice generation

Team Management

  • Multi-User Access - Invite team members to tenant
  • Role-Based Access Control - Owner, Admin, Manager, Staff
  • Permission Management - Granular feature permissions
  • Team Member Invitations - Email invites with expiry
  • Team Member Removal - Revoke access
  • Activity Tracking - Team member audit logs

Billing & Usage

  • Usage Tracking - API calls, storage, transactions
  • Usage Limits - Enforce tier limits
  • Overage Handling - Charge for overages or block access
  • Billing History - Complete invoice history
  • Payment Status - Track payment success/failure
  • Failed Payment Handling - Retry logic, suspension
  • Revenue Analytics - MRR, churn, LTV tracking

Subscription Tiers

Free Tier (€0/month):

  • 1 location
  • 100 transactions/month
  • 1 user
  • Email support
  • Basic features only

Pro Tier (€49/month):

  • 3 locations
  • Unlimited transactions
  • 5 users
  • Priority email support
  • All features
  • WhatsApp notifications
  • Advanced analytics

Enterprise Tier (€149/month):

  • Unlimited locations
  • Unlimited transactions
  • Unlimited users
  • Phone + email support
  • All features
  • Custom branding
  • Dedicated account manager
  • SLA guarantee

Compliance & Security

  • GDPR Compliance - Data protection built-in
  • Data Residency - EU data storage (Spain/Germany)
  • Tenant Data Export - Complete data export capability
  • Tenant Deletion - GDPR-compliant account deletion
  • Audit Logging - Complete tenant activity logs
  • Security Settings - 2FA, IP whitelist (Enterprise)

Business Value

For Bakery Owners

  • Predictable Pricing - Clear monthly costs
  • Start Free - Try before buying with 14-day trial
  • Scale as You Grow - Upgrade when needed
  • Team Collaboration - Invite staff with appropriate access
  • Professional Invoicing - Automatic Spanish tax-compliant invoices
  • Easy Cancellation - Cancel anytime, no long-term commitment

Quantifiable Impact

  • MRR per Customer: €0-149/month based on tier
  • Customer Acquisition Cost: €200-300 (PPC + sales)
  • Customer Lifetime Value: €1,200-3,600 (avg 24-month retention)
  • Churn Rate: <10%/month target (industry: 5-15%)
  • Expansion Revenue: 30-40% customers upgrade within 6 months
  • Payment Success Rate: 95%+ with Stripe

For Platform (Bakery-IA)

  • Scalable Revenue: Subscription model scales with customers
  • Automated Billing: No manual invoicing needed
  • European Payments: SEPA support for Spanish/EU customers
  • Churn Prevention: Usage tracking enables proactive retention
  • Expansion Opportunities: Upsell based on usage
  • Financial Visibility: Real-time revenue metrics

Technology Stack

  • Framework: FastAPI (Python 3.11+) - Async web framework
  • Database: PostgreSQL 17 - Tenant and subscription data
  • Payments: Stripe API - Payment processing
  • Caching: Redis 7.4 - Subscription cache
  • Messaging: RabbitMQ 4.1 - Event publishing
  • ORM: SQLAlchemy 2.0 (async) - Database abstraction
  • Logging: Structlog - Structured JSON logging
  • Metrics: Prometheus Client - Subscription metrics

API Endpoints (Key Routes)

Tenant Management

  • POST /api/v1/tenants - Create new tenant (signup)
  • GET /api/v1/tenants/{tenant_id} - Get tenant details
  • PUT /api/v1/tenants/{tenant_id} - Update tenant
  • DELETE /api/v1/tenants/{tenant_id} - Delete tenant (GDPR)
  • GET /api/v1/tenants/{tenant_id}/settings - Get settings
  • PUT /api/v1/tenants/{tenant_id}/settings - Update settings

Subscription Management

  • GET /api/v1/tenants/{tenant_id}/subscription - Get subscription
  • POST /api/v1/tenants/{tenant_id}/subscription - Create subscription
  • PUT /api/v1/tenants/{tenant_id}/subscription - Update subscription (upgrade/downgrade)
  • DELETE /api/v1/tenants/{tenant_id}/subscription - Cancel subscription
  • POST /api/v1/tenants/{tenant_id}/subscription/reactivate - Reactivate cancelled subscription

Payment Methods

  • GET /api/v1/tenants/{tenant_id}/payment-methods - List payment methods
  • POST /api/v1/tenants/{tenant_id}/payment-methods - Add payment method
  • PUT /api/v1/tenants/{tenant_id}/payment-methods/{pm_id}/default - Set default
  • DELETE /api/v1/tenants/{tenant_id}/payment-methods/{pm_id} - Remove payment method

Team Management

  • GET /api/v1/tenants/{tenant_id}/members - List team members
  • POST /api/v1/tenants/{tenant_id}/members/invite - Invite team member
  • PUT /api/v1/tenants/{tenant_id}/members/{member_id} - Update member role
  • DELETE /api/v1/tenants/{tenant_id}/members/{member_id} - Remove member
  • GET /api/v1/tenants/invitations/{invitation_token} - Get invitation details
  • POST /api/v1/tenants/invitations/{invitation_token}/accept - Accept invitation

Billing & Usage

  • GET /api/v1/tenants/{tenant_id}/invoices - List invoices
  • GET /api/v1/tenants/{tenant_id}/invoices/{invoice_id} - Get invoice
  • GET /api/v1/tenants/{tenant_id}/usage - Current usage statistics
  • GET /api/v1/tenants/{tenant_id}/usage/history - Historical usage

Stripe Webhooks

  • POST /api/v1/stripe/webhooks - Stripe webhook receiver

Analytics (Internal)

  • GET /api/v1/tenants/analytics/mrr - Monthly recurring revenue
  • GET /api/v1/tenants/analytics/churn - Churn rate
  • GET /api/v1/tenants/analytics/ltv - Customer lifetime value

Database Schema

Main Tables

tenants

CREATE TABLE tenants (
    id UUID PRIMARY KEY,
    tenant_name VARCHAR(255) NOT NULL,
    business_legal_name VARCHAR(255),
    tax_id VARCHAR(50),                          -- CIF/NIF for Spanish businesses
    business_type VARCHAR(100),                  -- bakery, pastry_shop, cafe, franchise

    -- Contact
    email VARCHAR(255) NOT NULL,
    phone VARCHAR(50),
    address_line1 VARCHAR(255),
    address_line2 VARCHAR(255),
    city VARCHAR(100),
    postal_code VARCHAR(20),
    country VARCHAR(100) DEFAULT 'España',

    -- Status
    status VARCHAR(50) DEFAULT 'trial',          -- trial, active, suspended, cancelled
    trial_ends_at TIMESTAMP,
    suspended_at TIMESTAMP,
    suspended_reason TEXT,
    cancelled_at TIMESTAMP,
    cancellation_reason TEXT,

    -- Subscription
    subscription_tier VARCHAR(50) DEFAULT 'free', -- free, pro, enterprise
    stripe_customer_id VARCHAR(255),             -- Stripe customer ID
    stripe_subscription_id VARCHAR(255),         -- Stripe subscription ID

    -- Settings
    timezone VARCHAR(50) DEFAULT 'Europe/Madrid',
    language VARCHAR(10) DEFAULT 'es',
    currency VARCHAR(10) DEFAULT 'EUR',
    settings JSONB,                              -- Custom settings

    -- Usage limits
    max_locations INTEGER DEFAULT 1,
    max_users INTEGER DEFAULT 1,
    max_transactions_per_month INTEGER DEFAULT 100,

    -- Branding (Enterprise only)
    logo_url VARCHAR(500),
    primary_color VARCHAR(10),

    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(email)
);

tenant_subscriptions

CREATE TABLE tenant_subscriptions (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    stripe_subscription_id VARCHAR(255) UNIQUE,
    stripe_customer_id VARCHAR(255),

    -- Plan details
    plan_tier VARCHAR(50) NOT NULL,              -- free, pro, enterprise
    plan_interval VARCHAR(50) DEFAULT 'month',   -- month, year
    plan_amount DECIMAL(10, 2) NOT NULL,         -- Monthly amount in euros

    -- Status
    status VARCHAR(50) NOT NULL,                 -- active, trialing, past_due, cancelled, unpaid
    trial_start TIMESTAMP,
    trial_end TIMESTAMP,
    current_period_start TIMESTAMP,
    current_period_end TIMESTAMP,
    cancel_at_period_end BOOLEAN DEFAULT FALSE,
    cancelled_at TIMESTAMP,

    -- Payment
    default_payment_method_id VARCHAR(255),

    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

tenant_members

CREATE TABLE tenant_members (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    user_id UUID NOT NULL,                       -- Link to auth service user
    role VARCHAR(50) NOT NULL,                   -- owner, admin, manager, staff

    -- Permissions
    permissions JSONB,                           -- Granular permissions

    -- Status
    status VARCHAR(50) DEFAULT 'active',         -- active, inactive, invited
    invited_by UUID,
    invited_at TIMESTAMP,
    accepted_at TIMESTAMP,

    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(tenant_id, user_id)
);

tenant_invitations

CREATE TABLE tenant_invitations (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    invitation_token VARCHAR(255) UNIQUE NOT NULL,
    email VARCHAR(255) NOT NULL,
    role VARCHAR(50) NOT NULL,
    invited_by UUID NOT NULL,

    status VARCHAR(50) DEFAULT 'pending',        -- pending, accepted, expired, cancelled
    expires_at TIMESTAMP NOT NULL,
    accepted_at TIMESTAMP,
    accepted_by UUID,

    created_at TIMESTAMP DEFAULT NOW()
);

tenant_usage

CREATE TABLE tenant_usage (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    usage_date DATE NOT NULL,

    -- Usage metrics
    api_calls INTEGER DEFAULT 0,
    transactions_count INTEGER DEFAULT 0,
    storage_mb DECIMAL(10, 2) DEFAULT 0.00,
    whatsapp_messages INTEGER DEFAULT 0,
    sms_messages INTEGER DEFAULT 0,
    emails_sent INTEGER DEFAULT 0,

    -- Costs
    estimated_cost DECIMAL(10, 4) DEFAULT 0.0000,

    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(tenant_id, usage_date)
);

tenant_invoices

CREATE TABLE tenant_invoices (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    stripe_invoice_id VARCHAR(255) UNIQUE,

    -- Invoice details
    invoice_number VARCHAR(100),
    invoice_date DATE NOT NULL,
    due_date DATE,
    period_start DATE,
    period_end DATE,

    -- Amounts
    subtotal DECIMAL(10, 2) NOT NULL,
    tax_amount DECIMAL(10, 2) DEFAULT 0.00,
    total_amount DECIMAL(10, 2) NOT NULL,
    amount_paid DECIMAL(10, 2) DEFAULT 0.00,
    amount_due DECIMAL(10, 2) NOT NULL,
    currency VARCHAR(10) DEFAULT 'EUR',

    -- Status
    status VARCHAR(50) NOT NULL,                 -- draft, open, paid, void, uncollectible
    paid_at TIMESTAMP,

    -- PDF
    invoice_pdf_url VARCHAR(500),

    created_at TIMESTAMP DEFAULT NOW()
);

tenant_payment_methods

CREATE TABLE tenant_payment_methods (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    stripe_payment_method_id VARCHAR(255) UNIQUE,

    payment_method_type VARCHAR(50),             -- card, sepa_debit

    -- Card details (if card)
    card_brand VARCHAR(50),
    card_last_four VARCHAR(4),
    card_exp_month INTEGER,
    card_exp_year INTEGER,

    -- SEPA details (if sepa_debit)
    sepa_last_four VARCHAR(4),
    sepa_bank_code VARCHAR(50),
    sepa_country VARCHAR(10),

    is_default BOOLEAN DEFAULT FALSE,

    created_at TIMESTAMP DEFAULT NOW()
);

tenant_audit_log

CREATE TABLE tenant_audit_log (
    id UUID PRIMARY KEY,
    tenant_id UUID REFERENCES tenants(id) ON DELETE CASCADE,
    user_id UUID,
    action VARCHAR(100) NOT NULL,                -- tenant_created, subscription_upgraded, member_invited, etc.
    resource_type VARCHAR(100),
    resource_id VARCHAR(255),
    details JSONB,
    ip_address INET,
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT NOW(),
    INDEX idx_audit_tenant_date (tenant_id, created_at DESC)
);

Indexes for Performance

CREATE INDEX idx_tenants_status ON tenants(status);
CREATE INDEX idx_tenants_subscription_tier ON tenants(subscription_tier);
CREATE INDEX idx_subscriptions_stripe ON tenant_subscriptions(stripe_subscription_id);
CREATE INDEX idx_subscriptions_status ON tenant_subscriptions(tenant_id, status);
CREATE INDEX idx_members_tenant ON tenant_members(tenant_id);
CREATE INDEX idx_members_user ON tenant_members(user_id);
CREATE INDEX idx_invitations_token ON tenant_invitations(invitation_token);
CREATE INDEX idx_usage_tenant_date ON tenant_usage(tenant_id, usage_date DESC);
CREATE INDEX idx_invoices_tenant ON tenant_invoices(tenant_id, invoice_date DESC);

Business Logic Examples

Tenant Registration with Stripe

async def create_tenant_with_subscription(
    tenant_data: TenantCreate,
    plan_tier: str = 'pro',
    payment_method_id: str = None
) -> Tenant:
    """
    Create new tenant and Stripe subscription.
    """
    import stripe
    stripe.api_key = os.getenv('STRIPE_SECRET_KEY')

    # Create tenant
    tenant = Tenant(
        tenant_name=tenant_data.tenant_name,
        business_legal_name=tenant_data.business_legal_name,
        tax_id=tenant_data.tax_id,
        email=tenant_data.email,
        phone=tenant_data.phone,
        country='España',
        status='trial' if not payment_method_id else 'active',
        subscription_tier=plan_tier,
        trial_ends_at=datetime.utcnow() + timedelta(days=14) if not payment_method_id else None
    )

    # Set tier limits
    if plan_tier == 'free':
        tenant.max_locations = 1
        tenant.max_users = 1
        tenant.max_transactions_per_month = 100
    elif plan_tier == 'pro':
        tenant.max_locations = 3
        tenant.max_users = 5
        tenant.max_transactions_per_month = -1  # Unlimited
    elif plan_tier == 'enterprise':
        tenant.max_locations = -1  # Unlimited
        tenant.max_users = -1  # Unlimited
        tenant.max_transactions_per_month = -1  # Unlimited

    db.add(tenant)
    await db.flush()

    try:
        # Create Stripe customer
        stripe_customer = stripe.Customer.create(
            email=tenant.email,
            name=tenant.business_legal_name or tenant.tenant_name,
            metadata={
                'tenant_id': str(tenant.id),
                'tax_id': tenant.tax_id
            },
            tax_id_data=[{
                'type': 'eu_vat',  # Spanish NIF/CIF
                'value': tenant.tax_id
            }] if tenant.tax_id else None
        )

        tenant.stripe_customer_id = stripe_customer.id

        # Attach payment method if provided
        if payment_method_id:
            stripe.PaymentMethod.attach(
                payment_method_id,
                customer=stripe_customer.id
            )

            # Set as default
            stripe.Customer.modify(
                stripe_customer.id,
                invoice_settings={'default_payment_method': payment_method_id}
            )

        # Get price ID for plan
        price_id = get_stripe_price_id(plan_tier, 'month')

        # Create subscription
        subscription_params = {
            'customer': stripe_customer.id,
            'items': [{'price': price_id}],
            'metadata': {'tenant_id': str(tenant.id)},
            'payment_behavior': 'default_incomplete' if not payment_method_id else 'allow_incomplete'
        }

        # Add trial if no payment method
        if not payment_method_id:
            subscription_params['trial_period_days'] = 14

        stripe_subscription = stripe.Subscription.create(**subscription_params)

        tenant.stripe_subscription_id = stripe_subscription.id

        # Create subscription record
        subscription = TenantSubscription(
            tenant_id=tenant.id,
            stripe_subscription_id=stripe_subscription.id,
            stripe_customer_id=stripe_customer.id,
            plan_tier=plan_tier,
            plan_interval='month',
            plan_amount=get_plan_amount(plan_tier),
            status=stripe_subscription.status,
            trial_start=datetime.fromtimestamp(stripe_subscription.trial_start) if stripe_subscription.trial_start else None,
            trial_end=datetime.fromtimestamp(stripe_subscription.trial_end) if stripe_subscription.trial_end else None,
            current_period_start=datetime.fromtimestamp(stripe_subscription.current_period_start),
            current_period_end=datetime.fromtimestamp(stripe_subscription.current_period_end)
        )
        db.add(subscription)

        # Create owner member record
        owner_user = await create_user_from_tenant(tenant_data)

        member = TenantMember(
            tenant_id=tenant.id,
            user_id=owner_user.id,
            role='owner',
            status='active'
        )
        db.add(member)

        # Log audit
        audit = TenantAuditLog(
            tenant_id=tenant.id,
            user_id=owner_user.id,
            action='tenant_created',
            details={
                'plan_tier': plan_tier,
                'trial': not bool(payment_method_id)
            }
        )
        db.add(audit)

        await db.commit()

        # Publish event
        await publish_event('tenants', 'tenant.created', {
            'tenant_id': str(tenant.id),
            'plan_tier': plan_tier,
            'trial': not bool(payment_method_id)
        })

        logger.info("Tenant created with subscription",
                   tenant_id=str(tenant.id),
                   plan_tier=plan_tier)

        return tenant

    except stripe.error.StripeError as e:
        # Rollback tenant creation
        await db.rollback()

        logger.error("Stripe subscription creation failed",
                    tenant_id=str(tenant.id) if tenant.id else None,
                    error=str(e))

        raise Exception(f"Payment processing failed: {str(e)}")

def get_stripe_price_id(plan_tier: str, interval: str) -> str:
    """
    Get Stripe price ID for plan tier and billing interval.
    """
    # These would be created in Stripe dashboard
    price_ids = {
        ('pro', 'month'): 'price_pro_monthly',
        ('pro', 'year'): 'price_pro_yearly',
        ('enterprise', 'month'): 'price_enterprise_monthly',
        ('enterprise', 'year'): 'price_enterprise_yearly'
    }
    return price_ids.get((plan_tier, interval))

def get_plan_amount(plan_tier: str) -> Decimal:
    """
    Get plan amount in euros.
    """
    amounts = {
        'free': Decimal('0.00'),
        'pro': Decimal('49.00'),
        'enterprise': Decimal('149.00')
    }
    return amounts.get(plan_tier, Decimal('0.00'))

Subscription Upgrade/Downgrade

async def update_subscription(
    tenant_id: UUID,
    new_plan_tier: str,
    user_id: UUID
) -> TenantSubscription:
    """
    Upgrade or downgrade tenant subscription.
    """
    import stripe
    stripe.api_key = os.getenv('STRIPE_SECRET_KEY')

    tenant = await db.get(Tenant, tenant_id)
    subscription = await db.query(TenantSubscription).filter(
        TenantSubscription.tenant_id == tenant_id,
        TenantSubscription.status == 'active'
    ).first()

    if not subscription:
        raise ValueError("No active subscription found")

    try:
        # Get new price
        new_price_id = get_stripe_price_id(new_plan_tier, subscription.plan_interval)

        # Update Stripe subscription
        stripe_subscription = stripe.Subscription.retrieve(subscription.stripe_subscription_id)

        # Update subscription items (Stripe handles proration automatically)
        stripe_subscription = stripe.Subscription.modify(
            subscription.stripe_subscription_id,
            items=[{
                'id': stripe_subscription['items']['data'][0].id,
                'price': new_price_id
            }],
            proration_behavior='always_invoice',  # Create invoice for proration
            metadata={'tenant_id': str(tenant_id)}
        )

        # Update tenant tier
        old_tier = tenant.subscription_tier
        tenant.subscription_tier = new_plan_tier

        # Update limits
        if new_plan_tier == 'free':
            tenant.max_locations = 1
            tenant.max_users = 1
            tenant.max_transactions_per_month = 100
        elif new_plan_tier == 'pro':
            tenant.max_locations = 3
            tenant.max_users = 5
            tenant.max_transactions_per_month = -1
        elif new_plan_tier == 'enterprise':
            tenant.max_locations = -1
            tenant.max_users = -1
            tenant.max_transactions_per_month = -1

        # Update subscription record
        subscription.plan_tier = new_plan_tier
        subscription.plan_amount = get_plan_amount(new_plan_tier)
        subscription.status = stripe_subscription.status

        # Log audit
        audit = TenantAuditLog(
            tenant_id=tenant_id,
            user_id=user_id,
            action='subscription_changed',
            details={
                'old_tier': old_tier,
                'new_tier': new_plan_tier,
                'change_type': 'upgrade' if get_plan_amount(new_plan_tier) > get_plan_amount(old_tier) else 'downgrade'
            }
        )
        db.add(audit)

        await db.commit()

        # Publish event
        await publish_event('tenants', 'tenant.subscription_changed', {
            'tenant_id': str(tenant_id),
            'old_tier': old_tier,
            'new_tier': new_plan_tier
        })

        logger.info("Subscription updated",
                   tenant_id=str(tenant_id),
                   old_tier=old_tier,
                   new_tier=new_plan_tier)

        return subscription

    except stripe.error.StripeError as e:
        logger.error("Subscription update failed",
                    tenant_id=str(tenant_id),
                    error=str(e))
        raise Exception(f"Subscription update failed: {str(e)}")

Stripe Webhook Handler

async def handle_stripe_webhook(payload: bytes, sig_header: str):
    """
    Handle Stripe webhook events.
    """
    import stripe
    stripe.api_key = os.getenv('STRIPE_SECRET_KEY')
    webhook_secret = os.getenv('STRIPE_WEBHOOK_SECRET')

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, webhook_secret
        )
    except ValueError:
        raise Exception("Invalid payload")
    except stripe.error.SignatureVerificationError:
        raise Exception("Invalid signature")

    # Handle event types
    if event['type'] == 'customer.subscription.updated':
        subscription = event['data']['object']
        await handle_subscription_updated(subscription)

    elif event['type'] == 'customer.subscription.deleted':
        subscription = event['data']['object']
        await handle_subscription_cancelled(subscription)

    elif event['type'] == 'invoice.paid':
        invoice = event['data']['object']
        await handle_invoice_paid(invoice)

    elif event['type'] == 'invoice.payment_failed':
        invoice = event['data']['object']
        await handle_payment_failed(invoice)

    elif event['type'] == 'customer.subscription.trial_will_end':
        subscription = event['data']['object']
        await handle_trial_ending(subscription)

    logger.info("Stripe webhook processed",
               event_type=event['type'],
               event_id=event['id'])

async def handle_subscription_updated(stripe_subscription: dict):
    """
    Handle subscription update from Stripe.
    """
    tenant_id = UUID(stripe_subscription['metadata'].get('tenant_id'))

    subscription = await db.query(TenantSubscription).filter(
        TenantSubscription.stripe_subscription_id == stripe_subscription['id']
    ).first()

    if subscription:
        subscription.status = stripe_subscription['status']
        subscription.current_period_start = datetime.fromtimestamp(stripe_subscription['current_period_start'])
        subscription.current_period_end = datetime.fromtimestamp(stripe_subscription['current_period_end'])
        subscription.cancel_at_period_end = stripe_subscription['cancel_at_period_end']

        await db.commit()

async def handle_payment_failed(stripe_invoice: dict):
    """
    Handle failed payment from Stripe.
    """
    customer_id = stripe_invoice['customer']

    tenant = await db.query(Tenant).filter(
        Tenant.stripe_customer_id == customer_id
    ).first()

    if tenant:
        # Send notification
        await send_payment_failed_notification(tenant.id)

        # If 3rd failed attempt, suspend account
        failed_attempts = await get_failed_payment_count(tenant.id)
        if failed_attempts >= 3:
            tenant.status = 'suspended'
            tenant.suspended_at = datetime.utcnow()
            tenant.suspended_reason = 'payment_failed'
            await db.commit()

            await send_account_suspended_notification(tenant.id)

Events & Messaging

Published Events (RabbitMQ)

Exchange: tenants Routing Keys: tenant.created, tenant.subscription_changed, tenant.cancelled

Tenant Created Event

{
    "event_type": "tenant_created",
    "tenant_id": "uuid",
    "tenant_name": "Panadería García",
    "plan_tier": "pro",
    "trial": false,
    "timestamp": "2025-11-06T10:00:00Z"
}

Subscription Changed Event

{
    "event_type": "tenant_subscription_changed",
    "tenant_id": "uuid",
    "old_tier": "pro",
    "new_tier": "enterprise",
    "change_type": "upgrade",
    "timestamp": "2025-11-06T14:00:00Z"
}

Custom Metrics (Prometheus)

# Tenant metrics
tenants_total = Gauge(
    'tenants_total',
    'Total tenants',
    ['status', 'subscription_tier']
)

monthly_recurring_revenue_euros = Gauge(
    'monthly_recurring_revenue_euros',
    'Total MRR',
    []
)

churn_rate_percentage = Gauge(
    'churn_rate_percentage_monthly',
    'Monthly churn rate',
    []
)

trial_conversion_rate = Gauge(
    'trial_conversion_rate_percentage',
    'Trial to paid conversion rate',
    []
)

Configuration

Environment Variables

Service Configuration:

  • PORT - Service port (default: 8017)
  • DATABASE_URL - PostgreSQL connection string
  • REDIS_URL - Redis connection string
  • RABBITMQ_URL - RabbitMQ connection string

Stripe Configuration:

  • STRIPE_SECRET_KEY - Stripe secret key
  • STRIPE_PUBLISHABLE_KEY - Stripe publishable key
  • STRIPE_WEBHOOK_SECRET - Stripe webhook signing secret
  • STRIPE_PRICE_PRO_MONTHLY - Pro plan monthly price ID
  • STRIPE_PRICE_ENTERPRISE_MONTHLY - Enterprise plan monthly price ID

Trial Configuration:

  • DEFAULT_TRIAL_DAYS - Free trial length (default: 14)
  • TRIAL_REMINDER_DAYS - Days before trial ends to remind (default: 3)

Development Setup

Prerequisites

  • Python 3.11+
  • PostgreSQL 17
  • Redis 7.4
  • RabbitMQ 4.1
  • Stripe account

Local Development

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

pip install -r requirements.txt

export DATABASE_URL=postgresql://user:pass@localhost:5432/tenant
export REDIS_URL=redis://localhost:6379/0
export RABBITMQ_URL=amqp://guest:guest@localhost:5672/
export STRIPE_SECRET_KEY=sk_test_your_key
export STRIPE_WEBHOOK_SECRET=whsec_your_secret

alembic upgrade head
python main.py

Integration Points

Dependencies

  • Stripe API - Payment processing
  • Auth Service - User management
  • PostgreSQL - Tenant data
  • Redis - Subscription caching
  • RabbitMQ - Event publishing

Dependents

  • All Services - Tenant authentication and limits
  • Frontend Dashboard - Subscription management UI
  • Billing - Invoice generation

Business Value for VUE Madrid

Problem Statement

Manual billing and customer management doesn't scale:

  • Manual invoicing time-consuming and error-prone
  • No automated subscription management
  • Difficult to track MRR and churn
  • Complex European payment regulations (SEPA, VAT)
  • No self-service tier changes

Solution

Bakery-IA Tenant Service provides:

  • Automated Billing: Stripe handles everything
  • European Payments: SEPA Direct Debit for Spanish/EU
  • Self-Service: Customers manage subscriptions
  • Revenue Visibility: Real-time MRR, churn, LTV
  • Scalable: Handle thousands of customers

Quantifiable Impact

Revenue Model:

  • €0-149/month per customer (€66 average)
  • €60K/year at 100 customers
  • €360K/year at 500 customers
  • €1.8M/year at 2,000 customers

Business Metrics:

  • 30-40% customers upgrade within 6 months
  • 14-day trial → 35-45% conversion rate (industry standard)
  • <10% monthly churn (target)
  • €1,200-3,600 customer LTV (24-month retention)

Operational Efficiency:

  • 100% automated billing (zero manual invoicing)
  • 95%+ payment success rate (Stripe)
  • Self-service reduces support by 70%

Target Market Fit (Spanish Bakeries)

  • SEPA Support: Direct debit popular in Spain/EU
  • Spanish Invoicing: Tax-compliant invoices automatic
  • Euro Currency: Native euro pricing, no conversion
  • Affordable: €49/month accessible for SMBs
  • Transparent: Clear pricing, no hidden fees

ROI for Platform

Investment: Stripe fees 1.4% + €0.25/transaction (EU cards) Customer Acquisition Cost: €200-300 Payback Period: 3-5 months (at €66 average MRR) Annual Value: €66 × 12 = €792/customer/year 3-Year LTV: €2,376/customer (assuming retention)


Copyright © 2025 Bakery-IA. All rights reserved.