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>
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 detailsPUT /api/v1/tenants/{tenant_id}- Update tenantDELETE /api/v1/tenants/{tenant_id}- Delete tenant (GDPR)GET /api/v1/tenants/{tenant_id}/settings- Get settingsPUT /api/v1/tenants/{tenant_id}/settings- Update settings
Subscription Management
GET /api/v1/tenants/{tenant_id}/subscription- Get subscriptionPOST /api/v1/tenants/{tenant_id}/subscription- Create subscriptionPUT /api/v1/tenants/{tenant_id}/subscription- Update subscription (upgrade/downgrade)DELETE /api/v1/tenants/{tenant_id}/subscription- Cancel subscriptionPOST /api/v1/tenants/{tenant_id}/subscription/reactivate- Reactivate cancelled subscription
Payment Methods
GET /api/v1/tenants/{tenant_id}/payment-methods- List payment methodsPOST /api/v1/tenants/{tenant_id}/payment-methods- Add payment methodPUT /api/v1/tenants/{tenant_id}/payment-methods/{pm_id}/default- Set defaultDELETE /api/v1/tenants/{tenant_id}/payment-methods/{pm_id}- Remove payment method
Team Management
GET /api/v1/tenants/{tenant_id}/members- List team membersPOST /api/v1/tenants/{tenant_id}/members/invite- Invite team memberPUT /api/v1/tenants/{tenant_id}/members/{member_id}- Update member roleDELETE /api/v1/tenants/{tenant_id}/members/{member_id}- Remove memberGET /api/v1/tenants/invitations/{invitation_token}- Get invitation detailsPOST /api/v1/tenants/invitations/{invitation_token}/accept- Accept invitation
Billing & Usage
GET /api/v1/tenants/{tenant_id}/invoices- List invoicesGET /api/v1/tenants/{tenant_id}/invoices/{invoice_id}- Get invoiceGET /api/v1/tenants/{tenant_id}/usage- Current usage statisticsGET /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 revenueGET /api/v1/tenants/analytics/churn- Churn rateGET /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 stringREDIS_URL- Redis connection stringRABBITMQ_URL- RabbitMQ connection string
Stripe Configuration:
STRIPE_SECRET_KEY- Stripe secret keySTRIPE_PUBLISHABLE_KEY- Stripe publishable keySTRIPE_WEBHOOK_SECRET- Stripe webhook signing secretSTRIPE_PRICE_PRO_MONTHLY- Pro plan monthly price IDSTRIPE_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.