New enterprise feature

This commit is contained in:
Urtzi Alfaro
2025-11-30 09:12:40 +01:00
parent f9d0eec6ec
commit 972db02f6d
176 changed files with 19741 additions and 1361 deletions

View File

@@ -0,0 +1,218 @@
"""
Tenant Location Repository
Handles database operations for tenant location data
"""
from typing import List, Optional, Dict, Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, update, delete
from sqlalchemy.orm import selectinload
import structlog
from app.models.tenant_location import TenantLocation
from app.models.tenants import Tenant
from shared.database.exceptions import DatabaseError
from .base import BaseRepository
logger = structlog.get_logger()
class TenantLocationRepository(BaseRepository):
"""Repository for tenant location operations"""
def __init__(self, session: AsyncSession):
super().__init__(TenantLocation, session)
async def create_location(self, location_data: Dict[str, Any]) -> TenantLocation:
"""
Create a new tenant location
Args:
location_data: Dictionary containing location information
Returns:
Created TenantLocation object
"""
try:
# Create new location instance
location = TenantLocation(**location_data)
self.session.add(location)
await self.session.commit()
await self.session.refresh(location)
logger.info(f"Created new tenant location: {location.id} for tenant {location.tenant_id}")
return location
except Exception as e:
await self.session.rollback()
logger.error(f"Failed to create tenant location: {str(e)}")
raise DatabaseError(f"Failed to create tenant location: {str(e)}")
async def get_location_by_id(self, location_id: str) -> Optional[TenantLocation]:
"""
Get a location by its ID
Args:
location_id: UUID of the location
Returns:
TenantLocation object if found, None otherwise
"""
try:
stmt = select(TenantLocation).where(TenantLocation.id == location_id)
result = await self.session.execute(stmt)
location = result.scalar_one_or_none()
return location
except Exception as e:
logger.error(f"Failed to get location by ID: {str(e)}")
raise DatabaseError(f"Failed to get location by ID: {str(e)}")
async def get_locations_by_tenant(self, tenant_id: str) -> List[TenantLocation]:
"""
Get all locations for a specific tenant
Args:
tenant_id: UUID of the tenant
Returns:
List of TenantLocation objects
"""
try:
stmt = select(TenantLocation).where(TenantLocation.tenant_id == tenant_id)
result = await self.session.execute(stmt)
locations = result.scalars().all()
return locations
except Exception as e:
logger.error(f"Failed to get locations by tenant: {str(e)}")
raise DatabaseError(f"Failed to get locations by tenant: {str(e)}")
async def get_location_by_type(self, tenant_id: str, location_type: str) -> Optional[TenantLocation]:
"""
Get a location by tenant and type
Args:
tenant_id: UUID of the tenant
location_type: Type of location (e.g., 'central_production', 'retail_outlet')
Returns:
TenantLocation object if found, None otherwise
"""
try:
stmt = select(TenantLocation).where(
TenantLocation.tenant_id == tenant_id,
TenantLocation.location_type == location_type
)
result = await self.session.execute(stmt)
location = result.scalar_one_or_none()
return location
except Exception as e:
logger.error(f"Failed to get location by type: {str(e)}")
raise DatabaseError(f"Failed to get location by type: {str(e)}")
async def update_location(self, location_id: str, location_data: Dict[str, Any]) -> Optional[TenantLocation]:
"""
Update a tenant location
Args:
location_id: UUID of the location to update
location_data: Dictionary containing updated location information
Returns:
Updated TenantLocation object if successful, None if location not found
"""
try:
stmt = (
update(TenantLocation)
.where(TenantLocation.id == location_id)
.values(**location_data)
)
await self.session.execute(stmt)
# Now fetch the updated location
location_stmt = select(TenantLocation).where(TenantLocation.id == location_id)
result = await self.session.execute(location_stmt)
location = result.scalar_one_or_none()
if location:
await self.session.commit()
logger.info(f"Updated tenant location: {location_id}")
return location
else:
await self.session.rollback()
logger.warning(f"Location not found for update: {location_id}")
return None
except Exception as e:
await self.session.rollback()
logger.error(f"Failed to update location: {str(e)}")
raise DatabaseError(f"Failed to update location: {str(e)}")
async def delete_location(self, location_id: str) -> bool:
"""
Delete a tenant location
Args:
location_id: UUID of the location to delete
Returns:
True if deleted successfully, False if location not found
"""
try:
stmt = delete(TenantLocation).where(TenantLocation.id == location_id)
result = await self.session.execute(stmt)
if result.rowcount > 0:
await self.session.commit()
logger.info(f"Deleted tenant location: {location_id}")
return True
else:
await self.session.rollback()
logger.warning(f"Location not found for deletion: {location_id}")
return False
except Exception as e:
await self.session.rollback()
logger.error(f"Failed to delete location: {str(e)}")
raise DatabaseError(f"Failed to delete location: {str(e)}")
async def get_active_locations_by_tenant(self, tenant_id: str) -> List[TenantLocation]:
"""
Get all active locations for a specific tenant
Args:
tenant_id: UUID of the tenant
Returns:
List of active TenantLocation objects
"""
try:
stmt = select(TenantLocation).where(
TenantLocation.tenant_id == tenant_id,
TenantLocation.is_active == True
)
result = await self.session.execute(stmt)
locations = result.scalars().all()
return locations
except Exception as e:
logger.error(f"Failed to get active locations by tenant: {str(e)}")
raise DatabaseError(f"Failed to get active locations by tenant: {str(e)}")
async def get_locations_by_tenant_with_type(self, tenant_id: str, location_types: List[str]) -> List[TenantLocation]:
"""
Get locations for a specific tenant filtered by location types
Args:
tenant_id: UUID of the tenant
location_types: List of location types to filter by
Returns:
List of TenantLocation objects matching the criteria
"""
try:
stmt = select(TenantLocation).where(
TenantLocation.tenant_id == tenant_id,
TenantLocation.location_type.in_(location_types)
)
result = await self.session.execute(stmt)
locations = result.scalars().all()
return locations
except Exception as e:
logger.error(f"Failed to get locations by tenant and type: {str(e)}")
raise DatabaseError(f"Failed to get locations by tenant and type: {str(e)}")

