diff --git a/retail/api/base_service_view.py b/retail/api/base_service_view.py new file mode 100644 index 0000000..d2703fd --- /dev/null +++ b/retail/api/base_service_view.py @@ -0,0 +1,41 @@ +# base_service_view.py +from rest_framework import views + +from rest_framework.permissions import IsAuthenticated + +from retail.clients.flows.client import FlowsClient +from retail.clients.integrations.client import IntegrationsClient +from retail.services.flows.service import FlowsService +from retail.services.integrations.service import IntegrationsService + + +class BaseServiceView(views.APIView): + """ + BaseServiceView is a base class that provides common service and client + injection logic for views. Other views should inherit from this class to + reuse the integration and flows service logic. + """ + + permission_classes = [IsAuthenticated] + + integrations_service_class = IntegrationsService + integrations_client_class = IntegrationsClient + flows_service_class = FlowsService + flows_client_class = FlowsClient + + _integrations_service = None + _flows_service = None + + @property + def integrations_service(self): + if not self._integrations_service: + self._integrations_service = self.integrations_service_class( + self.integrations_client_class() + ) + return self._integrations_service + + @property + def flows_service(self): + if not self._flows_service: + self._flows_service = self.flows_service_class(self.flows_client_class()) + return self._flows_service diff --git a/retail/api/features/views.py b/retail/api/features/views.py index 472531f..cc0cfdd 100644 --- a/retail/api/features/views.py +++ b/retail/api/features/views.py @@ -1,20 +1,17 @@ from rest_framework import views, status from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated - +from retail.api.base_service_view import BaseServiceView from retail.api.features.serializers import FeaturesSerializer +from retail.api.usecases.remove_globals_keys import RemoveGlobalsKeysUsecase from retail.features.models import Feature, IntegratedFeature -class FeaturesView(views.APIView): - - permission_classes = [IsAuthenticated] - - def get(self, request, project_uuid): +class FeaturesView(BaseServiceView): + def get(self, request, project_uuid: str): try: - category = request.query_params.get("category", None) + integrated_features = IntegratedFeature.objects.filter( project__uuid=project_uuid ).values_list("feature__uuid", flat=True) @@ -26,7 +23,16 @@ def get(self, request, project_uuid): serializer = FeaturesSerializer(features, many=True) - return Response({"results": serializer.data}, status=status.HTTP_200_OK) + usecase = RemoveGlobalsKeysUsecase( + integrations_service=self.integrations_service, + flows_service=self.flows_service, + ) + + # Execute usecase to modify globals + user_email = request.user.email + features_data = usecase.execute(serializer.data, user_email, project_uuid) + + return Response({"results": features_data}, status=status.HTTP_200_OK) except Exception as e: return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST) diff --git a/retail/api/integrated_feature/views.py b/retail/api/integrated_feature/views.py index 520100f..eed0d90 100644 --- a/retail/api/integrated_feature/views.py +++ b/retail/api/integrated_feature/views.py @@ -1,20 +1,18 @@ from django.contrib.auth.models import User -from rest_framework import views, viewsets, status +from rest_framework import status from rest_framework.response import Response -from rest_framework.permissions import IsAuthenticated - +from retail.api.base_service_view import BaseServiceView from retail.api.integrated_feature.serializers import IntegratedFeatureSerializer +from retail.api.usecases.populate_globals_values import PopulateGlobalsValuesUsecase + from retail.features.models import Feature, IntegratedFeature from retail.features.integrated_feature_eda import IntegratedFeatureEDA from retail.projects.models import Project -class IntegratedFeatureView(views.APIView): - - permission_classes = [IsAuthenticated] - +class IntegratedFeatureView(BaseServiceView): def post(self, request, *args, **kwargs): feature = Feature.objects.get(uuid=kwargs["feature_uuid"]) try: @@ -23,9 +21,10 @@ def post(self, request, *args, **kwargs): return Response( status=status.HTTP_404_NOT_FOUND, data={ - "error": f"Project with uuid equals {request.data['project_uuid' ]} does not exists!" + "error": f"Project with uuid equals {request.data['project_uuid']} does not exist!" }, ) + user, _ = User.objects.get_or_create(email=request.user.email) feature_version = feature.last_version @@ -35,7 +34,7 @@ def post(self, request, *args, **kwargs): sectors_data = [] integrated_feature.sectors = [] - if feature_version.sectors != None: + if feature_version.sectors is not None: for sector in feature_version.sectors: for r_sector in request.data.get("sectors", []): if r_sector.get("name") == sector.get("name"): @@ -46,13 +45,19 @@ def post(self, request, *args, **kwargs): } integrated_feature.sectors.append(new_sector) break - for globals_key, globals_value in request.data.get( - "globals_values", {} - ).items(): - integrated_feature.globals_values[globals_key] = globals_value - integrated_feature.save( - update_fields=["sectors", "globals_values"] - ) + + # Treat and fill specific globals + fill_globals_usecase = PopulateGlobalsValuesUsecase( + self.integrations_service, self.flows_service + ) + treated_globals_values = fill_globals_usecase.execute( + request.data.get("globals_values", {}), + request.user.email, + request.data["project_uuid"], + ) + # Add all globals from the request, including treated ones + for globals_key, globals_value in treated_globals_values.items(): + integrated_feature.globals_values[globals_key] = globals_value for sector in integrated_feature.sectors: sectors_data.append( @@ -68,24 +73,26 @@ def post(self, request, *args, **kwargs): actions = [] for function in feature.functions.all(): function_last_version = function.last_version - if function_last_version.action_base_flow_uuid != None: + if function_last_version.action_base_flow_uuid is not None: actions.append( { "name": function_last_version.action_name, "prompt": function_last_version.action_prompt, - "root_flow_uuid": str(function_last_version.action_base_flow_uuid), - "type": "" + "root_flow_uuid": str( + function_last_version.action_base_flow_uuid + ), + "type": "", } ) if feature_version.action_base_flow_uuid: actions.append( - { - "name": feature_version.action_name, - "prompt": feature_version.action_prompt, - "root_flow_uuid": str(feature_version.action_base_flow_uuid), - "type": "" - } - ) + { + "name": feature_version.action_name, + "prompt": feature_version.action_prompt, + "root_flow_uuid": str(feature_version.action_base_flow_uuid), + "type": "", + } + ) body = { "definition": integrated_feature.feature_version.definition, @@ -95,11 +102,13 @@ def post(self, request, *args, **kwargs): "feature_version": str(integrated_feature.feature_version.uuid), "feature_uuid": str(integrated_feature.feature.uuid), "sectors": sectors_data, - "action": actions + "action": actions, } IntegratedFeatureEDA().publisher(body=body, exchange="integrated-feature.topic") - print(f"message send `integrated feature` - body: {body}") + print(f"message sent `integrated feature` - body: {body}") + + serializer = IntegratedFeatureSerializer(integrated_feature.feature) response = { "status": 200, @@ -109,6 +118,7 @@ def post(self, request, *args, **kwargs): "project": integrated_feature.project.uuid, "user": integrated_feature.user.email, "integrated_on": integrated_feature.integrated_on, + **serializer.data, }, } return Response(response) diff --git a/retail/api/usecases/populate_globals_values.py b/retail/api/usecases/populate_globals_values.py new file mode 100644 index 0000000..edc8baf --- /dev/null +++ b/retail/api/usecases/populate_globals_values.py @@ -0,0 +1,53 @@ +""" +PopulateGlobalsValuesUsecase is responsible for filling in specific global keys +in the globals_values dictionary using data fetched from external services. + +Attributes: + integrations_service: Service used to fetch data related to VTEX integrations. + flows_service: Service used to fetch user API tokens from the flows system. +""" + + +class PopulateGlobalsValuesUsecase: + def __init__(self, integrations_service, flows_service): + self.integrations_service = integrations_service + self.flows_service = flows_service + + def execute(self, globals_values: dict, user_email: str, project_uuid: str) -> dict: + """ + Fill in the keys of globals_values using the appropriate services. + Only the following keys are manipulated: + - x_vtex_api_appkey + - x_vtex_api_apptoken + - url_api_vtex + - api_token (from flows, mapped to 'token') + """ + filled_globals_values = globals_values.copy() + + # Handle the keys related to the integrations service + integration_data = self.integrations_service.get_vtex_integration_detail( + project_uuid + ) + if integration_data: + if "url_api_vtex" in globals_values: + filled_globals_values["url_api_vtex"] = integration_data.get( + "domain", globals_values["url_api_vtex"] + ) + if "x_vtex_api_appkey" in globals_values: + filled_globals_values["x_vtex_api_appkey"] = integration_data.get( + "app_key", globals_values["x_vtex_api_appkey"] + ) + if "x_vtex_api_apptoken" in globals_values: + filled_globals_values["x_vtex_api_apptoken"] = integration_data.get( + "app_token", globals_values["x_vtex_api_apptoken"] + ) + + # Handle the key related to the flows service (api_token) + flow_data = self.flows_service.get_user_api_token(user_email, project_uuid) + if flow_data: + if "api_token" in globals_values: + filled_globals_values["api_token"] = flow_data.get( + "token", globals_values["api_token"] + ) + + return filled_globals_values diff --git a/retail/api/usecases/remove_globals_keys.py b/retail/api/usecases/remove_globals_keys.py new file mode 100644 index 0000000..25d0fce --- /dev/null +++ b/retail/api/usecases/remove_globals_keys.py @@ -0,0 +1,55 @@ +""" +RemoveGlobalsKeysUsecase is responsible for removing specific global keys +from the feature's globals list based on data fetched from external services. + +Attributes: + integrations_service: Service used to fetch data related to VTEX integrations. + flows_service: Service used to fetch user API tokens from the flows system. +""" + + +class RemoveGlobalsKeysUsecase: + def __init__(self, integrations_service, flows_service): + self.integrations_service = integrations_service + self.flows_service = flows_service + + def execute( + self, features: list[dict], user_email: str, project_uuid: str + ) -> list[dict]: + # Fetch data from the services, handling cases where data might be None + integrations_data = self.integrations_service.get_vtex_integration_detail( + project_uuid + ) + flows_data = self.flows_service.get_user_api_token(user_email, project_uuid) + + for feature in features: + globals_to_remove = [] + + # Check and mark globals for removal based on integrations data if available + if integrations_data: + if "x_vtex_api_appkey" in feature["globals"] and integrations_data.get( + "app_key" + ): + globals_to_remove.append("x_vtex_api_appkey") + if "x_vtex_api_apptoken" in feature[ + "globals" + ] and integrations_data.get("app_token"): + globals_to_remove.append("x_vtex_api_apptoken") + if "url_api_vtex" in feature["globals"] and integrations_data.get( + "domain" + ): + globals_to_remove.append("url_api_vtex") + + # Check and mark globals for removal based on flows data if available + if flows_data: + if "user_api_token" in feature["globals"] and flows_data.get( + "api_token" + ): + globals_to_remove.append("user_api_token") + + # Remove the marked globals + feature["globals"] = [ + g for g in feature["globals"] if g not in globals_to_remove + ] + + return features diff --git a/retail/clients/flows/client.py b/retail/clients/flows/client.py index a1228c3..d1f8060 100644 --- a/retail/clients/flows/client.py +++ b/retail/clients/flows/client.py @@ -3,9 +3,10 @@ from django.conf import settings from retail.clients.base import RequestClient, InternalAuthentication +from retail.interfaces.clients.flows.interface import FlowsClientInterface -class FlowsClient(RequestClient): +class FlowsClient(RequestClient, FlowsClientInterface): def __init__(self): self.base_url = settings.FLOWS_REST_ENDPOINT self.authentication_instance = InternalAuthentication() diff --git a/retail/clients/integrations/client.py b/retail/clients/integrations/client.py index 38c2a0e..1297ba1 100644 --- a/retail/clients/integrations/client.py +++ b/retail/clients/integrations/client.py @@ -3,9 +3,10 @@ from django.conf import settings from retail.clients.base import RequestClient, InternalAuthentication +from retail.interfaces.clients.integrations.interface import IntegrationsClientInterface -class IntegrationsClient(RequestClient): +class IntegrationsClient(RequestClient, IntegrationsClientInterface): def __init__(self): self.base_url = settings.INTEGRATIONS_REST_ENDPOINT self.authentication_instance = InternalAuthentication() diff --git a/retail/interfaces/clients/flows/interface.py b/retail/interfaces/clients/flows/interface.py new file mode 100644 index 0000000..1907123 --- /dev/null +++ b/retail/interfaces/clients/flows/interface.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class FlowsClientInterface(ABC): + @abstractmethod + def get_user_api_token(self, user_email: str, project_uuid: str): + pass diff --git a/retail/interfaces/clients/integrations/interface.py b/retail/interfaces/clients/integrations/interface.py new file mode 100644 index 0000000..1df03f8 --- /dev/null +++ b/retail/interfaces/clients/integrations/interface.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class IntegrationsClientInterface(ABC): + @abstractmethod + def get_vtex_integration_detail(self, project_uuid: str): + pass diff --git a/retail/services/__init__.py b/retail/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/retail/services/flows/__init__.py b/retail/services/flows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/retail/services/flows/service.py b/retail/services/flows/service.py new file mode 100644 index 0000000..2204733 --- /dev/null +++ b/retail/services/flows/service.py @@ -0,0 +1,18 @@ +from retail.clients.exceptions import CustomAPIException +from retail.interfaces.clients.flows.interface import FlowsClientInterface + + +class FlowsService: + def __init__(self, client: FlowsClientInterface): + self.client = client + + def get_user_api_token(self, user_email: str, project_uuid: str) -> dict: + """ + Retrieve the user API token for a given email and project UUID. + Handles communication errors and returns None in case of failure. + """ + try: + return self.client.get_user_api_token(user_email, project_uuid) + except CustomAPIException as e: + print(f"Error {e.status_code} when retrieving user API token for project {project_uuid}.") + return None diff --git a/retail/services/integrations/__init__.py b/retail/services/integrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/retail/services/integrations/service.py b/retail/services/integrations/service.py new file mode 100644 index 0000000..53fed70 --- /dev/null +++ b/retail/services/integrations/service.py @@ -0,0 +1,18 @@ +from retail.clients.exceptions import CustomAPIException +from retail.interfaces.clients.integrations.interface import IntegrationsClientInterface + + +class IntegrationsService: + def __init__(self, client: IntegrationsClientInterface): + self.client = client + + def get_vtex_integration_detail(self, project_uuid: str) -> dict: + """ + Retrieve the VTEX integration details for a given project UUID. + Handles communication errors and returns None in case of failure. + """ + try: + return self.client.get_vtex_integration_detail(project_uuid) + except CustomAPIException as e: + print(f"Error {e.status_code} when retrieving VTEX integration for project {project_uuid}.") + return None