Initial commit - production deployment
This commit is contained in:
221
services/tenant/app/models/tenants.py
Normal file
221
services/tenant/app/models/tenants.py
Normal file
@@ -0,0 +1,221 @@
|
||||
# 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))
|
||||
|
||||
# 3D Secure (3DS) tracking
|
||||
threeds_authentication_required = Column(Boolean, default=False)
|
||||
threeds_authentication_required_at = Column(DateTime(timezone=True), nullable=True)
|
||||
threeds_authentication_completed = Column(Boolean, default=False)
|
||||
threeds_authentication_completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_threeds_setup_intent_id = Column(String(255), nullable=True)
|
||||
threeds_action_type = Column(String(100), nullable=True)
|
||||
|
||||
# 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"<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', '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"<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 with tenant linking support"""
|
||||
__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=True)
|
||||
|
||||
# User reference for tenant-independent subscriptions
|
||||
user_id = Column(UUID(as_uuid=True), nullable=True, index=True)
|
||||
|
||||
# Tenant linking status
|
||||
is_tenant_linked = Column(Boolean, default=False, nullable=False)
|
||||
tenant_linking_status = Column(String(50), nullable=True) # pending, completed, failed
|
||||
linked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
plan = Column(String(50), default="starter") # starter, professional, enterprise
|
||||
status = Column(String(50), default="active") # active, pending_cancellation, inactive, suspended, pending_tenant_linking
|
||||
|
||||
# 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)
|
||||
|
||||
# Payment provider references (generic names for provider-agnostic design)
|
||||
subscription_id = Column(String(255), nullable=True) # Payment provider subscription ID
|
||||
customer_id = Column(String(255), nullable=True) # Payment provider customer ID
|
||||
|
||||
# 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))
|
||||
|
||||
# 3D Secure (3DS) tracking
|
||||
threeds_authentication_required = Column(Boolean, default=False)
|
||||
threeds_authentication_required_at = Column(DateTime(timezone=True), nullable=True)
|
||||
threeds_authentication_completed = Column(Boolean, default=False)
|
||||
threeds_authentication_completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
last_threeds_setup_intent_id = Column(String(255), nullable=True)
|
||||
threeds_action_type = Column(String(100), nullable=True)
|
||||
|
||||
# Relationships
|
||||
tenant = relationship("Tenant")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Subscription(id={self.id}, tenant_id={self.tenant_id}, user_id={self.user_id}, plan={self.plan}, status={self.status})>"
|
||||
|
||||
def is_pending_tenant_linking(self) -> bool:
|
||||
"""Check if subscription is waiting to be linked to a tenant"""
|
||||
return self.tenant_linking_status == "pending" and not self.is_tenant_linked
|
||||
|
||||
def can_be_linked_to_tenant(self, user_id: str) -> bool:
|
||||
"""Check if subscription can be linked to a tenant by the given user"""
|
||||
return (self.is_pending_tenant_linking() and
|
||||
str(self.user_id) == user_id and
|
||||
self.tenant_id is None)
|
||||
Reference in New Issue
Block a user