""" Client Profile Schemas - ServiceManagerWeb Esquemas de validación para el perfil empresarial de clientes """ from pydantic import BaseModel, Field, validator, EmailStr from typing import Optional from decimal import Decimal from datetime import datetime import uuid class ClientProfileBase(BaseModel): """Schema base para ClientProfile.""" # === INFORMACIÓN GENERAL === business_name: Optional[str] = Field(None, max_length=255, description="Razón social") commercial_name: Optional[str] = Field(None, max_length=255, description="Nombre comercial") client_code: Optional[str] = Field(None, max_length=50, description="Clave de cliente") client_type: Optional[str] = Field(None, max_length=50, description="Tipo de cliente") rfc: Optional[str] = Field(None, max_length=13, description="RFC (México)") tax_id: Optional[str] = Field(None, max_length=50, description="ID fiscal general") # === UBICACIÓN === country: Optional[str] = Field(None, max_length=100, description="País") state: Optional[str] = Field(None, max_length=100, description="Estado/Provincia") city: Optional[str] = Field(None, max_length=100, description="Ciudad") address: Optional[str] = Field(None, description="Dirección completa") external_number: Optional[str] = Field(None, max_length=20, description="Número exterior") internal_number: Optional[str] = Field(None, max_length=20, description="Número interior") postal_code: Optional[str] = Field(None, max_length=10, description="Código postal") neighborhood: Optional[str] = Field(None, max_length=100, description="Colonia") # === CONTACTO === main_phone: Optional[str] = Field(None, max_length=20, description="Teléfono principal") secondary_phone: Optional[str] = Field(None, max_length=20, description="Teléfono secundario") direct_phone: Optional[str] = Field(None, max_length=20, description="Teléfono directo") phone_extension: Optional[str] = Field(None, max_length=10, description="Extensión") fax: Optional[str] = Field(None, max_length=20, description="Fax") # === INFORMACIÓN ADICIONAL === business_hours: Optional[str] = Field(None, max_length=255, description="Horario de atención") website: Optional[str] = Field(None, max_length=255, description="Página web") main_email: Optional[EmailStr] = Field(None, description="Email principal") billing_email: Optional[EmailStr] = Field(None, description="Email de facturación") # === MARKETING === advertising_medium: Optional[str] = Field(None, max_length=255, description="Medio de publicidad") nationality: Optional[str] = Field(None, max_length=100, description="Nacionalidad") # === CONFIGURACIÓN EMPRESARIAL === logo_url: Optional[str] = Field(None, max_length=500, description="URL del logo") company_representative: Optional[str] = Field(None, max_length=255, description="Representante de empresa") legal_representative: Optional[str] = Field(None, max_length=255, description="Representante legal") # === FINANZAS/FACTURACIÓN === credit_limit: Optional[Decimal] = Field(None, description="Límite de crédito") payment_terms: Optional[str] = Field(None, max_length=100, description="Términos de pago") preferred_currency: str = Field("MXN", max_length=3, description="Moneda preferida") # === METADATOS === send_to_billing: bool = Field(False, description="Enviar a facturación") is_active_client: bool = Field(True, description="Cliente activo") is_prospect: bool = Field(False, description="Es prospecto") notes: Optional[str] = Field(None, description="Notas adicionales") @validator('rfc') def validate_rfc(cls, v): """Validar formato de RFC mexicano.""" if v is None: return v v = v.strip().upper() if len(v) < 10 or len(v) > 13: raise ValueError('RFC debe tener entre 10 y 13 caracteres') # Validación básica de formato RFC import re rfc_pattern = r'^[A-ZÑ&]{3,4}[0-9]{6}[A-Z0-9]{3}$' if not re.match(rfc_pattern, v): raise ValueError('Formato de RFC inválido') return v @validator('postal_code') def validate_postal_code(cls, v): """Validar código postal.""" if v is None: return v v = v.strip() if not v.isdigit() or len(v) != 5: raise ValueError('Código postal debe tener 5 dígitos') return v @validator('website') def validate_website(cls, v): """Validar formato de sitio web.""" if v is None: return v v = v.strip() if not v.startswith(('http://', 'https://')): v = f"https://{v}" import re url_pattern = r'^https?://.+\..+' if not re.match(url_pattern, v): raise ValueError('Formato de sitio web inválido') return v @validator('preferred_currency') def validate_currency(cls, v): """Validar código de moneda.""" valid_currencies = ['MXN', 'USD', 'EUR', 'GBP', 'CAD'] if v not in valid_currencies: raise ValueError(f'Moneda debe ser una de: {", ".join(valid_currencies)}') return v class ClientProfileCreate(ClientProfileBase): """Schema para crear un perfil de cliente.""" pass class ClientProfileUpdate(ClientProfileBase): """Schema para actualizar un perfil de cliente.""" pass class ClientProfileResponse(ClientProfileBase): """Schema de respuesta para ClientProfile.""" id: uuid.UUID tenant_id: uuid.UUID created_at: datetime updated_at: datetime class Config: from_attributes = True @property def full_address(self) -> str: """Dirección completa formateada.""" address_parts = [] if self.address: address_parts.append(self.address) if self.external_number: if self.internal_number: address_parts.append(f"#{self.external_number}-{self.internal_number}") else: address_parts.append(f"#{self.external_number}") if self.neighborhood: address_parts.append(f"Col. {self.neighborhood}") if self.city and self.state: address_parts.append(f"{self.city}, {self.state}") if self.postal_code: address_parts.append(f"C.P. {self.postal_code}") if self.country: address_parts.append(self.country) return ", ".join(address_parts) @property def display_name(self) -> str: """Nombre para mostrar.""" return self.commercial_name or self.business_name or "Sin nombre" class ClientProfileSummary(BaseModel): """Schema resumido para listados.""" id: uuid.UUID tenant_id: uuid.UUID business_name: Optional[str] commercial_name: Optional[str] rfc: Optional[str] client_code: Optional[str] city: Optional[str] state: Optional[str] main_phone: Optional[str] main_email: Optional[str] is_active_client: bool is_prospect: bool created_at: datetime updated_at: datetime class Config: from_attributes = True @property def display_name(self) -> str: """Nombre para mostrar.""" return self.commercial_name or self.business_name or "Sin nombre"