From 6406f1095833d2899e718e2fceb1c3cd41443115 Mon Sep 17 00:00:00 2001 From: Kenio de Souza Date: Fri, 17 Oct 2025 10:23:31 -0300 Subject: [PATCH] =?UTF-8?q?Cria=C3=A7=C3=A3o=20dos=20endpoint's=20da=20tab?= =?UTF-8?q?ela=20client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../actions/client/client_delete_action.py | 25 +++ .../actions/client/client_index_action.py | 31 ++++ .../actions/client/client_save_action.py | 27 +++ .../actions/client/client_show_action.py | 31 ++++ .../actions/client/client_update_action.py | 28 ++++ .../controllers/client_controller.py | 156 ++++++++++++++++++ .../endpoints/client_endpoint.py | 133 +++++++++++++++ .../client/client_delete_repository.py | 38 +++++ .../client/client_index_repository.py | 51 ++++++ .../client/client_save_repository.py | 74 +++++++++ .../client/client_show_repository.py | 60 +++++++ .../client/client_update_repository.py | 87 ++++++++++ .../administrativo/schemas/client_schema.py | 113 +++++++++++++ .../services/client/client_delete_service.py | 26 +++ .../services/client/client_index_service.py | 36 ++++ .../services/client/client_save_service.py | 64 +++++++ .../services/client/client_show_service.py | 34 ++++ .../services/client/client_update_service.py | 65 ++++++++ packages/v1/api.py | 7 + 19 files changed, 1086 insertions(+) create mode 100644 packages/v1/administrativo/actions/client/client_delete_action.py create mode 100644 packages/v1/administrativo/actions/client/client_index_action.py create mode 100644 packages/v1/administrativo/actions/client/client_save_action.py create mode 100644 packages/v1/administrativo/actions/client/client_show_action.py create mode 100644 packages/v1/administrativo/actions/client/client_update_action.py create mode 100644 packages/v1/administrativo/controllers/client_controller.py create mode 100644 packages/v1/administrativo/endpoints/client_endpoint.py create mode 100644 packages/v1/administrativo/repositories/client/client_delete_repository.py create mode 100644 packages/v1/administrativo/repositories/client/client_index_repository.py create mode 100644 packages/v1/administrativo/repositories/client/client_save_repository.py create mode 100644 packages/v1/administrativo/repositories/client/client_show_repository.py create mode 100644 packages/v1/administrativo/repositories/client/client_update_repository.py create mode 100644 packages/v1/administrativo/schemas/client_schema.py create mode 100644 packages/v1/administrativo/services/client/client_delete_service.py create mode 100644 packages/v1/administrativo/services/client/client_index_service.py create mode 100644 packages/v1/administrativo/services/client/client_save_service.py create mode 100644 packages/v1/administrativo/services/client/client_show_service.py create mode 100644 packages/v1/administrativo/services/client/client_update_service.py diff --git a/packages/v1/administrativo/actions/client/client_delete_action.py b/packages/v1/administrativo/actions/client/client_delete_action.py new file mode 100644 index 0000000..b51d386 --- /dev/null +++ b/packages/v1/administrativo/actions/client/client_delete_action.py @@ -0,0 +1,25 @@ +from packages.v1.administrativo.schemas.client_schema import ClientIdSchema +from packages.v1.administrativo.repositories.client.client_delete_repository import ClientDeleteRepository + + +class ClientDeleteAction: + """ + Action para a exclusão de um registro na tabela 'client'. + Utiliza o schema com o ID do cliente e delega a operação ao repositório. + """ + + def execute(self, client_schema: ClientIdSchema): + """ + Executa a lógica de exclusão do cliente. + + A exclusão requer apenas a chave primária ('client_id'), que deve ser + encapsulada no schema 'ClientIdSchema'. + + :param client_schema: Schema contendo o ID do cliente a ser excluído. + :return: Resultado da operação de exclusão do repositório. + """ + # Instancia o repositório específico para a exclusão de clientes + delete_repository = ClientDeleteRepository() + + # Chama o método execute do repositório, passando o schema do cliente + return delete_repository.execute(client_schema) \ No newline at end of file diff --git a/packages/v1/administrativo/actions/client/client_index_action.py b/packages/v1/administrativo/actions/client/client_index_action.py new file mode 100644 index 0000000..d351e12 --- /dev/null +++ b/packages/v1/administrativo/actions/client/client_index_action.py @@ -0,0 +1,31 @@ +from abstracts.action import BaseAction +# O repositório deve ser adaptado para a listagem (indexação) da tabela 'client' +from packages.v1.administrativo.repositories.client.client_index_repository import ClientIndexRepository +from typing import Tuple, List, Dict, Any + + +class ClientIndexAction(BaseAction): + """ + Action responsável por orquestrar a listagem (indexação) de todos + os registros da tabela 'client' com suporte a paginação. + """ + + # O método execute recebe 'first' e 'skip' para paginação + def execute(self, first: int, skip: int) -> Tuple[List[Dict[str, Any]], int]: + """ + Executa a lógica de listagem de clientes com paginação. + + :param first: Número máximo de registros a retornar (LIMIT). + :param skip: Número de registros a pular (OFFSET). + :return: Tupla com a lista de clientes e o total de registros. + """ + # Instânciamento do repositório de indexação (listagem) de clientes + # Supondo que ClientIndexRepository é onde a lógica de acesso ao BD está (SELECT * FROM client LIMIT first OFFSET skip) + client_index_repository = ClientIndexRepository() + + # Execução do repositório para buscar os clientes com paginação + # A resposta (response) conteria os campos da DDL: client_id, cns, name, date_register, state, city, responsible, consultant, type_contract + response, total_records = client_index_repository.execute(first, skip) + + # Retorno da informação + return response, total_records \ No newline at end of file diff --git a/packages/v1/administrativo/actions/client/client_save_action.py b/packages/v1/administrativo/actions/client/client_save_action.py new file mode 100644 index 0000000..9f747ba --- /dev/null +++ b/packages/v1/administrativo/actions/client/client_save_action.py @@ -0,0 +1,27 @@ +from packages.v1.administrativo.schemas.client_schema import ClientSaveSchema +from packages.v1.administrativo.repositories.client.client_save_repository import ClientSaveRepository + + +class ClientSaveAction: + """ + Action responsável por orquestrar a operação de salvar (inserir ou atualizar) + um registro na tabela 'client'. + """ + + def execute(self, client_schema: ClientSaveSchema): + """ + Executa a lógica de salvamento do cliente. + + O schema 'ClientSaveSchema' deve conter todos os campos necessários + para a operação de persistência, baseados na DDL: + cns, name, date_register (opcional na entrada, pois tem DEFAULT), + state, city, responsible, consultant, e type_contract. + + :param client_schema: Schema contendo os dados do cliente a serem salvos. + :return: Resultado da operação de salvamento do repositório. + """ + # Instancia o repositório específico para a operação de salvar clientes + save_repository = ClientSaveRepository() + + # Chama o método execute do repositório, passando o objeto schema + return save_repository.execute(client_schema) \ No newline at end of file diff --git a/packages/v1/administrativo/actions/client/client_show_action.py b/packages/v1/administrativo/actions/client/client_show_action.py new file mode 100644 index 0000000..b75dc90 --- /dev/null +++ b/packages/v1/administrativo/actions/client/client_show_action.py @@ -0,0 +1,31 @@ +from abstracts.action import BaseAction +from packages.v1.administrativo.schemas.client_schema import ClientSchema +from packages.v1.administrativo.repositories.client.client_show_repository import ClientShowRepository + + +class ClientShowAction(BaseAction): + """ + Action responsável por orquestrar a visualização (show) de um registro + único na tabela 'client', geralmente utilizando o 'client_id'. + """ + + def execute(self, client_schema: ClientSchema): + """ + Executa a lógica de busca e exibição do cliente. + + O schema 'ClientSchema' é usado para transportar o 'client_id', que + será o critério principal para buscar os dados completos do cliente: + cns, name, date_register, state, city, responsible, consultant, + e type_contract. + + :param client_schema: Schema contendo o ID do cliente a ser exibido. + :return: O registro de cliente encontrado ou None/erro. + """ + # Instânciamento do repositório de visualização (show) + show_repository = ClientShowRepository() + + # Execução do repositório + response = show_repository.execute(client_schema) + + # Retorno da informação + return response \ No newline at end of file diff --git a/packages/v1/administrativo/actions/client/client_update_action.py b/packages/v1/administrativo/actions/client/client_update_action.py new file mode 100644 index 0000000..1bde432 --- /dev/null +++ b/packages/v1/administrativo/actions/client/client_update_action.py @@ -0,0 +1,28 @@ +from packages.v1.administrativo.schemas.client_schema import ClientUpdateSchema +from packages.v1.administrativo.repositories.client.client_update_repository import ClientUpdateRepository + + +class ClientUpdateAction: + """ + Action responsável por orquestrar a operação de atualização (UPDATE) + de um registro na tabela 'client', identificado pelo seu ID. + """ + + def execute(self, client_id: int, client_schema: ClientUpdateSchema): + """ + Executa a lógica de atualização do cliente. + + O 'client_id' identifica qual registro será modificado, e o + 'client_schema' contém os novos valores para os campos + (cns, name, state, city, responsible, consultant, type_contract). + O campo 'date_register' geralmente é omitido ou atualizado automaticamente. + + :param client_id: ID do cliente a ser atualizado. + :param client_schema: Schema contendo os novos dados do cliente. + :return: Resultado da operação de atualização do repositório. + """ + # Instancia o repositório específico para a operação de atualização de clientes + update_repository = ClientUpdateRepository() + + # Chama o método execute do repositório, passando o ID e o objeto schema + return update_repository.execute(client_id, client_schema) \ No newline at end of file diff --git a/packages/v1/administrativo/controllers/client_controller.py b/packages/v1/administrativo/controllers/client_controller.py new file mode 100644 index 0000000..04ed5a6 --- /dev/null +++ b/packages/v1/administrativo/controllers/client_controller.py @@ -0,0 +1,156 @@ +from actions.dynamic_import.dynamic_import import DynamicImport +# Adaptando os Schemas para a entidade 'Client' +from packages.v1.administrativo.schemas.client_schema import ( + ClientSchema, + ClientAuthenticateSchema, + ClientSaveSchema, + ClientUpdateSchema, + ClientIdSchema, + ClientFileSchema, + ClientCNSchema # Adaptando 'LogClientIdSchema' para um campo relevante de Cliente, como o 'cns' +) + +import json # Necessário para carregar o arquivo app.json +import math + +# Carrega as configurações de paginação do app.json +with open('config/app.json', 'r') as f: + app_config = json.load(f) + PAGINATION_FIRST = app_config.get('pagination', {}).get('first', 20) + PAGINATION_SKIP = app_config.get('pagination', {}).get('skip', 0) + + +class ClientController: + """ + Controller responsável por orquestrar as operações (CRUD e outras buscas) + para a tabela 'client'. + """ + + def __init__(self): + # Action responsável por carregar as services de acordo com o estado + self.dynamic_import = DynamicImport() + + # Define o pacote que deve ser carregado + self.dynamic_import.set_package("administrativo") + + # Define a tabela que o pacote pertence + self.dynamic_import.set_table("client") + pass + + + # Lista todos os clientes com paginação + def index(self, first: int = PAGINATION_FIRST, skip: int = PAGINATION_SKIP): + + # Importação da classe desejada + indexService = self.dynamic_import.service("client_index_service", "IndexService") + + # Instânciamento da classe service + self.indexService = indexService() + + # Lista todos os clientes, recebendo a lista de dados e o total de registros + data, total_records = self.indexService.execute(first, skip) + + # Cálculo dos metadados de paginação + total_pages = math.ceil(total_records / first) + current_page = (skip // first) + 1 + + next_page = None + # Verifica se existe uma próxima página + if current_page < total_pages: + next_page = current_page + 1 + + # Retorna a lista de clientes e os metadados de paginação + return { + 'message': 'Clientes localizados com sucesso', + 'data': data, + 'pagination': { + 'total_records': total_records, + 'total_pages': total_pages, + 'current_page': current_page, + 'next_page': next_page, + 'first': first, # Total de registros por página + 'skip': skip # Registros pulados + } + } + + + # Busca um cliente específico pelo cns (Adaptado de logClient) + def getByCns(self, client_schema: ClientCNSchema): + + #Importação da classe desejada + client_cns_service = self.dynamic_import.service('client_cns_service', 'ClientCNSService') + + # Instânciamento da classe desejada + self.client_cns_service = client_cns_service() + + # Busca e retorna o cliente desejado + return { + 'message': 'Cliente(s) localizados com sucesso pelo CNS', + 'data': self.client_cns_service.execute(client_schema) + } + + # Busca um cliente específico pelo ID (client_id) + def show(self, client_schema: ClientSchema): + + #Importação da classe desejada + show_service = self.dynamic_import.service('client_show_service', 'ShowService') + + # Instânciamento da classe desejada + self.show_service = show_service() + + # Busca e retorna o cliente desejado + return { + 'message': 'Cliente localizado com sucesso', + 'data': self.show_service.execute(client_schema) + } + + # Cadastra um novo cliente + def save(self, client_schema: ClientSaveSchema): + + #Importação da classe desejada + save_service = self.dynamic_import.service('client_save_service', 'ClientSaveService') + + # Instânciamento da classe desejada + self.save_service = save_service() + + # Busca e retorna o cliente desejado + return { + 'message': 'Cliente salvo com sucesso', + 'data': self.save_service.execute(client_schema) + } + + # Atualiza os dados de um cliente + def update(self, client_id: int, client_schema: ClientUpdateSchema): + + #Importação da classe desejada + update_service = self.dynamic_import.service('client_update_service', 'ClientUpdateService') + + # Instânciamento da classe desejada + self.update_service = update_service() + + # Busca e retorna o cliente desejado + return { + 'message': 'Cliente atualizado com sucesso', + 'data': self.update_service.execute(client_id, client_schema) + } + + # Exclui um cliente + def delete(self, client_schema: ClientIdSchema): + + #Importação da classe desejada + delete_service = self.dynamic_import.service('client_delete_service', 'DeleteService') + + # Instânciamento da classe desejada + self.delete_service = delete_service() + + # Busca e retorna o cliente desejado + return { + 'message': 'Cliente removido com sucesso', + 'data': self.delete_service.execute(client_schema) + } + + + # Métodos específicos do Log que não se aplicam diretamente a Client foram removidos ou adaptados: + # getGed, getServer, getDatabase, getBackup, getDisk, getWarning (Estes parecem ser específicos de logs/monitoramento). + # Mantendo apenas as operações CRUD e buscas por campos relevantes (CNS, State, ID). + pass \ No newline at end of file diff --git a/packages/v1/administrativo/endpoints/client_endpoint.py b/packages/v1/administrativo/endpoints/client_endpoint.py new file mode 100644 index 0000000..a1a6e57 --- /dev/null +++ b/packages/v1/administrativo/endpoints/client_endpoint.py @@ -0,0 +1,133 @@ +# Importação de bibliotecas +from typing import Optional +from fastapi import APIRouter, Body, Depends, status +from actions.jwt.get_current_user import get_current_user +from packages.v1.administrativo.controllers.user_controller import UserController +from packages.v1.administrativo.schemas.user_schema import ( + UserSchema, + UserAuthenticateSchema, + UserSaveSchema, + UserUpdateSchema, + UserEmailSchema, + UserIdSchema +) + +# Inicializa o roteador para as rotas de usuário +router = APIRouter() + +# Instânciamento do controller desejado +user_controller = UserController() + +# Autenticação de usuário +@router.post('/authenticate', + status_code=status.HTTP_200_OK, + summary='Cria o token de acesso do usuário', + response_description='Retorna o token de acesso do usuário') +async def index(user_authenticate_schema : UserAuthenticateSchema): + + # Efetua a autenticação de um usuário junto ao sistema + response = user_controller.authenticate(user_authenticate_schema) + + # Retorna os dados localizados + return response + +# Dados do usuário logado +@router.get('/me', + status_code=status.HTTP_200_OK, + summary='Retorna os dados do usuário que efetuou o login', + response_description='Dados do usuário que efetuou o login' ) +async def me(current_user: dict = Depends(get_current_user)): + + # Busca os dados do usuário logado + response = user_controller.me(current_user) + + # Retorna os dados localizados + return response + +# Lista todos os usuários +@router.get('/', + status_code=status.HTTP_200_OK, + summary='Lista todos os usuário cadastrados', + response_description='Lista todos os usuário cadastrados') +async def index(current_user: dict = Depends(get_current_user)): + + # Busca todos os usuários cadastrados + response = user_controller.index() + + # Retorna os dados localizados + return response + +# Localiza um usuário pelo email +@router.get('/email', + status_code=status.HTTP_200_OK, + summary='Busca um registro em especifico por e-mail informado', + response_description='Busca um registro em especifico') +async def getEmail(email : str, current_user: dict = Depends(get_current_user)): + + # Cria o schema com os dados recebidos + usuario_schema = UserEmailSchema(email=email) + + # Busca um usuário especifico pelo e-mail + response = user_controller.getEmail(usuario_schema) + + # Retorna os dados localizados + return response + +# Localiza um usuário pelo ID +@router.get('/{user_id}', + status_code=status.HTTP_200_OK, + summary='Busca um registro em especifico pelo ID do usuário', + response_description='Busca um registro em especifico') +async def show(user_id : int, current_user: dict = Depends(get_current_user)): + + # Cria o schema com os dados recebidos + usuario_schema = UserIdSchema(user_id=user_id) + + # Busca um usuário especifico pelo ID + response = user_controller.show(usuario_schema) + + # Retorna os dados localizados + return response + + +# Cadastro de usuários +@router.post('/', + status_code=status.HTTP_200_OK, + summary='Cadastra um usuário', + response_description='Cadastra um usuário') +async def save(usuario_schema : UserSaveSchema, current_user: dict = Depends(get_current_user)): + + # Efetua o cadastro do usuário junto ao banco de dados + response = user_controller.save(usuario_schema) + + # Retorna os dados localizados + return response + +# Atualiza os dados de usuário +@router.put('/{user_id}', + status_code=status.HTTP_200_OK, + summary='Atualiza um usuário', + response_description='Atualiza um usuário') +async def update(user_id : int, usuario_schema : UserUpdateSchema, current_user: dict = Depends(get_current_user)): + + # Efetua a atualização dos dados de usuário + response = user_controller.update(user_id, usuario_schema) + + # Retorna os dados localizados + return response + +# Exclui um determinado usuário +@router.delete('/{user_id}', + status_code=status.HTTP_200_OK, + summary='Remove um usuário', + response_description='Remove um usuário') +async def delete(user_id : int, current_user: dict = Depends(get_current_user)): + + # Cria o schema com os dados recebidos + usuario_schema = UserIdSchema(user_id=user_id) + + # Efetua a exclusão de um determinado usuário + response = user_controller.delete(usuario_schema) + + # Retorna os dados localizados + return response \ No newline at end of file diff --git a/packages/v1/administrativo/repositories/client/client_delete_repository.py b/packages/v1/administrativo/repositories/client/client_delete_repository.py new file mode 100644 index 0000000..72b4ba2 --- /dev/null +++ b/packages/v1/administrativo/repositories/client/client_delete_repository.py @@ -0,0 +1,38 @@ +from packages.v1.administrativo.schemas.client_schema import \ + ClientIdSchema +from abstracts.repository import BaseRepository +from fastapi import HTTPException, status + + +class ClientDeleteRepository(BaseRepository): + """ + Repositório responsável pela operação de exclusão (DELETE) de um + registro na tabela 'client', usando o client_id como critério. + """ + + def execute(self, client_schema: ClientIdSchema): + + try: + + # Montagem do sql para exclusão + sql = """ DELETE FROM client c WHERE c.client_id = :clientId """ + + # Preenchimento de parâmetros + params = { + "clientId" : client_schema.client_id + } + + # Execução do sql + response = self.run(sql, params) + + # Retorna o resultado (número de linhas afetadas) + return response + + + except Exception as e: + + # Informa que houve uma falha na exclusão do cliente + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Erro ao excluir cliente: {e}" + ) \ No newline at end of file diff --git a/packages/v1/administrativo/repositories/client/client_index_repository.py b/packages/v1/administrativo/repositories/client/client_index_repository.py new file mode 100644 index 0000000..751955b --- /dev/null +++ b/packages/v1/administrativo/repositories/client/client_index_repository.py @@ -0,0 +1,51 @@ +from abstracts.repository import BaseRepository +from typing import Tuple, List, Dict, Any + + +class ClientIndexRepository(BaseRepository): + """ + Repositório responsável por buscar e retornar todos os registros + da tabela 'client' (indexação), com suporte a paginação. + """ + + # O método execute recebe 'first' (limite) e 'skip' (offset) + def execute(self, first: int, skip: int) -> Tuple[List[Dict[str, Any]], int]: + """ + Executa a busca de clientes com paginação e retorna o total de registros. + + O SELECT retorna todos os campos da DDL da tabela client: + client_id, cns, name, date_register, state, city, responsible, consultant, type_contract. + + :param first: Número máximo de registros a retornar (LIMIT). + :param skip: Número de registros a pular (OFFSET). + :return: Uma tupla contendo a lista de clientes e o total de registros. + """ + + # 1. SQL para contar o total de registros (ignorando LIMIT/OFFSET) + sql_count = """ SELECT COUNT(*) + FROM client c """ + + # Assumindo que self.fetch_one() retorna o resultado do banco + total_records = self.fetch_one(sql_count)['COUNT(*)'] + + + # 2. SQL para listar os clientes com LIMIT e OFFSET (Paginação) + # Selecionando todos os campos da DDL + sql = f""" SELECT c.client_id, + c.cns, + c.name, + c.date_register, + c.state, + c.city, + c.responsible, + c.consultant, + c.type_contract + FROM client c + LIMIT {first} OFFSET {skip} """ + + # Execução do sql para buscar múltiplos registros + # Assumindo que self.fetch_all() retorna a lista de dicionários + response = self.fetch_all(sql) + + # Retorna os dados localizados e o total de registros + return response, total_records \ No newline at end of file diff --git a/packages/v1/administrativo/repositories/client/client_save_repository.py b/packages/v1/administrativo/repositories/client/client_save_repository.py new file mode 100644 index 0000000..0469762 --- /dev/null +++ b/packages/v1/administrativo/repositories/client/client_save_repository.py @@ -0,0 +1,74 @@ +from fastapi import HTTPException, status +from abstracts.repository import BaseRepository +from packages.v1.administrativo.schemas.client_schema import ClientSaveSchema # Importação do schema ClientSaveSchema + +class ClientSaveRepository(BaseRepository): + """ + Repositório responsável pela operação de salvar/atualizar (Upsert) + um registro na tabela 'client', utilizando a lógica INSERT...ON DUPLICATE KEY UPDATE. + """ + + def execute(self, client_schema: ClientSaveSchema): + + try: + # SQL adaptado para MySQL: INSERT ... ON DUPLICATE KEY UPDATE para a tabela 'client'. + + # Colunas para o INSERT (todos os campos da DDL): + insert_cols = """ + client_id, cns, name, date_register, state, city, responsible, consultant, type_contract + """ + + # Valores para o INSERT (usando os placeholders): + insert_vals = """ + :client_id, :cns, :name, :date_register, :state, :city, :responsible, :consultant, :type_contract + """ + + # Ações no ON DUPLICATE KEY UPDATE (atualiza os campos, e o date_register como data de atualização) + update_actions = """ + cns = VALUES(cns), + name = VALUES(name), + date_register = NOW(), # Atualiza o timestamp para o momento da modificação + state = VALUES(state), + city = VALUES(city), + responsible = VALUES(responsible), + consultant = VALUES(consultant), + type_contract = VALUES(type_contract) + """ + + sql = f""" + INSERT INTO client ({insert_cols}) + VALUES ({insert_vals}) + ON DUPLICATE KEY UPDATE + {update_actions}; + """ + + # Preenchimento de parâmetros. client_id será None/0 para INSERT ou o ID para UPDATE. + params = { + 'client_id': client_schema.client_id, + 'cns': client_schema.cns, + 'name': client_schema.name, + 'date_register': client_schema.date_register, + 'state': client_schema.state, + 'city': client_schema.city, + 'responsible': client_schema.responsible, + 'consultant': client_schema.consultant, + 'type_contract': client_schema.type_contract, + } + + # Execução do SQL. + result = self.run_and_return(sql, params) + + # Se for um INSERT e a execução retornar um ID, retorna o novo ID do cliente. + if not client_schema.client_id and result: + return result + + # Se for um UPDATE ou um resultado de sucesso da operação. + return result + + except Exception as e: + + # Informa que houve uma falha ao salvar/atualizar o cliente + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Erro ao salvar cliente: {e}" + ) \ No newline at end of file diff --git a/packages/v1/administrativo/repositories/client/client_show_repository.py b/packages/v1/administrativo/repositories/client/client_show_repository.py new file mode 100644 index 0000000..ab3bb87 --- /dev/null +++ b/packages/v1/administrativo/repositories/client/client_show_repository.py @@ -0,0 +1,60 @@ +from abstracts.repository import BaseRepository +from packages.v1.administrativo.schemas.client_schema import ClientSchema +from fastapi import HTTPException, status + + +class ClientShowRepository(BaseRepository): + """ + Repositório responsável por buscar um registro único na tabela 'client' + utilizando a chave primária 'client_id'. + """ + + def execute(self, client_schema: ClientSchema): + """ + Executa a busca de um cliente pelo seu ID. + + :param client_schema: Schema contendo o client_id. + :return: O registro de cliente encontrado ou None. + """ + + try: + # Montagem do sql. O SELECT retorna todos os campos da DDL: + # client_id, cns, name, date_register, state, city, responsible, consultant, type_contract. + sql = """ SELECT c.client_id, + c.cns, + c.name, + c.date_register, + c.state, + c.city, + c.responsible, + c.consultant, + c.type_contract + FROM client c + WHERE c.client_id = :clientId """ + + # Preenchimento de parâmetros + params = { + 'clientId' : client_schema.client_id + } + + # Execução do sql para buscar um único registro + response = self.fetch_one(sql, params) + + # Se não encontrar o cliente, pode-se lançar uma exceção + if not response: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Cliente com ID {client_schema.client_id} não encontrado." + ) + + return response + + except HTTPException as e: + raise e + + except Exception as e: + # Informa que houve uma falha na busca do cliente + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Erro ao buscar cliente: {e}" + ) \ No newline at end of file diff --git a/packages/v1/administrativo/repositories/client/client_update_repository.py b/packages/v1/administrativo/repositories/client/client_update_repository.py new file mode 100644 index 0000000..56f3e95 --- /dev/null +++ b/packages/v1/administrativo/repositories/client/client_update_repository.py @@ -0,0 +1,87 @@ +from abstracts.repository import BaseRepository +from packages.v1.administrativo.schemas.client_schema import ClientUpdateSchema +from fastapi import HTTPException, status +# A importação de 'datetime' foi removida no código original e não é necessária aqui. + + +class ClientUpdateRepository(BaseRepository): + """ + Repositório responsável pela atualização (UPDATE) dinâmica de um + registro na tabela 'client', identificado pelo 'client_id'. + """ + + def execute(self, client_id: int, client_schema: ClientUpdateSchema): + + try: + updates = [] + params = {} + + # --- Mapeamento e inclusão dos campos da DDL para atualização dinâmica --- + + # cns + if client_schema.cns is not None: + updates.append("cns = :cns") + params["cns"] = client_schema.cns + + # name + if client_schema.name is not None: + updates.append("name = :name") + params["name"] = client_schema.name + + # state + if client_schema.state is not None: + updates.append("state = :state") + params["state"] = client_schema.state + + # city + if client_schema.city is not None: + updates.append("city = :city") + params["city"] = client_schema.city + + # responsible + if client_schema.responsible is not None: + updates.append("responsible = :responsible") + params["responsible"] = client_schema.responsible + + # consultant + if client_schema.consultant is not None: + updates.append("consultant = :consultant") + params["consultant"] = client_schema.consultant + + # type_contract + if client_schema.type_contract is not None: + updates.append("type_contract = :type_contract") + params["type_contract"] = client_schema.type_contract + + # Os campos 'client_id' (chave) e 'date_register' (timestamp de criação) não são atualizados dinamicamente. + + if not updates: + # Se não houver campos para atualizar, retorna False + return False + + params["client_id"] = client_id + + # SQL para UPDATE + sql = f"UPDATE client SET {', '.join(updates)} WHERE client_id = :client_id;" + + # Executa a query + result = self.run(sql, params) + + if result is None or result == 0: + # Se 0 linhas afetadas, pode ser que o cliente não exista ou não houve alteração. + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail='Nenhum cliente localizado para esta solicitação ou nenhuma alteração realizada.' + ) + + # Retorna True, indicando o sucesso da operação + return True + + + except Exception as e: + + # Informa que houve uma falha na atualização do cliente + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"Erro ao atualizar cliente: {e}" + ) \ No newline at end of file diff --git a/packages/v1/administrativo/schemas/client_schema.py b/packages/v1/administrativo/schemas/client_schema.py new file mode 100644 index 0000000..ca2e2c8 --- /dev/null +++ b/packages/v1/administrativo/schemas/client_schema.py @@ -0,0 +1,113 @@ +from pydantic import BaseModel, constr, field_validator, model_validator +from fastapi import HTTPException, status +from typing import Optional +from datetime import datetime + +# Funções utilitárias para sanitização de entradas (evitar XSS, SQLi etc.) +# É necessário importar a função de sanitização se for utilizada nos validadores +from actions.validations.text import Text + +# ---------------------------------------------------- +# Schema base - Representa a estrutura completa do Cliente (usado em Show e Index) +# ---------------------------------------------------- +class ClientSchema(BaseModel): + # Campos da DDL, todos opcionais para o Schema base (principalmente para leitura) + client_id: Optional[int] = None + cns: Optional[str] = None + name: Optional[str] = None + date_register: Optional[datetime] = None + state: Optional[str] = None + city: Optional[str] = None + responsible: Optional[str] = None + consultant: Optional[str] = None + type_contract: Optional[str] = None + + class Config: + # Permite que o Pydantic mapeie campos vindos do banco (ex: via ORM) + from_attributes = True + +# ---------------------------------------------------- +# Schema para operações que requerem apenas o ID +# ---------------------------------------------------- +class ClientIdSchema(BaseModel): + client_id: Optional[int] = None + + # Valida se o ID não está vazio + @model_validator(mode='after') + def validate_client_id(self): + if not self.client_id or self.client_id <= 0: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail='O ID do cliente é obrigatório para esta operação.' + ) + return self + +# ---------------------------------------------------- +# Schema para localizar cliente pelo CNS +# ---------------------------------------------------- +class ClientCNSSchema(BaseModel): + cns: Optional[str] = None + + # Sanitiza o input + @field_validator('cns') + def sanitize_cns(cls, v): + if v: + return Text.sanitize_input(v) # Mantendo o padrão de sanitização + return v + + # Valida se o campo não está vazio + @model_validator(mode='after') + def validate_cns(self): + if not self.cns or len(self.cns.strip()) == 0: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail='Informe um CNS para a busca.' + ) + return self + +# ---------------------------------------------------- +# Schema para cadastrar (SAVE) um novo cliente (name é NOT NULL) +# ---------------------------------------------------- +class ClientSaveSchema(BaseModel): + # Opcional, pois é AUTO_INCREMENT, mas pode ser usado para Upsert + client_id: Optional[int] = None + + # name é NOT NULL na DDL, então é obrigatório no save + name: constr(min_length=1, max_length=550) + + # Os demais são NULL DEFAULT ou com DEFAULT, então podem ser Optional na entrada + cns: Optional[str] = None + state: Optional[str] = None + city: Optional[str] = None + responsible: Optional[str] = None + consultant: Optional[str] = None + type_contract: Optional[str] = None + + # Sanitiza os inputs de string + @field_validator('cns', 'name', 'state', 'city', 'responsible', 'consultant', 'type_contract') + def validate_and_sanitize_fields(cls, v): + if v is not None: + # Assumindo que Text.sanitize_input existe e faz a sanitização + return Text.sanitize_input(v) + return v + +# ---------------------------------------------------- +# Schema para atualizar (UPDATE) um cliente (tudo opcional) +# ---------------------------------------------------- +class ClientUpdateSchema(BaseModel): + # Todos os campos que podem ser atualizados são opcionais + cns: Optional[str] = None + name: Optional[constr(min_length=1, max_length=550)] = None + state: Optional[str] = None + city: Optional[str] = None + responsible: Optional[str] = None + consultant: Optional[str] = None + type_contract: Optional[str] = None + + # Sanitiza os inputs de string + @field_validator('cns', 'name', 'state', 'city', 'responsible', 'consultant', 'type_contract') + def validate_and_sanitize_fields(cls, v): + if v is not None: + # Assumindo que Text.sanitize_input existe e faz a sanitização + return Text.sanitize_input(v) + return v \ No newline at end of file diff --git a/packages/v1/administrativo/services/client/client_delete_service.py b/packages/v1/administrativo/services/client/client_delete_service.py new file mode 100644 index 0000000..c082fc2 --- /dev/null +++ b/packages/v1/administrativo/services/client/client_delete_service.py @@ -0,0 +1,26 @@ +from packages.v1.administrativo.schemas.client_schema import ClientIdSchema +from packages.v1.administrativo.actions.client.client_delete_action import ClientDeleteAction + + +class ClientDeleteService: + """ + Service responsável por orquestrar a exclusão de um cliente, + delegando a execução para a Action correspondente. + """ + + def execute(self, client_schema: ClientIdSchema): + """ + Executa o serviço de exclusão de um cliente. + + :param client_schema: Schema contendo o client_id do registro a ser excluído. + :return: Resultado da operação de exclusão (geralmente o número de linhas afetadas). + """ + + # Instânciamento de ação + delete_action = ClientDeleteAction() + + # Executa a ação em questão + data = delete_action.execute(client_schema) + + # Retorno da informação + return data \ No newline at end of file diff --git a/packages/v1/administrativo/services/client/client_index_service.py b/packages/v1/administrativo/services/client/client_index_service.py new file mode 100644 index 0000000..49faee6 --- /dev/null +++ b/packages/v1/administrativo/services/client/client_index_service.py @@ -0,0 +1,36 @@ +from fastapi import HTTPException, status +from packages.v1.administrativo.schemas.client_schema import ClientSchema +from packages.v1.administrativo.actions.client.client_index_action import ClientIndexAction + + +class ClientIndexService: + """ + Service responsável por orquestrar a listagem (indexação) de todos + os clientes, delegando a busca para a Action correspondente. + """ + + # O método execute pode ser adaptado para receber 'first' e 'skip' se a Action/Repository suportar paginação. + # No entanto, mantendo o padrão da assinatura do arquivo de referência, ele não recebe parâmetros aqui. + def execute(self): + """ + Executa o serviço de listagem de clientes. + + :return: Lista de registros de clientes. + """ + + # Instânciamento de ação + index_action = ClientIndexAction() + + # Executa a busca de todos os clientes (a Action/Repository fará a busca, potencialmente com paginação) + data = index_action.execute() + + # Verifica se foram localizados registros + if not data: + # Retorna uma exceção + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Não foi possível localizar os clientes' + ) + + # Retorna as informações localizadas + return data \ No newline at end of file diff --git a/packages/v1/administrativo/services/client/client_save_service.py b/packages/v1/administrativo/services/client/client_save_service.py new file mode 100644 index 0000000..d74801c --- /dev/null +++ b/packages/v1/administrativo/services/client/client_save_service.py @@ -0,0 +1,64 @@ +from actions.dynamic_import.dynamic_import import DynamicImport +from packages.v1.administrativo.schemas.client_schema import ClientSaveSchema, ClientCNSSchema +from packages.v1.administrativo.actions.client.client_save_action import ClientSaveAction +from fastapi import status, HTTPException + + +class ClientSaveService: + """ + Service responsável por orquestrar o salvamento (INSERT/UPDATE) de um cliente. + Inclui validações de regra de negócio, como a unicidade do CNS. + """ + + def __init__(self): + # Action responsável por carregar as services de acordo com o estado + self.dynamic_import = DynamicImport() + + # Define o pacote que deve ser carregado + self.dynamic_import.set_package("administrativo") + + # Define a tabela que o pacote pertence + self.dynamic_import.set_table("client") + pass + + def execute(self, client_schema: ClientSaveSchema): + """ + Executa a lógica de serviço para salvar um cliente. + + :param client_schema: Schema contendo os dados do cliente a serem salvos. + :return: Resultado da operação de salvamento. + """ + + # Armazena possíveis erros de validação + errors = [] + + # --- Validação de Unicidade do CNS (Cadastro Nacional de Saúde) --- + if client_schema.cns: + # Importação de service de busca por CNS (Adaptado de user_get_email_service) + cns_service = self.dynamic_import.service("client_get_cns_service", "GetCNSService") + + # Instânciamento da service + self.cns_service = cns_service() + + # Tenta localizar o cliente pelo CNS, sem levantar erro HTTP se não encontrar (o False no segundo param) + self.response = self.cns_service.execute(ClientCNSSchema(cns=client_schema.cns), False) + + # Se houver retorno, significa que o CNS já está sendo utilizado + if self.response: + # Se for um UPDATE (client_id existe) e o CNS encontrado pertencer ao próprio cliente que está atualizando, + # não deve ser considerado erro. A validação precisa garantir que o CNS pertence a OUTRO cliente. + if not client_schema.client_id or self.response.get('client_id') != client_schema.client_id: + errors.append({'input': 'cns', 'message': 'O CNS informado já está sendo utilizado por outro cliente.'}) + + # Se houver erros de validação, informa + if errors: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=errors + ) + + # Instânciamento de ações + save_action = ClientSaveAction() + + # Executa a ação de persistência + return save_action.execute(client_schema) \ No newline at end of file diff --git a/packages/v1/administrativo/services/client/client_show_service.py b/packages/v1/administrativo/services/client/client_show_service.py new file mode 100644 index 0000000..7ae37c8 --- /dev/null +++ b/packages/v1/administrativo/services/client/client_show_service.py @@ -0,0 +1,34 @@ +from fastapi import HTTPException, status +from packages.v1.administrativo.schemas.client_schema import ClientSchema +from packages.v1.administrativo.actions.client.client_show_action import ClientShowAction + + +class ClientShowService: + """ + Service responsável por orquestrar a visualização de um cliente específico + (geralmente pelo client_id), delegando a execução para a Action. + """ + + def execute(self, client_schema: ClientSchema): + """ + Executa o serviço de visualização de um cliente. + + :param client_schema: Schema contendo o client_id. + :return: O registro do cliente encontrado. + """ + + # Instânciamento de ação + show_action = ClientShowAction() + + # Executa a ação em questão + data = show_action.execute(client_schema) + + if not data: + # Retorna uma exceção se o registro não for encontrado + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail='Não foi possível localizar o registro do cliente' + ) + + # Retorno da informação + return data \ No newline at end of file diff --git a/packages/v1/administrativo/services/client/client_update_service.py b/packages/v1/administrativo/services/client/client_update_service.py new file mode 100644 index 0000000..c7f6e75 --- /dev/null +++ b/packages/v1/administrativo/services/client/client_update_service.py @@ -0,0 +1,65 @@ +from packages.v1.administrativo.schemas.client_schema import ClientUpdateSchema, ClientCNSSchema +from packages.v1.administrativo.actions.client.client_update_action import ClientUpdateAction +from actions.dynamic_import.dynamic_import import DynamicImport +from fastapi import status, HTTPException + + +class ClientUpdateService: + """ + Service responsável por orquestrar a atualização de um cliente, + incluindo validações de regra de negócio antes de delegar a ação. + """ + + def __init__(self): + # Action responsável por carregar as services de acordo com o estado + self.dynamic_import = DynamicImport() + + # Define o pacote que deve ser carregado + self.dynamic_import.set_package("administrativo") + + # Define a tabela que o pacote pertence + self.dynamic_import.set_table("client") + pass + + def execute(self, client_id: int, client_schema: ClientUpdateSchema): + """ + Executa o serviço de atualização de um cliente. + + :param client_id: ID do cliente a ser atualizado. + :param client_schema: Schema contendo os dados do cliente para atualização. + :return: Resultado da operação de atualização. + """ + + # Armazena possíveis erros de validação + errors = [] + + # --- Validação de Unicidade do CNS (Cadastro Nacional de Saúde) --- + if client_schema.cns: + # Importação de service de busca por CNS + cns_service = self.dynamic_import.service("client_get_cns_service", "GetCNSService") + + # Instânciamento da service + self.cns_service = cns_service() + + # Tenta localizar o cliente pelo CNS, sem levantar erro HTTP se não encontrar (o False no segundo param) + # É necessário usar um schema de busca específico para o CNS + self.response = self.cns_service.execute(ClientCNSSchema(cns=client_schema.cns), False) + + # Se houver retorno, significa que o CNS já está sendo utilizado + if self.response: + # O CNS é um erro APENAS se pertencer a outro cliente, e não ao que está sendo atualizado. + if self.response.get('client_id') != client_id: + errors.append({'input': 'cns', 'message': 'O CNS informado já está sendo utilizado por outro cliente.'}) + + # Se houver erros de validação, informa + if errors: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=errors + ) + + # Instânciamento da Action de atualização + update_action = ClientUpdateAction() + + # Executa a ação de atualização + return update_action.execute(client_id, client_schema) \ No newline at end of file diff --git a/packages/v1/api.py b/packages/v1/api.py index 3930cae..2503208 100644 --- a/packages/v1/api.py +++ b/packages/v1/api.py @@ -4,6 +4,7 @@ from fastapi import APIRouter # Importa os módulos de rotas específicos from packages.v1.administrativo.endpoints import user_endpoint from packages.v1.administrativo.endpoints import log_endpoint +from packages.v1.administrativo.endpoints import client_endpoint # Cria uma instância do APIRouter que vai agregar todas as rotas da API api_router = APIRouter() @@ -18,3 +19,9 @@ api_router.include_router( log_endpoint.router, prefix="/administrativo/log", tags=["Gerenciamento de log's"] ) +# Inclui as rotas de client +api_router.include_router( + client_endpoint.router, prefix="/administrativo/client", tags=["Gerenciamento de client's"] +) + +