# 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) # Regional/Localization configuration timezone = Column(String(50), default="Europe/Madrid", nullable=False) currency = Column(String(3), default="EUR", nullable=False) # Currency code: EUR, USD, GBP language = Column(String(5), default="es", nullable=False) # Language code: es, en, eu # 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) # Enterprise tier hierarchy fields parent_tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id", ondelete="RESTRICT"), nullable=True, index=True) tenant_type = Column(String(50), default="standalone", nullable=False) # standalone, parent, child hierarchy_path = Column(String(500), nullable=True) # Materialized path for queries # 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") locations = relationship("TenantLocation", back_populates="tenant", cascade="all, delete-orphan") child_tenants = relationship("Tenant", back_populates="parent_tenant", remote_side=[id]) parent_tenant = relationship("Tenant", back_populates="child_tenants", remote_side=[parent_tenant_id]) # 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"" 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', 'network_admin' 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"" # 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""