""" Advanced Template Variables API This API provides comprehensive variable management for document templates including: - Variable definition and configuration - Context-specific value management - Advanced processing with conditional logic and calculations - Variable testing and validation """ from __future__ import annotations from typing import List, Optional, Dict, Any, Union from fastapi import APIRouter, Depends, HTTPException, status, Query, Body from sqlalchemy.orm import Session, joinedload from sqlalchemy import func, or_, and_ from pydantic import BaseModel, Field from datetime import datetime from app.database.base import get_db from app.auth.security import get_current_user from app.models.user import User from app.models.template_variables import ( TemplateVariable, VariableContext, VariableAuditLog, VariableType, VariableTemplate, VariableGroup ) from app.services.advanced_variables import VariableProcessor from app.services.query_utils import paginate_with_total router = APIRouter() # Pydantic schemas for API class VariableCreate(BaseModel): name: str = Field(..., max_length=100, description="Unique variable name") display_name: Optional[str] = Field(None, max_length=200) description: Optional[str] = None variable_type: VariableType = VariableType.STRING required: bool = False default_value: Optional[str] = None formula: Optional[str] = None conditional_logic: Optional[Dict[str, Any]] = None data_source_query: Optional[str] = None lookup_table: Optional[str] = None lookup_key_field: Optional[str] = None lookup_value_field: Optional[str] = None validation_rules: Optional[Dict[str, Any]] = None format_pattern: Optional[str] = None depends_on: Optional[List[str]] = None scope: str = "global" category: Optional[str] = None tags: Optional[List[str]] = None cache_duration_minutes: int = 0 class VariableUpdate(BaseModel): display_name: Optional[str] = None description: Optional[str] = None required: Optional[bool] = None active: Optional[bool] = None default_value: Optional[str] = None formula: Optional[str] = None conditional_logic: Optional[Dict[str, Any]] = None data_source_query: Optional[str] = None lookup_table: Optional[str] = None lookup_key_field: Optional[str] = None lookup_value_field: Optional[str] = None validation_rules: Optional[Dict[str, Any]] = None format_pattern: Optional[str] = None depends_on: Optional[List[str]] = None category: Optional[str] = None tags: Optional[List[str]] = None cache_duration_minutes: Optional[int] = None class VariableResponse(BaseModel): id: int name: str display_name: Optional[str] description: Optional[str] variable_type: VariableType required: bool active: bool default_value: Optional[str] scope: str category: Optional[str] tags: Optional[List[str]] created_at: datetime updated_at: Optional[datetime] class Config: from_attributes = True class VariableContextSet(BaseModel): variable_name: str value: Any context_type: str = "global" context_id: str = "default" class VariableTestRequest(BaseModel): variables: List[str] context_type: str = "global" context_id: str = "default" test_context: Optional[Dict[str, Any]] = None class VariableTestResponse(BaseModel): resolved: Dict[str, Any] unresolved: List[str] processing_time_ms: float errors: List[str] class VariableAuditResponse(BaseModel): id: int variable_name: str context_type: Optional[str] context_id: Optional[str] old_value: Optional[str] new_value: Optional[str] change_type: str change_reason: Optional[str] changed_by: Optional[str] changed_at: datetime class Config: from_attributes = True @router.post("/variables/", response_model=VariableResponse) async def create_variable( variable_data: VariableCreate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Create a new template variable with advanced features""" # Check if variable name already exists existing = db.query(TemplateVariable).filter( TemplateVariable.name == variable_data.name ).first() if existing: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Variable with name '{variable_data.name}' already exists" ) # Validate dependencies if variable_data.depends_on: for dep_name in variable_data.depends_on: dep_var = db.query(TemplateVariable).filter( TemplateVariable.name == dep_name, TemplateVariable.active == True ).first() if not dep_var: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=f"Dependency variable '{dep_name}' not found" ) # Create variable variable = TemplateVariable( name=variable_data.name, display_name=variable_data.display_name, description=variable_data.description, variable_type=variable_data.variable_type, required=variable_data.required, default_value=variable_data.default_value, formula=variable_data.formula, conditional_logic=variable_data.conditional_logic, data_source_query=variable_data.data_source_query, lookup_table=variable_data.lookup_table, lookup_key_field=variable_data.lookup_key_field, lookup_value_field=variable_data.lookup_value_field, validation_rules=variable_data.validation_rules, format_pattern=variable_data.format_pattern, depends_on=variable_data.depends_on, scope=variable_data.scope, category=variable_data.category, tags=variable_data.tags, cache_duration_minutes=variable_data.cache_duration_minutes, created_by=current_user.username, active=True ) db.add(variable) db.commit() db.refresh(variable) return variable @router.get("/variables/", response_model=List[VariableResponse]) async def list_variables( skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), category: Optional[str] = Query(None), variable_type: Optional[VariableType] = Query(None), active_only: bool = Query(True), search: Optional[str] = Query(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """List template variables with filtering options""" query = db.query(TemplateVariable) if active_only: query = query.filter(TemplateVariable.active == True) if category: query = query.filter(TemplateVariable.category == category) if variable_type: query = query.filter(TemplateVariable.variable_type == variable_type) if search: search_filter = f"%{search}%" query = query.filter( or_( TemplateVariable.name.ilike(search_filter), TemplateVariable.display_name.ilike(search_filter), TemplateVariable.description.ilike(search_filter) ) ) query = query.order_by(TemplateVariable.category, TemplateVariable.name) variables, _ = paginate_with_total(query, skip, limit, False) return variables @router.get("/variables/{variable_id}", response_model=VariableResponse) async def get_variable( variable_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get a specific variable by ID""" variable = db.query(TemplateVariable).filter( TemplateVariable.id == variable_id ).first() if not variable: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Variable not found" ) return variable @router.put("/variables/{variable_id}", response_model=VariableResponse) async def update_variable( variable_id: int, variable_data: VariableUpdate, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update a template variable""" variable = db.query(TemplateVariable).filter( TemplateVariable.id == variable_id ).first() if not variable: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Variable not found" ) # Update fields that are provided update_data = variable_data.dict(exclude_unset=True) for field, value in update_data.items(): setattr(variable, field, value) db.commit() db.refresh(variable) return variable @router.delete("/variables/{variable_id}") async def delete_variable( variable_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Delete a template variable (soft delete by setting active=False)""" variable = db.query(TemplateVariable).filter( TemplateVariable.id == variable_id ).first() if not variable: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Variable not found" ) # Soft delete variable.active = False db.commit() return {"message": "Variable deleted successfully"} @router.post("/variables/test", response_model=VariableTestResponse) async def test_variables( test_request: VariableTestRequest, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Test variable resolution with given context""" import time start_time = time.time() errors = [] try: processor = VariableProcessor(db) resolved, unresolved = processor.resolve_variables( variables=test_request.variables, context_type=test_request.context_type, context_id=test_request.context_id, base_context=test_request.test_context or {} ) processing_time = (time.time() - start_time) * 1000 return VariableTestResponse( resolved=resolved, unresolved=unresolved, processing_time_ms=processing_time, errors=errors ) except Exception as e: processing_time = (time.time() - start_time) * 1000 errors.append(str(e)) return VariableTestResponse( resolved={}, unresolved=test_request.variables, processing_time_ms=processing_time, errors=errors ) @router.post("/variables/set-value") async def set_variable_value( context_data: VariableContextSet, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Set a variable value in a specific context""" processor = VariableProcessor(db) success = processor.set_variable_value( variable_name=context_data.variable_name, value=context_data.value, context_type=context_data.context_type, context_id=context_data.context_id, user_name=current_user.username ) if not success: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Failed to set variable value" ) return {"message": "Variable value set successfully"} @router.get("/variables/{variable_id}/contexts") async def get_variable_contexts( variable_id: int, context_type: Optional[str] = Query(None), skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all contexts where this variable has values""" query = db.query(VariableContext).filter( VariableContext.variable_id == variable_id ) if context_type: query = query.filter(VariableContext.context_type == context_type) query = query.order_by(VariableContext.context_type, VariableContext.context_id) contexts, total = paginate_with_total(query, skip, limit, True) return { "items": [ { "context_type": ctx.context_type, "context_id": ctx.context_id, "value": ctx.value, "computed_value": ctx.computed_value, "is_valid": ctx.is_valid, "validation_errors": ctx.validation_errors, "last_computed_at": ctx.last_computed_at } for ctx in contexts ], "total": total } @router.get("/variables/{variable_id}/audit", response_model=List[VariableAuditResponse]) async def get_variable_audit_log( variable_id: int, skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get audit log for a variable""" query = db.query(VariableAuditLog, TemplateVariable.name).join( TemplateVariable, VariableAuditLog.variable_id == TemplateVariable.id ).filter( VariableAuditLog.variable_id == variable_id ).order_by(VariableAuditLog.changed_at.desc()) audit_logs, _ = paginate_with_total(query, skip, limit, False) return [ VariableAuditResponse( id=log.id, variable_name=var_name, context_type=log.context_type, context_id=log.context_id, old_value=log.old_value, new_value=log.new_value, change_type=log.change_type, change_reason=log.change_reason, changed_by=log.changed_by, changed_at=log.changed_at ) for log, var_name in audit_logs ] @router.get("/templates/{template_id}/variables") async def get_template_variables( template_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all variables associated with a template""" processor = VariableProcessor(db) variables = processor.get_variables_for_template(template_id) return {"variables": variables} @router.post("/templates/{template_id}/variables/{variable_id}") async def associate_variable_with_template( template_id: int, variable_id: int, override_default: Optional[str] = Body(None), override_required: Optional[bool] = Body(None), display_order: int = Body(0), group_name: Optional[str] = Body(None), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Associate a variable with a template""" # Check if association already exists existing = db.query(VariableTemplate).filter( VariableTemplate.template_id == template_id, VariableTemplate.variable_id == variable_id ).first() if existing: # Update existing association existing.override_default = override_default existing.override_required = override_required existing.display_order = display_order existing.group_name = group_name else: # Create new association association = VariableTemplate( template_id=template_id, variable_id=variable_id, override_default=override_default, override_required=override_required, display_order=display_order, group_name=group_name ) db.add(association) db.commit() return {"message": "Variable associated with template successfully"} @router.delete("/templates/{template_id}/variables/{variable_id}") async def remove_variable_from_template( template_id: int, variable_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Remove variable association from template""" association = db.query(VariableTemplate).filter( VariableTemplate.template_id == template_id, VariableTemplate.variable_id == variable_id ).first() if not association: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Variable association not found" ) db.delete(association) db.commit() return {"message": "Variable removed from template successfully"} @router.get("/categories") async def get_variable_categories( db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get list of variable categories""" categories = db.query( TemplateVariable.category, func.count(TemplateVariable.id).label('count') ).filter( TemplateVariable.active == True, TemplateVariable.category.isnot(None) ).group_by(TemplateVariable.category).order_by(TemplateVariable.category).all() return [ {"category": cat, "count": count} for cat, count in categories ]