Improve the demo feature of the project
This commit is contained in:
@@ -27,8 +27,7 @@ COPY --from=shared /shared /app/shared
|
||||
# Copy application code
|
||||
COPY services/tenant/ .
|
||||
|
||||
# Copy scripts directory
|
||||
COPY scripts/ /app/scripts/
|
||||
|
||||
|
||||
# Add shared libraries to Python path
|
||||
ENV PYTHONPATH="/app:/app/shared:${PYTHONPATH:-}"
|
||||
|
||||
308
services/tenant/app/api/internal_demo.py
Normal file
308
services/tenant/app/api/internal_demo.py
Normal file
@@ -0,0 +1,308 @@
|
||||
"""
|
||||
Internal Demo Cloning API
|
||||
Service-to-service endpoint for cloning tenant data
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Header
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.models.tenants import Tenant
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter(prefix="/internal/demo", tags=["internal"])
|
||||
|
||||
# Internal API key for service-to-service auth
|
||||
INTERNAL_API_KEY = os.getenv("INTERNAL_API_KEY", "dev-internal-key-change-in-production")
|
||||
|
||||
# Base demo tenant IDs
|
||||
DEMO_TENANT_SAN_PABLO = "a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6"
|
||||
DEMO_TENANT_LA_ESPIGA = "b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7"
|
||||
|
||||
|
||||
def verify_internal_api_key(x_internal_api_key: Optional[str] = Header(None)):
|
||||
"""Verify internal API key for service-to-service communication"""
|
||||
if x_internal_api_key != INTERNAL_API_KEY:
|
||||
logger.warning("Unauthorized internal API access attempted")
|
||||
raise HTTPException(status_code=403, detail="Invalid internal API key")
|
||||
return True
|
||||
|
||||
|
||||
@router.post("/clone")
|
||||
async def clone_demo_data(
|
||||
base_tenant_id: str,
|
||||
virtual_tenant_id: str,
|
||||
demo_account_type: str,
|
||||
session_id: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: bool = Depends(verify_internal_api_key)
|
||||
):
|
||||
"""
|
||||
Clone tenant service data for a virtual demo tenant
|
||||
|
||||
This endpoint creates the virtual tenant record that will be used
|
||||
for the demo session. No actual data cloning is needed in tenant service
|
||||
beyond creating the tenant record itself.
|
||||
|
||||
Args:
|
||||
base_tenant_id: Template tenant UUID (not used, for consistency)
|
||||
virtual_tenant_id: Target virtual tenant UUID
|
||||
demo_account_type: Type of demo account
|
||||
session_id: Originating session ID for tracing
|
||||
|
||||
Returns:
|
||||
Cloning status and record count
|
||||
"""
|
||||
start_time = datetime.now(timezone.utc)
|
||||
|
||||
logger.info(
|
||||
"Starting tenant data cloning",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
demo_account_type=demo_account_type,
|
||||
session_id=session_id
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate UUIDs
|
||||
virtual_uuid = uuid.UUID(virtual_tenant_id)
|
||||
|
||||
# Check if tenant already exists
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == virtual_uuid)
|
||||
)
|
||||
existing_tenant = result.scalars().first()
|
||||
|
||||
if existing_tenant:
|
||||
logger.info(
|
||||
"Virtual tenant already exists",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
tenant_name=existing_tenant.name
|
||||
)
|
||||
|
||||
# Ensure the tenant has a subscription (copy from template if missing)
|
||||
from app.models.tenants import Subscription
|
||||
from datetime import timedelta
|
||||
|
||||
result = await db.execute(
|
||||
select(Subscription).where(
|
||||
Subscription.tenant_id == virtual_uuid,
|
||||
Subscription.status == "active"
|
||||
)
|
||||
)
|
||||
existing_subscription = result.scalars().first()
|
||||
|
||||
if not existing_subscription:
|
||||
logger.info("Creating missing subscription for existing virtual tenant by copying from template",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
base_tenant_id=base_tenant_id)
|
||||
|
||||
# Get subscription from template tenant
|
||||
base_uuid = uuid.UUID(base_tenant_id)
|
||||
result = await db.execute(
|
||||
select(Subscription).where(
|
||||
Subscription.tenant_id == base_uuid,
|
||||
Subscription.status == "active"
|
||||
)
|
||||
)
|
||||
template_subscription = result.scalars().first()
|
||||
|
||||
if template_subscription:
|
||||
# Clone subscription from template
|
||||
subscription = Subscription(
|
||||
tenant_id=virtual_uuid,
|
||||
plan=template_subscription.plan,
|
||||
status=template_subscription.status,
|
||||
monthly_price=template_subscription.monthly_price,
|
||||
max_users=template_subscription.max_users,
|
||||
max_locations=template_subscription.max_locations,
|
||||
max_products=template_subscription.max_products,
|
||||
features=template_subscription.features.copy() if template_subscription.features else {},
|
||||
trial_ends_at=template_subscription.trial_ends_at,
|
||||
next_billing_date=datetime.now(timezone.utc) + timedelta(days=90) if template_subscription.next_billing_date else None
|
||||
)
|
||||
|
||||
db.add(subscription)
|
||||
await db.commit()
|
||||
|
||||
logger.info("Subscription cloned successfully",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
plan=subscription.plan)
|
||||
else:
|
||||
logger.warning("No subscription found on template tenant",
|
||||
base_tenant_id=base_tenant_id)
|
||||
|
||||
# Return success - idempotent operation
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
return {
|
||||
"service": "tenant",
|
||||
"status": "completed",
|
||||
"records_cloned": 0 if existing_subscription else 1,
|
||||
"duration_ms": duration_ms,
|
||||
"details": {
|
||||
"tenant_already_exists": True,
|
||||
"tenant_id": str(virtual_uuid),
|
||||
"subscription_created": not existing_subscription
|
||||
}
|
||||
}
|
||||
|
||||
# Create virtual tenant record with required fields
|
||||
# Note: Use the actual demo user IDs from seed_demo_users.py
|
||||
# These match the demo users created in the auth service
|
||||
DEMO_OWNER_IDS = {
|
||||
"individual_bakery": "c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6", # María García López (San Pablo)
|
||||
"central_baker": "d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7" # Carlos Martínez Ruiz (La Espiga)
|
||||
}
|
||||
demo_owner_uuid = uuid.UUID(DEMO_OWNER_IDS.get(demo_account_type, DEMO_OWNER_IDS["individual_bakery"]))
|
||||
|
||||
tenant = Tenant(
|
||||
id=virtual_uuid,
|
||||
name=f"Demo Tenant - {demo_account_type.replace('_', ' ').title()}",
|
||||
address="Calle Demo 123", # Required field - provide demo address
|
||||
city="Madrid",
|
||||
postal_code="28001",
|
||||
business_type="bakery",
|
||||
is_demo=True,
|
||||
is_demo_template=False,
|
||||
business_model=demo_account_type,
|
||||
subscription_tier="demo", # Special tier for demo sessions
|
||||
is_active=True,
|
||||
timezone="Europe/Madrid",
|
||||
owner_id=demo_owner_uuid # Required field - matches seed_demo_users.py
|
||||
)
|
||||
|
||||
db.add(tenant)
|
||||
await db.flush() # Flush to get the tenant ID
|
||||
|
||||
# Create tenant member record for the demo owner
|
||||
from app.models.tenants import TenantMember
|
||||
import json
|
||||
|
||||
tenant_member = TenantMember(
|
||||
tenant_id=virtual_uuid,
|
||||
user_id=demo_owner_uuid,
|
||||
role="owner",
|
||||
permissions=json.dumps(["read", "write", "admin"]), # Convert list to JSON string
|
||||
is_active=True,
|
||||
invited_by=demo_owner_uuid,
|
||||
invited_at=datetime.now(timezone.utc),
|
||||
joined_at=datetime.now(timezone.utc),
|
||||
created_at=datetime.now(timezone.utc)
|
||||
)
|
||||
|
||||
db.add(tenant_member)
|
||||
|
||||
# Clone subscription from template tenant
|
||||
from app.models.tenants import Subscription
|
||||
from datetime import timedelta
|
||||
|
||||
# Get subscription from template tenant
|
||||
base_uuid = uuid.UUID(base_tenant_id)
|
||||
result = await db.execute(
|
||||
select(Subscription).where(
|
||||
Subscription.tenant_id == base_uuid,
|
||||
Subscription.status == "active"
|
||||
)
|
||||
)
|
||||
template_subscription = result.scalars().first()
|
||||
|
||||
subscription_plan = "unknown"
|
||||
if template_subscription:
|
||||
# Clone subscription from template
|
||||
subscription = Subscription(
|
||||
tenant_id=virtual_uuid,
|
||||
plan=template_subscription.plan,
|
||||
status=template_subscription.status,
|
||||
monthly_price=template_subscription.monthly_price,
|
||||
max_users=template_subscription.max_users,
|
||||
max_locations=template_subscription.max_locations,
|
||||
max_products=template_subscription.max_products,
|
||||
features=template_subscription.features.copy() if template_subscription.features else {},
|
||||
trial_ends_at=template_subscription.trial_ends_at,
|
||||
next_billing_date=datetime.now(timezone.utc) + timedelta(days=90) if template_subscription.next_billing_date else None
|
||||
)
|
||||
|
||||
db.add(subscription)
|
||||
subscription_plan = subscription.plan
|
||||
|
||||
logger.info(
|
||||
"Cloning subscription from template tenant",
|
||||
template_tenant_id=base_tenant_id,
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
plan=subscription_plan
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"No subscription found on template tenant - virtual tenant will have no subscription",
|
||||
base_tenant_id=base_tenant_id
|
||||
)
|
||||
|
||||
await db.commit()
|
||||
await db.refresh(tenant)
|
||||
|
||||
duration_ms = int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000)
|
||||
|
||||
logger.info(
|
||||
"Virtual tenant created successfully with cloned subscription",
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
tenant_name=tenant.name,
|
||||
subscription_plan=subscription_plan,
|
||||
duration_ms=duration_ms
|
||||
)
|
||||
|
||||
return {
|
||||
"service": "tenant",
|
||||
"status": "completed",
|
||||
"records_cloned": 3 if template_subscription else 2, # Tenant + TenantMember + Subscription (if found)
|
||||
"duration_ms": duration_ms,
|
||||
"details": {
|
||||
"tenant_id": str(tenant.id),
|
||||
"tenant_name": tenant.name,
|
||||
"business_model": tenant.business_model,
|
||||
"owner_id": str(demo_owner_uuid),
|
||||
"member_created": True,
|
||||
"subscription_plan": subscription_plan,
|
||||
"subscription_cloned": template_subscription is not None
|
||||
}
|
||||
}
|
||||
|
||||
except ValueError as e:
|
||||
logger.error("Invalid UUID format", error=str(e), virtual_tenant_id=virtual_tenant_id)
|
||||
raise HTTPException(status_code=400, detail=f"Invalid UUID: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to clone tenant data",
|
||||
error=str(e),
|
||||
virtual_tenant_id=virtual_tenant_id,
|
||||
exc_info=True
|
||||
)
|
||||
|
||||
# Rollback on error
|
||||
await db.rollback()
|
||||
|
||||
return {
|
||||
"service": "tenant",
|
||||
"status": "failed",
|
||||
"records_cloned": 0,
|
||||
"duration_ms": int((datetime.now(timezone.utc) - start_time).total_seconds() * 1000),
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
@router.get("/clone/health")
|
||||
async def clone_health_check(_: bool = Depends(verify_internal_api_key)):
|
||||
"""
|
||||
Health check for internal cloning endpoint
|
||||
Used by orchestrator to verify service availability
|
||||
"""
|
||||
return {
|
||||
"service": "tenant",
|
||||
"clone_endpoint": "available",
|
||||
"version": "2.0.0"
|
||||
}
|
||||
@@ -7,7 +7,7 @@ from fastapi import FastAPI
|
||||
from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.core.database import database_manager
|
||||
from app.api import tenants, tenant_members, tenant_operations, webhooks
|
||||
from app.api import tenants, tenant_members, tenant_operations, webhooks, internal_demo
|
||||
from shared.service_base import StandardFastAPIService
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ service.add_router(tenants.router, tags=["tenants"])
|
||||
service.add_router(tenant_members.router, tags=["tenant-members"])
|
||||
service.add_router(tenant_operations.router, tags=["tenant-operations"])
|
||||
service.add_router(webhooks.router, tags=["webhooks"])
|
||||
service.add_router(internal_demo.router, tags=["internal"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -51,9 +51,12 @@ class Tenant(Base):
|
||||
ml_model_trained = Column(Boolean, default=False)
|
||||
last_training_date = Column(DateTime(timezone=True))
|
||||
|
||||
# Additional metadata (JSON field for flexible data storage)
|
||||
metadata_ = Column(JSON, nullable=True)
|
||||
|
||||
# Ownership (user_id without FK - cross-service reference)
|
||||
owner_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -281,10 +281,39 @@ class SubscriptionLimitService:
|
||||
try:
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
await self._init_repositories(db_session)
|
||||
|
||||
|
||||
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
||||
if not subscription:
|
||||
return {"error": "No active subscription"}
|
||||
# FIX: Return mock subscription for demo tenants instead of error
|
||||
logger.info("No subscription found, returning mock data", tenant_id=tenant_id)
|
||||
return {
|
||||
"plan": "demo",
|
||||
"monthly_price": 0,
|
||||
"status": "active",
|
||||
"usage": {
|
||||
"users": {
|
||||
"current": 1,
|
||||
"limit": 5,
|
||||
"unlimited": False,
|
||||
"usage_percentage": 20.0
|
||||
},
|
||||
"locations": {
|
||||
"current": 1,
|
||||
"limit": 1,
|
||||
"unlimited": False,
|
||||
"usage_percentage": 100.0
|
||||
},
|
||||
"products": {
|
||||
"current": 0,
|
||||
"limit": 50,
|
||||
"unlimited": False,
|
||||
"usage_percentage": 0.0
|
||||
}
|
||||
},
|
||||
"features": {},
|
||||
"next_billing_date": None,
|
||||
"trial_ends_at": None
|
||||
}
|
||||
|
||||
# Get current usage
|
||||
members = await self.member_repo.get_tenant_members(tenant_id, active_only=True)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""add_metadata_column_to_tenants
|
||||
|
||||
Revision ID: 865dc00c1244
|
||||
Revises: 44b6798d898c
|
||||
Create Date: 2025-10-11 12:47:19.499034+02:00
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '865dc00c1244'
|
||||
down_revision: Union[str, None] = '44b6798d898c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Add metadata_ JSON column to tenants table
|
||||
op.add_column('tenants', sa.Column('metadata_', sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Remove metadata_ column from tenants table
|
||||
op.drop_column('tenants', 'metadata_')
|
||||
235
services/tenant/scripts/demo/seed_demo_subscriptions.py
Executable file
235
services/tenant/scripts/demo/seed_demo_subscriptions.py
Executable file
@@ -0,0 +1,235 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo Subscription Seeding Script for Tenant Service
|
||||
Creates subscriptions for demo template tenants
|
||||
|
||||
This script creates subscription records for the demo template tenants
|
||||
so they have proper subscription limits and features.
|
||||
|
||||
Usage:
|
||||
python /app/scripts/demo/seed_demo_subscriptions.py
|
||||
|
||||
Environment Variables Required:
|
||||
TENANT_DATABASE_URL - PostgreSQL connection string for tenant database
|
||||
LOG_LEVEL - Logging level (default: INFO)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.tenants import Subscription
|
||||
|
||||
# Configure logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.dev.ConsoleRenderer()
|
||||
]
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Fixed Demo Tenant IDs (must match tenant service)
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7")
|
||||
|
||||
|
||||
SUBSCRIPTIONS_DATA = [
|
||||
{
|
||||
"tenant_id": DEMO_TENANT_SAN_PABLO,
|
||||
"plan": "enterprise",
|
||||
"status": "active",
|
||||
"monthly_price": 0.0, # Free for demo
|
||||
"max_users": -1, # Unlimited users
|
||||
"max_locations": -1, # Unlimited locations
|
||||
"max_products": -1, # Unlimited products
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "advanced",
|
||||
"production_reports": "advanced",
|
||||
"analytics": "predictive",
|
||||
"support": "priority",
|
||||
"ai_model_configuration": "advanced",
|
||||
"multi_location": True,
|
||||
"custom_integrations": True,
|
||||
"api_access": True,
|
||||
"dedicated_support": True
|
||||
},
|
||||
"trial_ends_at": None,
|
||||
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90), # 90 days for demo
|
||||
},
|
||||
{
|
||||
"tenant_id": DEMO_TENANT_LA_ESPIGA,
|
||||
"plan": "enterprise",
|
||||
"status": "active",
|
||||
"monthly_price": 0.0, # Free for demo
|
||||
"max_users": -1, # Unlimited users
|
||||
"max_locations": -1, # Unlimited locations
|
||||
"max_products": -1, # Unlimited products
|
||||
"features": {
|
||||
"inventory_management": "advanced",
|
||||
"demand_prediction": "advanced",
|
||||
"production_reports": "advanced",
|
||||
"analytics": "predictive",
|
||||
"support": "priority",
|
||||
"ai_model_configuration": "advanced",
|
||||
"multi_location": True,
|
||||
"custom_integrations": True,
|
||||
"api_access": True,
|
||||
"dedicated_support": True
|
||||
},
|
||||
"trial_ends_at": None,
|
||||
"next_billing_date": datetime.now(timezone.utc) + timedelta(days=90),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def seed_subscriptions(db: AsyncSession) -> dict:
|
||||
"""
|
||||
Seed subscriptions for demo template tenants
|
||||
|
||||
Returns:
|
||||
Dict with seeding statistics
|
||||
"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("💳 Starting Demo Subscription Seeding")
|
||||
logger.info("=" * 80)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for subscription_data in SUBSCRIPTIONS_DATA:
|
||||
tenant_id = subscription_data["tenant_id"]
|
||||
|
||||
# Check if subscription already exists for this tenant
|
||||
result = await db.execute(
|
||||
select(Subscription).where(
|
||||
Subscription.tenant_id == tenant_id,
|
||||
Subscription.status == "active"
|
||||
)
|
||||
)
|
||||
existing_subscription = result.scalars().first()
|
||||
|
||||
if existing_subscription:
|
||||
logger.info(
|
||||
"Subscription already exists - updating",
|
||||
tenant_id=str(tenant_id),
|
||||
subscription_id=str(existing_subscription.id)
|
||||
)
|
||||
|
||||
# Update existing subscription
|
||||
for key, value in subscription_data.items():
|
||||
if key != "tenant_id": # Don't update the tenant_id
|
||||
setattr(existing_subscription, key, value)
|
||||
|
||||
existing_subscription.updated_at = datetime.now(timezone.utc)
|
||||
updated_count += 1
|
||||
|
||||
else:
|
||||
logger.info(
|
||||
"Creating new subscription",
|
||||
tenant_id=str(tenant_id),
|
||||
plan=subscription_data["plan"]
|
||||
)
|
||||
|
||||
# Create new subscription
|
||||
subscription = Subscription(**subscription_data)
|
||||
db.add(subscription)
|
||||
created_count += 1
|
||||
|
||||
# Commit all changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info(
|
||||
"✅ Demo Subscription Seeding Completed",
|
||||
created=created_count,
|
||||
updated=updated_count,
|
||||
total=len(SUBSCRIPTIONS_DATA)
|
||||
)
|
||||
logger.info("=" * 80)
|
||||
|
||||
return {
|
||||
"service": "subscriptions",
|
||||
"created": created_count,
|
||||
"updated": updated_count,
|
||||
"total": len(SUBSCRIPTIONS_DATA)
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
|
||||
logger.info("Demo Subscription Seeding Script Starting")
|
||||
logger.info("Log Level: %s", os.getenv("LOG_LEVEL", "INFO"))
|
||||
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("TENANT_DATABASE_URL") or os.getenv("DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("❌ TENANT_DATABASE_URL or DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Convert to async URL if needed
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
logger.info("Connecting to tenant database")
|
||||
|
||||
# Create engine and session
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
echo=False,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
async_session = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_subscriptions(session)
|
||||
|
||||
logger.info("")
|
||||
logger.info("📊 Seeding Summary:")
|
||||
logger.info(f" ✅ Created: {result['created']}")
|
||||
logger.info(f" 🔄 Updated: {result['updated']}")
|
||||
logger.info(f" 📦 Total: {result['total']}")
|
||||
logger.info("")
|
||||
logger.info("🎉 Success! Demo subscriptions are ready.")
|
||||
logger.info("")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error("=" * 80)
|
||||
logger.error("❌ Demo Subscription Seeding Failed")
|
||||
logger.error("=" * 80)
|
||||
logger.error("Error: %s", str(e))
|
||||
logger.error("", exc_info=True)
|
||||
return 1
|
||||
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
263
services/tenant/scripts/demo/seed_demo_tenants.py
Executable file
263
services/tenant/scripts/demo/seed_demo_tenants.py
Executable file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo Tenant Seeding Script for Tenant Service
|
||||
Creates the two demo template tenants: San Pablo and La Espiga
|
||||
|
||||
This script runs as a Kubernetes init job inside the tenant-service container.
|
||||
It creates template tenants that will be cloned for demo sessions.
|
||||
|
||||
Usage:
|
||||
python /app/scripts/demo/seed_demo_tenants.py
|
||||
|
||||
Environment Variables Required:
|
||||
TENANT_DATABASE_URL - PostgreSQL connection string for tenant database
|
||||
AUTH_SERVICE_URL - URL of auth service (optional, for user creation)
|
||||
DEMO_MODE - Set to 'production' for production seeding
|
||||
LOG_LEVEL - Logging level (default: INFO)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import uuid
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# Add app to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import select
|
||||
import structlog
|
||||
|
||||
from app.models.tenants import Tenant
|
||||
|
||||
# Configure logging
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.stdlib.add_log_level,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.dev.ConsoleRenderer()
|
||||
]
|
||||
)
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
# Fixed Demo Tenant IDs (these are the template tenants that will be cloned)
|
||||
DEMO_TENANT_SAN_PABLO = uuid.UUID("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")
|
||||
DEMO_TENANT_LA_ESPIGA = uuid.UUID("b2c3d4e5-f6a7-48b9-c0d1-e2f3a4b5c6d7")
|
||||
|
||||
|
||||
TENANTS_DATA = [
|
||||
{
|
||||
"id": DEMO_TENANT_SAN_PABLO,
|
||||
"name": "Panadería San Pablo",
|
||||
"business_model": "san_pablo",
|
||||
"subscription_tier": "demo_template",
|
||||
"is_demo": False, # Template tenants are not marked as demo
|
||||
"is_demo_template": True, # They are templates for cloning
|
||||
"is_active": True,
|
||||
# Required fields
|
||||
"address": "Calle Mayor 45",
|
||||
"city": "Madrid",
|
||||
"postal_code": "28013",
|
||||
"owner_id": uuid.UUID("c1a2b3c4-d5e6-47a8-b9c0-d1e2f3a4b5c6"), # María García López (San Pablo owner)
|
||||
"metadata_": {
|
||||
"type": "traditional_bakery",
|
||||
"description": "Panadería tradicional familiar con venta al público",
|
||||
"characteristics": [
|
||||
"Producción en lotes pequeños adaptados a la demanda diaria",
|
||||
"Venta directa al consumidor final (walk-in customers)",
|
||||
"Ciclos de producción diarios comenzando de madrugada",
|
||||
"Variedad limitada de productos clásicos",
|
||||
"Proveedores locales de confianza",
|
||||
"Atención personalizada al cliente",
|
||||
"Ubicación en zona urbana residencial"
|
||||
],
|
||||
"location_type": "urban",
|
||||
"size": "small",
|
||||
"employees": 8,
|
||||
"opening_hours": "07:00-21:00",
|
||||
"production_shifts": 1,
|
||||
"target_market": "local_consumers"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": DEMO_TENANT_LA_ESPIGA,
|
||||
"name": "Panadería La Espiga - Obrador Central",
|
||||
"business_model": "la_espiga",
|
||||
"subscription_tier": "demo_template",
|
||||
"is_demo": False,
|
||||
"is_demo_template": True,
|
||||
"is_active": True,
|
||||
# Required fields
|
||||
"address": "Polígono Industrial Las Rozas, Nave 12",
|
||||
"city": "Las Rozas de Madrid",
|
||||
"postal_code": "28232",
|
||||
"owner_id": uuid.UUID("d2e3f4a5-b6c7-48d9-e0f1-a2b3c4d5e6f7"), # Carlos Martínez Ruiz (La Espiga owner)
|
||||
"metadata_": {
|
||||
"type": "central_workshop",
|
||||
"description": "Obrador central con distribución mayorista B2B",
|
||||
"characteristics": [
|
||||
"Producción industrial en lotes grandes",
|
||||
"Distribución a clientes mayoristas (hoteles, restaurantes, supermercados)",
|
||||
"Operación 24/7 con múltiples turnos de producción",
|
||||
"Amplia variedad de productos estandarizados",
|
||||
"Proveedores regionales con contratos de volumen",
|
||||
"Logística de distribución optimizada",
|
||||
"Ubicación en polígono industrial"
|
||||
],
|
||||
"location_type": "industrial",
|
||||
"size": "large",
|
||||
"employees": 25,
|
||||
"opening_hours": "24/7",
|
||||
"production_shifts": 3,
|
||||
"distribution_radius_km": 50,
|
||||
"target_market": "b2b_wholesale",
|
||||
"production_capacity_kg_day": 2000
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
async def seed_tenants(db: AsyncSession) -> dict:
|
||||
"""
|
||||
Seed the demo template tenants
|
||||
|
||||
Returns:
|
||||
Dict with seeding statistics
|
||||
"""
|
||||
logger.info("=" * 80)
|
||||
logger.info("🏢 Starting Demo Tenant Seeding")
|
||||
logger.info("=" * 80)
|
||||
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
|
||||
for tenant_data in TENANTS_DATA:
|
||||
tenant_id = tenant_data["id"]
|
||||
tenant_name = tenant_data["name"]
|
||||
|
||||
# Check if tenant already exists
|
||||
result = await db.execute(
|
||||
select(Tenant).where(Tenant.id == tenant_id)
|
||||
)
|
||||
existing_tenant = result.scalars().first()
|
||||
|
||||
if existing_tenant:
|
||||
logger.info(
|
||||
"Tenant already exists - updating",
|
||||
tenant_id=str(tenant_id),
|
||||
tenant_name=tenant_name
|
||||
)
|
||||
|
||||
# Update existing tenant
|
||||
for key, value in tenant_data.items():
|
||||
if key != "id": # Don't update the ID
|
||||
setattr(existing_tenant, key, value)
|
||||
|
||||
existing_tenant.updated_at = datetime.now(timezone.utc)
|
||||
updated_count += 1
|
||||
|
||||
else:
|
||||
logger.info(
|
||||
"Creating new tenant",
|
||||
tenant_id=str(tenant_id),
|
||||
tenant_name=tenant_name
|
||||
)
|
||||
|
||||
# Create new tenant
|
||||
tenant = Tenant(**tenant_data)
|
||||
db.add(tenant)
|
||||
created_count += 1
|
||||
|
||||
# Commit all changes
|
||||
await db.commit()
|
||||
|
||||
logger.info("=" * 80)
|
||||
logger.info(
|
||||
"✅ Demo Tenant Seeding Completed",
|
||||
created=created_count,
|
||||
updated=updated_count,
|
||||
total=len(TENANTS_DATA)
|
||||
)
|
||||
logger.info("=" * 80)
|
||||
|
||||
return {
|
||||
"service": "tenant",
|
||||
"created": created_count,
|
||||
"updated": updated_count,
|
||||
"total": len(TENANTS_DATA)
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main execution function"""
|
||||
|
||||
logger.info("Demo Tenant Seeding Script Starting")
|
||||
logger.info("Mode: %s", os.getenv("DEMO_MODE", "development"))
|
||||
logger.info("Log Level: %s", os.getenv("LOG_LEVEL", "INFO"))
|
||||
|
||||
# Get database URL from environment
|
||||
database_url = os.getenv("TENANT_DATABASE_URL") or os.getenv("DATABASE_URL")
|
||||
if not database_url:
|
||||
logger.error("❌ TENANT_DATABASE_URL or DATABASE_URL environment variable must be set")
|
||||
return 1
|
||||
|
||||
# Convert to async URL if needed
|
||||
if database_url.startswith("postgresql://"):
|
||||
database_url = database_url.replace("postgresql://", "postgresql+asyncpg://", 1)
|
||||
|
||||
logger.info("Connecting to tenant database")
|
||||
|
||||
# Create engine and session
|
||||
engine = create_async_engine(
|
||||
database_url,
|
||||
echo=False,
|
||||
pool_pre_ping=True,
|
||||
pool_size=5,
|
||||
max_overflow=10
|
||||
)
|
||||
|
||||
async_session = sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False
|
||||
)
|
||||
|
||||
try:
|
||||
async with async_session() as session:
|
||||
result = await seed_tenants(session)
|
||||
|
||||
logger.info("")
|
||||
logger.info("📊 Seeding Summary:")
|
||||
logger.info(f" ✅ Created: {result['created']}")
|
||||
logger.info(f" 🔄 Updated: {result['updated']}")
|
||||
logger.info(f" 📦 Total: {result['total']}")
|
||||
logger.info("")
|
||||
logger.info("🎉 Success! Template tenants are ready for cloning.")
|
||||
logger.info("")
|
||||
logger.info("Next steps:")
|
||||
logger.info(" 1. Run seed jobs for other services (inventory, recipes, etc.)")
|
||||
logger.info(" 2. Verify tenant data in database")
|
||||
logger.info(" 3. Test demo session creation")
|
||||
logger.info("")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error("=" * 80)
|
||||
logger.error("❌ Demo Tenant Seeding Failed")
|
||||
logger.error("=" * 80)
|
||||
logger.error("Error: %s", str(e))
|
||||
logger.error("", exc_info=True)
|
||||
return 1
|
||||
|
||||
finally:
|
||||
await engine.dispose()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit_code = asyncio.run(main())
|
||||
sys.exit(exit_code)
|
||||
Reference in New Issue
Block a user