Add traslations
This commit is contained in:
@@ -219,10 +219,11 @@ async def get_tenant_children_count(
|
||||
)
|
||||
|
||||
|
||||
@router.post(route_builder.build_base_route("bulk-children", include_tenant_prefix=False), response_model=BulkChildTenantsResponse)
|
||||
@router.post("/api/v1/tenants/{tenant_id}/bulk-children", response_model=BulkChildTenantsResponse)
|
||||
@track_endpoint_metrics("bulk_create_child_tenants")
|
||||
async def bulk_create_child_tenants(
|
||||
request: BulkChildTenantsCreate,
|
||||
tenant_id: str = Path(..., description="Parent tenant ID"),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
||||
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
||||
):
|
||||
@@ -242,7 +243,7 @@ async def bulk_create_child_tenants(
|
||||
try:
|
||||
logger.info(
|
||||
"Bulk child tenant creation request received",
|
||||
parent_tenant_id=request.parent_tenant_id,
|
||||
parent_tenant_id=tenant_id,
|
||||
child_count=len(request.child_tenants),
|
||||
user_id=current_user.get("user_id")
|
||||
)
|
||||
@@ -252,7 +253,7 @@ async def bulk_create_child_tenants(
|
||||
from app.models.tenants import Tenant
|
||||
tenant_repo = TenantRepository(Tenant, session)
|
||||
|
||||
parent_tenant = await tenant_repo.get_by_id(request.parent_tenant_id)
|
||||
parent_tenant = await tenant_repo.get_by_id(tenant_id)
|
||||
if not parent_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@@ -262,7 +263,7 @@ async def bulk_create_child_tenants(
|
||||
# Verify user has access to parent tenant (owners/admins only)
|
||||
access_info = await tenant_service.verify_user_access(
|
||||
current_user["user_id"],
|
||||
request.parent_tenant_id
|
||||
tenant_id
|
||||
)
|
||||
if not access_info.has_access or access_info.role not in ["owner", "admin"]:
|
||||
raise HTTPException(
|
||||
@@ -271,8 +272,8 @@ async def bulk_create_child_tenants(
|
||||
)
|
||||
|
||||
# Verify parent is enterprise tier
|
||||
parent_subscription_tier = await tenant_service.get_subscription_tier(request.parent_tenant_id)
|
||||
if parent_subscription_tier != "enterprise":
|
||||
parent_subscription = await tenant_service.subscription_repo.get_active_subscription(tenant_id)
|
||||
if not parent_subscription or parent_subscription.plan != "enterprise":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Only enterprise tier tenants can have child tenants"
|
||||
@@ -290,121 +291,136 @@ async def bulk_create_child_tenants(
|
||||
failed_tenants = []
|
||||
|
||||
for child_data in request.child_tenants:
|
||||
try:
|
||||
# Create child tenant
|
||||
child_tenant = Tenant(
|
||||
name=child_data.name,
|
||||
subdomain=None, # Child tenants typically don't have subdomains
|
||||
business_type=parent_tenant.business_type,
|
||||
business_model="retail_bakery", # Child outlets are typically retail
|
||||
address=child_data.address,
|
||||
city=child_data.city,
|
||||
postal_code=child_data.postal_code,
|
||||
latitude=child_data.latitude,
|
||||
longitude=child_data.longitude,
|
||||
phone=child_data.phone or parent_tenant.phone,
|
||||
email=child_data.email or parent_tenant.email,
|
||||
timezone=parent_tenant.timezone,
|
||||
owner_id=parent_tenant.owner_id,
|
||||
parent_tenant_id=parent_tenant.id,
|
||||
tenant_type="child",
|
||||
hierarchy_path=f"{parent_tenant.hierarchy_path}/{str(parent_tenant.id)}",
|
||||
is_active=True,
|
||||
is_demo=parent_tenant.is_demo,
|
||||
demo_session_id=parent_tenant.demo_session_id,
|
||||
demo_expires_at=parent_tenant.demo_expires_at
|
||||
)
|
||||
|
||||
session.add(child_tenant)
|
||||
await session.flush() # Get the ID without committing
|
||||
|
||||
# Create TenantLocation record for the child with location_code
|
||||
from app.models.tenant_location import TenantLocation
|
||||
location = TenantLocation(
|
||||
tenant_id=child_tenant.id,
|
||||
name=child_data.name,
|
||||
location_code=child_data.location_code,
|
||||
city=child_data.city,
|
||||
zone=child_data.zone,
|
||||
address=child_data.address,
|
||||
postal_code=child_data.postal_code,
|
||||
latitude=child_data.latitude,
|
||||
longitude=child_data.longitude,
|
||||
status="ACTIVE",
|
||||
is_primary=True,
|
||||
enterprise_location=True,
|
||||
location_type="retail"
|
||||
)
|
||||
session.add(location)
|
||||
|
||||
# Inherit subscription from parent
|
||||
from app.models.tenants import Subscription
|
||||
parent_subscription = await session.execute(
|
||||
session.query(Subscription).filter(
|
||||
Subscription.tenant_id == parent_tenant.id,
|
||||
Subscription.status == "active"
|
||||
).statement
|
||||
)
|
||||
parent_sub = parent_subscription.scalar_one_or_none()
|
||||
|
||||
if parent_sub:
|
||||
child_subscription = Subscription(
|
||||
tenant_id=child_tenant.id,
|
||||
plan=parent_sub.plan,
|
||||
status="active",
|
||||
billing_cycle=parent_sub.billing_cycle,
|
||||
price=0, # Child tenants don't pay separately
|
||||
trial_ends_at=parent_sub.trial_ends_at
|
||||
# Create a nested transaction (savepoint) for each child tenant
|
||||
# This allows us to rollback individual child tenant creation without affecting others
|
||||
async with session.begin_nested():
|
||||
try:
|
||||
# Create child tenant with full tenant model fields
|
||||
child_tenant = Tenant(
|
||||
name=child_data.name,
|
||||
subdomain=None, # Child tenants typically don't have subdomains
|
||||
business_type=child_data.business_type or parent_tenant.business_type,
|
||||
business_model=child_data.business_model or "retail_bakery", # Child outlets are typically retail
|
||||
address=child_data.address,
|
||||
city=child_data.city,
|
||||
postal_code=child_data.postal_code,
|
||||
latitude=child_data.latitude,
|
||||
longitude=child_data.longitude,
|
||||
phone=child_data.phone or parent_tenant.phone,
|
||||
email=child_data.email or parent_tenant.email,
|
||||
timezone=child_data.timezone or parent_tenant.timezone,
|
||||
owner_id=parent_tenant.owner_id,
|
||||
parent_tenant_id=parent_tenant.id,
|
||||
tenant_type="child",
|
||||
hierarchy_path=f"{parent_tenant.hierarchy_path}", # Will be updated after flush
|
||||
is_active=True,
|
||||
is_demo=parent_tenant.is_demo,
|
||||
demo_session_id=parent_tenant.demo_session_id,
|
||||
demo_expires_at=parent_tenant.demo_expires_at,
|
||||
metadata_={
|
||||
"location_code": child_data.location_code,
|
||||
"zone": child_data.zone,
|
||||
**(child_data.metadata or {})
|
||||
}
|
||||
)
|
||||
session.add(child_subscription)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(child_tenant)
|
||||
await session.refresh(location)
|
||||
session.add(child_tenant)
|
||||
await session.flush() # Get the ID without committing
|
||||
|
||||
# Build response
|
||||
created_tenants.append(ChildTenantResponse(
|
||||
id=str(child_tenant.id),
|
||||
name=child_tenant.name,
|
||||
subdomain=child_tenant.subdomain,
|
||||
business_type=child_tenant.business_type,
|
||||
business_model=child_tenant.business_model,
|
||||
tenant_type=child_tenant.tenant_type,
|
||||
parent_tenant_id=str(child_tenant.parent_tenant_id),
|
||||
address=child_tenant.address,
|
||||
city=child_tenant.city,
|
||||
postal_code=child_tenant.postal_code,
|
||||
phone=child_tenant.phone,
|
||||
is_active=child_tenant.is_active,
|
||||
subscription_plan="enterprise",
|
||||
ml_model_trained=child_tenant.ml_model_trained,
|
||||
last_training_date=child_tenant.last_training_date,
|
||||
owner_id=str(child_tenant.owner_id),
|
||||
created_at=child_tenant.created_at,
|
||||
location_code=location.location_code,
|
||||
zone=location.zone,
|
||||
hierarchy_path=child_tenant.hierarchy_path
|
||||
))
|
||||
# Update hierarchy_path now that we have the child tenant ID
|
||||
child_tenant.hierarchy_path = f"{parent_tenant.hierarchy_path}.{str(child_tenant.id)}"
|
||||
|
||||
logger.info(
|
||||
"Child tenant created successfully",
|
||||
child_tenant_id=str(child_tenant.id),
|
||||
child_name=child_tenant.name,
|
||||
location_code=child_data.location_code
|
||||
)
|
||||
# Create TenantLocation record for the child
|
||||
from app.models.tenant_location import TenantLocation
|
||||
location = TenantLocation(
|
||||
tenant_id=child_tenant.id,
|
||||
name=child_data.name,
|
||||
city=child_data.city,
|
||||
address=child_data.address,
|
||||
postal_code=child_data.postal_code,
|
||||
latitude=child_data.latitude,
|
||||
longitude=child_data.longitude,
|
||||
is_active=True,
|
||||
location_type="retail"
|
||||
)
|
||||
session.add(location)
|
||||
|
||||
except Exception as child_error:
|
||||
logger.error(
|
||||
"Failed to create child tenant",
|
||||
child_name=child_data.name,
|
||||
error=str(child_error)
|
||||
)
|
||||
failed_tenants.append({
|
||||
"name": child_data.name,
|
||||
"location_code": child_data.location_code,
|
||||
"error": str(child_error)
|
||||
})
|
||||
await session.rollback()
|
||||
# Inherit subscription from parent
|
||||
from app.models.tenants import Subscription
|
||||
from sqlalchemy import select
|
||||
parent_subscription_result = await session.execute(
|
||||
select(Subscription).where(
|
||||
Subscription.tenant_id == parent_tenant.id,
|
||||
Subscription.status == "active"
|
||||
)
|
||||
)
|
||||
parent_sub = parent_subscription_result.scalar_one_or_none()
|
||||
|
||||
if parent_sub:
|
||||
child_subscription = Subscription(
|
||||
tenant_id=child_tenant.id,
|
||||
plan=parent_sub.plan,
|
||||
status="active",
|
||||
billing_cycle=parent_sub.billing_cycle,
|
||||
monthly_price=0, # Child tenants don't pay separately
|
||||
trial_ends_at=parent_sub.trial_ends_at
|
||||
)
|
||||
session.add(child_subscription)
|
||||
|
||||
# Commit the nested transaction (savepoint)
|
||||
await session.flush()
|
||||
|
||||
# Refresh objects to get their final state
|
||||
await session.refresh(child_tenant)
|
||||
await session.refresh(location)
|
||||
|
||||
# Build response
|
||||
created_tenants.append(ChildTenantResponse(
|
||||
id=str(child_tenant.id),
|
||||
name=child_tenant.name,
|
||||
subdomain=child_tenant.subdomain,
|
||||
business_type=child_tenant.business_type,
|
||||
business_model=child_tenant.business_model,
|
||||
tenant_type=child_tenant.tenant_type,
|
||||
parent_tenant_id=str(child_tenant.parent_tenant_id),
|
||||
address=child_tenant.address,
|
||||
city=child_tenant.city,
|
||||
postal_code=child_tenant.postal_code,
|
||||
phone=child_tenant.phone,
|
||||
is_active=child_tenant.is_active,
|
||||
subscription_plan="enterprise",
|
||||
ml_model_trained=child_tenant.ml_model_trained,
|
||||
last_training_date=child_tenant.last_training_date,
|
||||
owner_id=str(child_tenant.owner_id),
|
||||
created_at=child_tenant.created_at,
|
||||
location_code=child_data.location_code,
|
||||
zone=child_data.zone,
|
||||
hierarchy_path=child_tenant.hierarchy_path
|
||||
))
|
||||
|
||||
logger.info(
|
||||
"Child tenant created successfully",
|
||||
child_tenant_id=str(child_tenant.id),
|
||||
child_name=child_tenant.name,
|
||||
location_code=child_data.location_code
|
||||
)
|
||||
|
||||
except Exception as child_error:
|
||||
logger.error(
|
||||
"Failed to create child tenant",
|
||||
child_name=child_data.name,
|
||||
error=str(child_error)
|
||||
)
|
||||
failed_tenants.append({
|
||||
"name": child_data.name,
|
||||
"location_code": child_data.location_code,
|
||||
"error": str(child_error)
|
||||
})
|
||||
# Nested transaction will automatically rollback on exception
|
||||
# This only rolls back the current child tenant, not the entire batch
|
||||
|
||||
# Commit all successful child tenant creations
|
||||
await session.commit()
|
||||
|
||||
# TODO: Configure distribution routes if requested
|
||||
distribution_configured = False
|
||||
@@ -414,7 +430,7 @@ async def bulk_create_child_tenants(
|
||||
# For now, we'll skip this and just log
|
||||
logger.info(
|
||||
"Distribution route configuration requested",
|
||||
parent_tenant_id=request.parent_tenant_id,
|
||||
parent_tenant_id=tenant_id,
|
||||
child_count=len(created_tenants)
|
||||
)
|
||||
# distribution_configured = await configure_distribution_routes(...)
|
||||
@@ -426,13 +442,13 @@ async def bulk_create_child_tenants(
|
||||
|
||||
logger.info(
|
||||
"Bulk child tenant creation completed",
|
||||
parent_tenant_id=request.parent_tenant_id,
|
||||
parent_tenant_id=tenant_id,
|
||||
created_count=len(created_tenants),
|
||||
failed_count=len(failed_tenants)
|
||||
)
|
||||
|
||||
return BulkChildTenantsResponse(
|
||||
parent_tenant_id=request.parent_tenant_id,
|
||||
parent_tenant_id=tenant_id,
|
||||
created_count=len(created_tenants),
|
||||
failed_count=len(failed_tenants),
|
||||
created_tenants=created_tenants,
|
||||
@@ -445,7 +461,7 @@ async def bulk_create_child_tenants(
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Bulk child tenant creation failed",
|
||||
parent_tenant_id=request.parent_tenant_id,
|
||||
parent_tenant_id=tenant_id,
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
@@ -204,7 +204,7 @@ class TenantStatsResponse(BaseModel):
|
||||
# ============================================================================
|
||||
|
||||
class ChildTenantCreate(BaseModel):
|
||||
"""Schema for creating a child tenant in enterprise hierarchy"""
|
||||
"""Schema for creating a child tenant in enterprise hierarchy - Updated to match tenant model"""
|
||||
name: str = Field(..., min_length=2, max_length=200, description="Child tenant name (e.g., 'Madrid - Salamanca')")
|
||||
city: str = Field(..., min_length=2, max_length=100, description="City where the outlet is located")
|
||||
zone: Optional[str] = Field(None, max_length=100, description="Zone or neighborhood")
|
||||
@@ -212,14 +212,24 @@ class ChildTenantCreate(BaseModel):
|
||||
postal_code: str = Field(..., pattern=r"^\d{5}$", description="5-digit postal code")
|
||||
location_code: str = Field(..., min_length=1, max_length=10, description="Short location code (e.g., MAD, BCN)")
|
||||
|
||||
# Optional coordinates (can be geocoded from address if not provided)
|
||||
# Coordinates (can be geocoded from address if not provided)
|
||||
latitude: Optional[float] = Field(None, ge=-90, le=90, description="Latitude coordinate")
|
||||
longitude: Optional[float] = Field(None, ge=-180, le=180, description="Longitude coordinate")
|
||||
|
||||
# Optional contact info (inherits from parent if not provided)
|
||||
# Contact info (inherits from parent if not provided)
|
||||
phone: Optional[str] = Field(None, min_length=9, max_length=20, description="Contact phone")
|
||||
email: Optional[str] = Field(None, description="Contact email")
|
||||
|
||||
# Business info
|
||||
business_type: Optional[str] = Field(None, max_length=100, description="Type of business")
|
||||
business_model: Optional[str] = Field(None, max_length=100, description="Business model")
|
||||
|
||||
# Timezone configuration
|
||||
timezone: Optional[str] = Field(None, max_length=50, description="Timezone for scheduling")
|
||||
|
||||
# Additional metadata
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata for the child tenant")
|
||||
|
||||
@field_validator('location_code')
|
||||
@classmethod
|
||||
def validate_location_code(cls, v):
|
||||
@@ -243,10 +253,42 @@ class ChildTenantCreate(BaseModel):
|
||||
raise ValueError('Invalid Spanish phone number')
|
||||
return v
|
||||
|
||||
@field_validator('business_type')
|
||||
@classmethod
|
||||
def validate_business_type(cls, v):
|
||||
"""Validate business type if provided"""
|
||||
if v is None:
|
||||
return v
|
||||
valid_types = ['bakery', 'coffee_shop', 'pastry_shop', 'restaurant']
|
||||
if v not in valid_types:
|
||||
raise ValueError(f'Business type must be one of: {valid_types}')
|
||||
return v
|
||||
|
||||
@field_validator('business_model')
|
||||
@classmethod
|
||||
def validate_business_model(cls, v):
|
||||
"""Validate business model if provided"""
|
||||
if v is None:
|
||||
return v
|
||||
valid_models = ['individual_bakery', 'central_baker_satellite', 'retail_bakery', 'hybrid_bakery']
|
||||
if v not in valid_models:
|
||||
raise ValueError(f'Business model must be one of: {valid_models}')
|
||||
return v
|
||||
|
||||
@field_validator('timezone')
|
||||
@classmethod
|
||||
def validate_timezone(cls, v):
|
||||
"""Validate timezone if provided"""
|
||||
if v is None:
|
||||
return v
|
||||
# Basic timezone validation - should match common timezone formats
|
||||
if not re.match(r'^[A-Za-z_+/]+$', v):
|
||||
raise ValueError('Invalid timezone format')
|
||||
return v
|
||||
|
||||
|
||||
class BulkChildTenantsCreate(BaseModel):
|
||||
"""Schema for bulk creating child tenants during onboarding"""
|
||||
parent_tenant_id: str = Field(..., description="ID of the parent (central baker) tenant")
|
||||
child_tenants: List[ChildTenantCreate] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
|
||||
Reference in New Issue
Block a user