Files
bakery-ia/services/tenant/app/models/tenants.py
2025-11-01 21:35:03 +01:00

176 lines
7.4 KiB
Python

# services/tenant/app/models/tenants.py - FIXED VERSION
"""
Tenant models for bakery management - FIXED
Removed cross-service User relationship to eliminate circular dependencies
"""
from sqlalchemy import Column, String, Boolean, DateTime, Float, ForeignKey, Text, Integer, JSON
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship
from datetime import datetime, timezone
import uuid
from shared.database.base import Base
class Tenant(Base):
"""Tenant/Bakery model"""
__tablename__ = "tenants"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
name = Column(String(200), nullable=False)
subdomain = Column(String(100), unique=True)
business_type = Column(String(100), default="bakery")
business_model = Column(String(100), default="individual_bakery") # individual_bakery, central_baker_satellite, retail_bakery, hybrid_bakery
# Location info
address = Column(Text, nullable=False)
city = Column(String(100), default="Madrid")
postal_code = Column(String(10), nullable=False)
latitude = Column(Float)
longitude = Column(Float)
# Timezone configuration for accurate scheduling
timezone = Column(String(50), default="Europe/Madrid", nullable=False)
# Contact info
phone = Column(String(20))
email = Column(String(255))
# Status
is_active = Column(Boolean, default=True)
# Demo account flags
is_demo = Column(Boolean, default=False, index=True)
is_demo_template = Column(Boolean, default=False, index=True)
base_demo_tenant_id = Column(UUID(as_uuid=True), nullable=True, index=True)
demo_session_id = Column(String(100), nullable=True, index=True)
demo_expires_at = Column(DateTime(timezone=True), nullable=True)
# ML status
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))
# Relationships - only within tenant service
members = relationship("TenantMember", back_populates="tenant", cascade="all, delete-orphan")
subscriptions = relationship("Subscription", back_populates="tenant", cascade="all, delete-orphan")
# REMOVED: users relationship - no cross-service SQLAlchemy relationships
@property
def subscription_tier(self):
"""
Get current subscription tier from active subscription
Note: This is a computed property that requires subscription relationship to be loaded.
For performance-critical operations, use the subscription cache service directly.
"""
# Find active subscription
for subscription in self.subscriptions:
if subscription.status == 'active':
return subscription.plan
return "starter" # Default fallback
def __repr__(self):
return f"<Tenant(id={self.id}, name={self.name})>"
class TenantMember(Base):
"""
Tenant membership model for team access.
This model represents TENANT-SPECIFIC roles, which are distinct from global user roles.
TENANT ROLES (stored here):
- owner: Full control of the tenant, can transfer ownership, manage all aspects
- admin: Tenant administrator, can manage team members and most operations
- member: Standard team member, regular operational access
- viewer: Read-only observer, view-only access to tenant data
ROLE MAPPING TO GLOBAL ROLES:
When users are created through tenant management (pilot phase), their tenant role
is mapped to a global user role in the Auth service:
- tenant 'admin' → global 'admin' (system-wide admin access)
- tenant 'member' → global 'manager' (management-level access)
- tenant 'viewer' → global 'user' (basic user access)
- tenant 'owner' → No automatic global role (owner is tenant-specific)
This mapping is implemented in app/api/tenant_members.py lines 68-76.
Note: user_id is a cross-service reference (no FK) to avoid circular dependencies.
User enrichment is handled at the service layer via Auth service calls.
"""
__tablename__ = "tenant_members"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
user_id = Column(UUID(as_uuid=True), nullable=False, index=True) # No FK - cross-service reference
# Role and permissions specific to this tenant
# Valid values: 'owner', 'admin', 'member', 'viewer'
role = Column(String(50), default="member")
permissions = Column(Text) # JSON string of permissions
# Status
is_active = Column(Boolean, default=True)
invited_by = Column(UUID(as_uuid=True)) # No FK - cross-service reference
invited_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
joined_at = Column(DateTime(timezone=True))
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
# Relationships - only within tenant service
tenant = relationship("Tenant", back_populates="members")
# REMOVED: user relationship - no cross-service SQLAlchemy relationships
def __repr__(self):
return f"<TenantMember(tenant_id={self.tenant_id}, user_id={self.user_id}, role={self.role})>"
# Additional models for subscriptions, plans, etc.
class Subscription(Base):
"""Subscription model for tenant billing"""
__tablename__ = "subscriptions"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False)
plan = Column(String(50), default="starter") # starter, professional, enterprise
status = Column(String(50), default="active") # active, pending_cancellation, inactive, suspended
# Billing
monthly_price = Column(Float, default=0.0)
billing_cycle = Column(String(20), default="monthly") # monthly, yearly
next_billing_date = Column(DateTime(timezone=True))
trial_ends_at = Column(DateTime(timezone=True))
cancelled_at = Column(DateTime(timezone=True), nullable=True)
cancellation_effective_date = Column(DateTime(timezone=True), nullable=True)
stripe_subscription_id = Column(String(255), nullable=True)
stripe_customer_id = Column(String(255), nullable=True)
# Limits
max_users = Column(Integer, default=5)
max_locations = Column(Integer, default=1)
max_products = Column(Integer, default=50)
# Features - Store plan features as JSON
features = Column(JSON)
# 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))
# Relationships
tenant = relationship("Tenant")
def __repr__(self):
return f"<Subscription(tenant_id={self.tenant_id}, plan={self.plan}, status={self.status})>"