diff --git a/README.md b/README.md index f7f53570f..e34340b5a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,18 @@ The code consists of multiple services. Each service is located in a separate fo There is also a `utils` folder that contains some helper code or specifications that are used by multiple services. Check the `utils` folder for more information. +### Architecture + +The following diagrams give a high-level view of the system and how the services interact. + +**System overview** + + + +**Service architecture** + + + ### Running the code with Docker Compose [recommended] To run the code, you need to clone this repository, make sure you have Docker and Docker Compose installed, and run the following command in the root folder of the repository: diff --git a/architecture_diagram.png b/architecture_diagram.png new file mode 100644 index 000000000..2071d8908 Binary files /dev/null and b/architecture_diagram.png differ diff --git a/docker-compose.yaml b/docker-compose.yaml index b4a60a537..e4818a9c8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,6 +13,7 @@ services: volumes: # Mount the frontend directory - ./frontend/src:/usr/share/nginx/html + orchestrator: build: # Use the current directory as the build context @@ -35,12 +36,13 @@ services: - ./utils:/app/utils # Mount the orchestrator/src directory in the current directory to the /app/orchestrator/src directory in the container - ./orchestrator/src:/app/orchestrator/src + fraud_detection: build: # Use the current directory as the build context # This allows us to access the files in the current directory inside the Dockerfile context: ./ - # Use the Dockerfile in the fraud_detection directorys + # Use the Dockerfile in the fraud_detection directory dockerfile: ./fraud_detection/Dockerfile ports: # Expose port 50051 on the host, and map port 50051 of the container to port 50051 on the host @@ -56,4 +58,37 @@ services: # Mount the utils directory in the current directory to the /app/utils directory in the container - ./utils:/app/utils # Mount the fraud_detection/src directory in the current directory to the /app/fraud_detection/src directory in the container - - ./fraud_detection/src:/app/fraud_detection/src \ No newline at end of file + - ./fraud_detection/src:/app/fraud_detection/src + + transaction_verification: + build: + # Use the current directory as the build context + context: ./ + # Use the Dockerfile in the transaction_verification directory + dockerfile: ./transaction_verification/Dockerfile + ports: + # Expose port 50052 on the host, and map port 50052 of the container to port 50052 on the host + - 50052:50052 + environment: + # Pass the environment variables to the container + - PYTHONUNBUFFERED=TRUE + # The PYTHONFILE environment variable specifies the absolute entry point of the application + - PYTHONFILE=/app/transaction_verification/src/app.py + volumes: + # Mount the utils directory in the current directory to the /app/utils directory in the container + - ./utils:/app/utils + # Mount the transaction_verification/src directory in the current directory to the container + - ./transaction_verification/src:/app/transaction_verification/src + + suggestions: + build: + context: ./ + dockerfile: ./suggestions/Dockerfile + ports: + - 50053:50053 + environment: + - PYTHONUNBUFFERED=TRUE + - PYTHONFILE=/app/suggestions/src/app.py + volumes: + - ./utils:/app/utils + - ./suggestions/src:/app/suggestions/src \ No newline at end of file diff --git a/fraud_detection/src/app.py b/fraud_detection/src/app.py index b2f1d2fce..ecee0fad5 100644 --- a/fraud_detection/src/app.py +++ b/fraud_detection/src/app.py @@ -1,45 +1,89 @@ import sys import os +from concurrent import futures # This set of lines are needed to import the gRPC stubs. -# The path of the stubs is relative to the current file, or absolute inside the container. -# Change these lines only if strictly needed. FILE = __file__ if '__file__' in globals() else os.getenv("PYTHONFILE", "") -fraud_detection_grpc_path = os.path.abspath(os.path.join(FILE, '../../../utils/pb/fraud_detection')) +fraud_detection_grpc_path = os.path.abspath( + os.path.join(FILE, '../../../utils/pb/fraud_detection') +) sys.path.insert(0, fraud_detection_grpc_path) + +import grpc import fraud_detection_pb2 as fraud_detection import fraud_detection_pb2_grpc as fraud_detection_grpc -import grpc -from concurrent import futures -# Create a class to define the server functions, derived from -# fraud_detection_pb2_grpc.HelloServiceServicer class HelloService(fraud_detection_grpc.HelloServiceServicer): - # Create an RPC function to say hello def SayHello(self, request, context): - # Create a HelloResponse object response = fraud_detection.HelloResponse() - # Set the greeting field of the response object response.greeting = "Hello, " + request.name - # Print the greeting message print(response.greeting) - # Return the response object return response + def CheckFraud(self, request, context): + print("Received fraud check request") + print("user_name:", request.user_name) + masked_card_number = mask_fixed(request.card_number) + print("card_number:", masked_card_number) + print("item_count:", request.item_count) + + # Compute length based on digits only to avoid counting spaces or other characters + card_digits = extract_card_digits(request.card_number) + print("card length (digits only):", len(card_digits)) + is_fraud = False + message = "No fraud detected." + + # Very simple dummy rules for now + if request.item_count > 20: + is_fraud = True + message = "Too many items in order." + + # Treat any non-16-digit card number as invalid + elif len(card_digits) != 16: + is_fraud = True + message = "Invalid card number." + + elif card_digits.startswith("0000"): + is_fraud = True + message = "Suspicious card number pattern." + + elif card_digits.endswith("0000"): + is_fraud = True + message = "Suspicious card number pattern." + elif "fraud" in request.user_name.lower(): + is_fraud = True + message = "Suspicious user name." + + response = fraud_detection.FraudCheckResponse() + response.is_fraud = is_fraud + response.message = message + + print("Returning fraud result:", response.is_fraud, response.message) + return response + + +def extract_card_digits(card: str) -> str: + """ + Return only the digit characters from the given card number. + """ + return ''.join(c for c in str(card) if c.isdigit()) + + def serve(): - # Create a gRPC server server = grpc.server(futures.ThreadPoolExecutor()) - # Add HelloService fraud_detection_grpc.add_HelloServiceServicer_to_server(HelloService(), server) - # Listen on port 50051 + port = "50051" server.add_insecure_port("[::]:" + port) - # Start the server server.start() - print("Server started. Listening on port 50051.") - # Keep thread alive + print("Fraud detection server started. Listening on port 50051.") server.wait_for_termination() +def mask_fixed(card: str) -> str: + digits = ''.join(c for c in str(card) if c.isdigit()) + masked = '*' * 12 + digits[-4:].rjust(4, '*') + return ' '.join(masked[i:i+4] for i in range(0, 16, 4)) + if __name__ == '__main__': serve() \ No newline at end of file diff --git a/frontend/src/index.html b/frontend/src/index.html index 15c47351f..2ace91b7d 100644 --- a/frontend/src/index.html +++ b/frontend/src/index.html @@ -22,26 +22,32 @@