View File

@@ -381,3 +381,188 @@ class TenantRepository(TenantBaseRepository):
async def activate_tenant(self, tenant_id: str) -> Optional[Tenant]:
"""Activate a tenant"""
return await self.activate_record(tenant_id)
async def get_child_tenants(self, parent_tenant_id: str) -> List[Tenant]:
"""Get all child tenants for a parent tenant"""
try:
return await self.get_multi(
filters={"parent_tenant_id": parent_tenant_id, "is_active": True},
order_by="created_at",
order_desc=False
)
except Exception as e:
logger.error("Failed to get child tenants",
parent_tenant_id=parent_tenant_id,
error=str(e))
raise DatabaseError(f"Failed to get child tenants: {str(e)}")
async def get_child_tenant_count(self, parent_tenant_id: str) -> int:
"""Get count of child tenants for a parent tenant"""
try:
child_tenants = await self.get_child_tenants(parent_tenant_id)
return len(child_tenants)
except Exception as e:
logger.error("Failed to get child tenant count",
parent_tenant_id=parent_tenant_id,
error=str(e))
return 0
async def get_user_tenants_with_hierarchy(self, user_id: str) -> List[Dict[str, Any]]:
"""
Get all tenants a user has access to, organized in hierarchy.
Returns parent tenants with their children nested.
"""
try:
# Get all tenants where user is owner or member
query_text = """
SELECT DISTINCT t.*
FROM tenants t
LEFT JOIN tenant_members tm ON t.id = tm.tenant_id
WHERE (t.owner_id = :user_id OR tm.user_id = :user_id)
AND t.is_active = true
ORDER BY t.tenant_type DESC, t.created_at ASC
"""
result = await self.session.execute(text(query_text), {"user_id": user_id})
tenants = []
for row in result.fetchall():
record_dict = dict(row._mapping)
tenant = self.model(**record_dict)
tenants.append(tenant)
# Organize into hierarchy
tenant_hierarchy = []
parent_map = {}
# First pass: collect all parent/standalone tenants
for tenant in tenants:
if tenant.tenant_type in ['parent', 'standalone']:
tenant_dict = {
'id': str(tenant.id),
'name': tenant.name,
'subdomain': tenant.subdomain,
'tenant_type': tenant.tenant_type,
'business_type': tenant.business_type,
'business_model': tenant.business_model,
'city': tenant.city,
'is_active': tenant.is_active,
'children': [] if tenant.tenant_type == 'parent' else None
}
tenant_hierarchy.append(tenant_dict)
parent_map[str(tenant.id)] = tenant_dict
# Second pass: attach children to their parents
for tenant in tenants:
if tenant.tenant_type == 'child' and tenant.parent_tenant_id:
parent_id = str(tenant.parent_tenant_id)
if parent_id in parent_map:
child_dict = {
'id': str(tenant.id),
'name': tenant.name,
'subdomain': tenant.subdomain,
'tenant_type': 'child',
'parent_tenant_id': parent_id,
'city': tenant.city,
'is_active': tenant.is_active
}
parent_map[parent_id]['children'].append(child_dict)
return tenant_hierarchy
except Exception as e:
logger.error("Failed to get user tenants with hierarchy",
user_id=user_id,
error=str(e))
return []
async def get_tenants_by_session_id(self, session_id: str) -> List[Tenant]:
"""
Get tenants associated with a specific demo session using the demo_session_id field.
"""
try:
return await self.get_multi(
filters={
"demo_session_id": session_id,
"is_active": True
},
order_by="created_at",
order_desc=True
)
except Exception as e:
logger.error("Failed to get tenants by session ID",
session_id=session_id,
error=str(e))
raise DatabaseError(f"Failed to get tenants by session ID: {str(e)}")
async def get_professional_demo_tenants(self, session_id: str) -> List[Tenant]:
"""
Get professional demo tenants filtered by session.
Args:
session_id: Required demo session ID to filter tenants
Returns:
List of professional demo tenants for this specific session
"""
try:
filters = {
"business_model": "professional_bakery",
"is_demo": True,
"is_active": True,
"demo_session_id": session_id # Always filter by session
}
return await self.get_multi(
filters=filters,
order_by="created_at",
order_desc=True
)
except Exception as e:
logger.error("Failed to get professional demo tenants",
session_id=session_id,
error=str(e))
raise DatabaseError(f"Failed to get professional demo tenants: {str(e)}")
async def get_enterprise_demo_tenants(self, session_id: str) -> List[Tenant]:
"""
Get enterprise demo tenants (parent and children) filtered by session.
Args:
session_id: Required demo session ID to filter tenants
Returns:
List of enterprise demo tenants (1 parent + 3 children) for this specific session
"""
try:
# Get enterprise demo parent tenants for this session
parent_tenants = await self.get_multi(
filters={
"tenant_type": "parent",
"is_demo": True,
"is_active": True,
"demo_session_id": session_id # Always filter by session
},
order_by="created_at",
order_desc=True
)
# Get child tenants for the enterprise demo session
child_tenants = await self.get_multi(
filters={
"tenant_type": "child",
"is_demo": True,
"is_active": True,
"demo_session_id": session_id # Always filter by session
},
order_by="created_at",
order_desc=True
)
# Combine parent and child tenants
return parent_tenants + child_tenants
except Exception as e:
logger.error("Failed to get enterprise demo tenants",
session_id=session_id,
error=str(e))
raise DatabaseError(f"Failed to get enterprise demo tenants: {str(e)}")