from pydantic import BaseModel, constr, field_validator from fastapi import HTTPException, status from typing import List from typing import Optional from datetime import datetime from decimal import Decimal # Importar Decimal para campos monetários # Funções para sanitização de entradas (evitar XSS, SQLi etc.) from actions.validations.text import Text # Funções para sanitização de entradas (evitar XSS, SQLi etc.) # É importante que esta função seja mantida/implementada no seu ambiente # from actions.validations.text import Text # Descomentar se for usar # Schemas dos itens from packages.v1.administrativo.schemas.ato_parte_schema import AtoParteSaveSchema from packages.v1.administrativo.schemas.ato_documento_schema import ( AtoDocumentoSaveSchema, ) # ---------------------------------------------------- # Schema Base: Usado para leitura (GET, SHOW, INDEX) # Inclui todos os campos, incluindo os gerados pelo banco # ---------------------------------------------------- class AtoPrincipalSchema(BaseModel): # DDL: bigint NOT NULL AUTO_INCREMENT ato_principal_id: Optional[int] = None # DDL: bigint DEFAULT NULL origem_ato_principal_id: Optional[int] = None # DDL: bigint NOT NULL identificacao_pedido_cgj: int # DDL: int NOT NULL tipo_ato: int # DDL: varchar(50) NOT NULL (UNIQUE) codigo_selo: constr(max_length=50) # DDL: varchar(50) NOT NULL (UNIQUE) codigo_ato: constr(max_length=50) # DDL: varchar(255) NOT NULL (mas o banco pode conter valores NULL em registros antigos) nome_civil_ato: Optional[constr(max_length=255)] = None # DDL: varchar(255) NOT NULL (mas o banco pode conter valores NULL em registros antigos) nome_serventuario_praticou_ato: Optional[constr(max_length=255)] = None # DDL: datetime NOT NULL data_solicitacao: datetime # DDL: varchar(45) DEFAULT NULL ip_maquina: Optional[constr(max_length=45)] = None # DDL: text NOT NULL inteiro_teor: Optional[str] = None # permite valor nulo # DDL: decimal(12,2) DEFAULT NULL valor_entrada: Optional[Decimal] = None # DDL: decimal(12,2) NOT NULL emolumento: Decimal # DDL: decimal(12,2) NOT NULL taxa_judiciaria: Decimal # DDL: decimal(12,2) NOT NULL fundos_estaduais: Decimal # DDL: varchar(50) DEFAULT NULL protocolo_protesto: Optional[constr(max_length=50)] = None # DDL: varchar(50) DEFAULT NULL protocolo_imovel: Optional[constr(max_length=50)] = None # DDL: datetime NOT NULL DEFAULT CURRENT_TIMESTAMP created_at: Optional[datetime] = None # DDL: datetime NOT NULL DEFAULT CURRENT_TIMESTAMP updated_at: Optional[datetime] = None class Config: from_attributes = True # Permite a conversão de float/int para Decimal json_encoders = {Decimal: lambda v: float(v)} # Define a precisão para Decimal decimal_places = 2 # ---------------------------------------------------- # Schema para Requisição de ID: Usado em SHOW e DELETE # ---------------------------------------------------- class AtoPrincipalIdSchema(BaseModel): ato_principal_id: int @field_validator("ato_principal_id") def validate_ato_principal_id(cls, v: int): if v <= 0: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "ato_principal_id", "message": "ID do Ato Principal deve ser um valor positivo.", } ], ) return v # ---------------------------------------------------- # Schema para Requisição de Código: Usado em SHOW e DELETE # ---------------------------------------------------- class AtoPrincipalCodigoAtoSchema(BaseModel): codigo_ato: str # Campo string equivalente ao varchar(50) @field_validator("codigo_ato") def validate_codigo_ato(cls, v: str): # Remove espaços extras v = v.strip() # Verifica se está vazio if not v: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "codigo_ato", "message": "O campo 'código do ato' é obrigatório.", } ], ) # Verifica o tamanho máximo (equivalente a varchar(50)) if len(v) > 50: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "codigo_ato", "message": "O campo 'código do ato' deve ter no máximo 50 caracteres.", } ], ) return Text.sanitize_input(v) # ---------------------------------------------------- # Schema para Criação (SAVE): Campos obrigatórios e sem ID # ---------------------------------------------------- class AtoPrincipalSaveSchema(BaseModel): ato_principal_id: Optional[int] = None # Opcional no SAVE origem_ato_principal_id: Optional[int] = None # Campos obrigatórios (NOT NULL) identificacao_pedido_cgj: int tipo_ato: int codigo_selo: constr(max_length=50) codigo_ato: constr(max_length=50) nome_civil_ato: constr(max_length=255) nome_serventuario_praticou_ato: constr(max_length=255) data_solicitacao: datetime inteiro_teor: str # Campos opcionais ip_maquina: Optional[constr(max_length=45)] = None valor_entrada: Optional[Decimal] = None # Aceita None emolumento: Decimal = Decimal(0) # Valor padrão = 0 taxa_judiciaria: Decimal = Decimal(0) # Valor padrão = 0 fundos_estaduais: Decimal = Decimal(0) # Valor padrão = 0 protocolo_protesto: Optional[constr(max_length=50)] = None protocolo_imovel: Optional[constr(max_length=50)] = None # Relações ato_partes: List[AtoParteSaveSchema] = [] ato_documentos: List[AtoDocumentoSaveSchema] = [] atos_vinculados: Optional[List["AtoPrincipalSaveSchema"]] = ( None # referência recursiva ) # ---------------------------------------------------- # Validação e sanitização de campos de texto obrigatórios # ---------------------------------------------------- @field_validator( "codigo_selo", "codigo_ato", "nome_civil_ato", "nome_serventuario_praticou_ato", "inteiro_teor", ) def validate_required_strings(cls, v: str, info): if not v or not v.strip(): raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": info.field_name, "message": f"O campo '{info.field_name}' não pode ser vazio.", } ], ) # Sanitiza o texto antes de retornar return Text.sanitize_input(v.strip()) # ---------------------------------------------------- # Validação de campos monetários (aceita 0 e None em valor_entrada) # ---------------------------------------------------- @field_validator( "valor_entrada", "emolumento", "taxa_judiciaria", "fundos_estaduais" ) def validate_positive_values(cls, v: Optional[Decimal], info): # valor_entrada pode ser None if info.field_name == "valor_entrada" and v is None: return v # Se valor foi informado, valida if v is not None and v < 0: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": info.field_name, "message": "Os valores monetários não podem ser negativos.", } ], ) # Retorna o valor (inclusive 0 é aceito) return v class Config: from_attributes = True json_encoders = {Decimal: lambda v: float(v)} decimal_places = 2 # ---------------------------------------------------- # Schema para Atualização (UPDATE): Todos opcionais (parciais) # ---------------------------------------------------- class AtoPrincipalUpdateSchema(BaseModel): # Todos os campos que podem ser atualizados são opcionais origem_ato_principal_id: Optional[int] = None identificacao_pedido_cgj: Optional[int] = None tipo_ato: Optional[int] = None codigo_selo: Optional[constr(max_length=50)] = None codigo_ato: Optional[constr(max_length=50)] = None nome_civil_ato: Optional[constr(max_length=255)] = None nome_serventuario_praticou_ato: Optional[constr(max_length=255)] = None data_solicitacao: Optional[datetime] = None ip_maquina: Optional[constr(max_length=45)] = None inteiro_teor: Optional[str] = None valor_entrada: Optional[Decimal] = None emolumento: Optional[Decimal] = None taxa_judiciaria: Optional[Decimal] = None fundos_estaduais: Optional[Decimal] = None protocolo_protesto: Optional[constr(max_length=50)] = None protocolo_imovel: Optional[constr(max_length=50)] = None # Reutiliza a validação de strings obrigatórias (só valida se o campo for fornecido) @field_validator( "codigo_selo", "codigo_ato", "nome_civil_ato", "nome_serventuario_praticou_ato", "inteiro_teor", ) def validate_non_empty_strings_on_update(cls, v: Optional[str]): if v is not None: v_stripped = v.strip() if not v_stripped: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "campo_de_texto", "message": "Este campo não pode ser atualizado para um valor vazio.", } ], ) return Text.sanitize_input(v) # Reutiliza a validação de valores positivos @field_validator( "valor_entrada", "emolumento", "taxa_judiciaria", "fundos_estaduais" ) def validate_positive_values_on_update(cls, v: Optional[Decimal]): if v is not None and v < 0: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "campo_monetario", "message": "Os valores monetários não podem ser negativos.", } ], ) return Text.sanitize_input(v) class Config: from_attributes = True json_encoders = {Decimal: lambda v: float(v)} decimal_places = 2 # ---------------------------------------------------- # Schema auxiliar para buscar por campos únicos (opcional, mas útil para o Save Service) # ---------------------------------------------------- class AtoPrincipalCodigoSchema(BaseModel): codigo: constr(max_length=50) # Pode ser codigo_selo ou codigo_ato @field_validator("codigo") def validate_codigo(cls, v: str): v = v.strip() if not v: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=[ { "input": "codigo", "message": "O código de busca não pode ser vazio.", } ], ) return v # --- ADIÇÃO CRÍTICA PARA RECURSIVIDADE (Resolução da forward reference) --- # Necessário para resolver a referência da classe dentro de si mesma. # Tenta Pydantic v2 (model_rebuild) e, se falhar, Pydantic v1 (update_forward_refs). try: AtoPrincipalSaveSchema.model_rebuild() except AttributeError: # Se estiver usando Pydantic v1 AtoPrincipalSaveSchema.update_forward_refs()