Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**

![System diagram](./system_diagram.png)

**Service architecture**

![Service architecture diagram](./architecture_diagram.png)

### 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:
Expand Down
Binary file added architecture_diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 37 additions & 2 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
- ./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
80 changes: 62 additions & 18 deletions fraud_detection/src/app.py
Original file line number Diff line number Diff line change
@@ -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()
54 changes: 37 additions & 17 deletions frontend/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,32 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
<label for="name" class="block text-sm font-medium text-gray-700">Name:</label>
<input type="text" id="name" name="name" value="John Doe" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="contact" class="block text-sm font-medium text-gray-700">Contact:</label>
<input type="email" id="contact" name="contact" value="john.doe@example.com" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="creditCard" class="block text-sm font-medium text-gray-700">Credit Card Number:</label>
<input type="text" id="creditCard" name="creditCard" value="4111111111111111" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="expirationDate" class="block text-sm font-medium text-gray-700">Expiration Date:</label>
<input type="text" id="expirationDate" name="expirationDate" value="12/25" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="cvv" class="block text-sm font-medium text-gray-700">CVV:</label>
<input type="text" id="cvv" name="cvv" value="123" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="userComment" class="block text-sm font-medium text-gray-700">Comment:</label>
<textarea id="userComment" name="userComment" class="w-full border border-gray-300 rounded-lg p-2 mt-1">Please handle with care.</textarea>
</div>

<div class="mb-4">
<label for="billingAddress" class="block text-sm font-medium text-gray-700">Billing Address:</label>
<input type="text" id="billingStreet" name="billingStreet" value="123 Main St" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
Expand All @@ -50,6 +56,7 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
<input type="text" id="billingZip" name="billingZip" value="62701" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
<input type="text" id="billingCountry" name="billingCountry" value="USA" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
</div>

<div class="mb-4">
<label for="shippingMethod" class="block text-sm font-medium text-gray-700">Shipping Method:</label>
<select id="shippingMethod" name="shippingMethod" required class="w-full border border-gray-300 rounded-lg p-2 mt-1">
Expand All @@ -58,15 +65,20 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
<option value="Next-Day">Next-Day</option>
</select>
</div>

<div class="mb-4 flex items-center">
<input type="checkbox" id="giftWrapping" name="giftWrapping" checked class="mr-2 border border-gray-300 rounded">
<label for="giftWrapping" class="text-sm font-medium text-gray-700">Gift Wrapping</label>
</div>

<div class="mb-4 flex items-center">
<input type="checkbox" id="terms" name="terms" checked required class="mr-2 border border-gray-300 rounded">
<label for="terms" class="text-sm font-medium text-gray-700">Accept Terms and Conditions</label>
</div>
<button type="submit" class="w-full bg-blue-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-blue-700">Submit Order</button>

<button type="submit" class="w-full bg-blue-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-blue-700">
Submit Order
</button>
</form>

<div id="response" class="mt-6 p-4 border rounded-lg hidden"></div>
Expand All @@ -90,17 +102,18 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
event.preventDefault();

const formData = new FormData(event.target);

const data = {
user: {
name: formData.get('name'),
contact: formData.get('contact'),
creditCard: {
number: formData.get('creditCard'),
expirationDate: formData.get('expirationDate'),
cvv: formData.get('cvv'),
},
userComment: formData.get('userComment'),
},
creditCard: {
number: formData.get('creditCard'),
expirationDate: formData.get('expirationDate'),
cvv: formData.get('cvv'),
},
userComment: formData.get('userComment'),
items: items,
billingAddress: {
street: formData.get('billingStreet'),
Expand All @@ -111,38 +124,45 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
},
shippingMethod: formData.get('shippingMethod'),
giftWrapping: formData.get('giftWrapping') === 'on',
termsAccepted: formData.get('terms') === 'on',
termsAndConditionsAccepted: formData.get('terms') === 'on',
};

try {
const response = await fetch('http://localhost:8081/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
});

const result = await response.json();
const responseDiv = document.getElementById('response');

if (response.ok) {
const suggestedBooks = result.suggestedBooks.map(book => `<li>${book.title} by ${book.author}</li>`).join('');
const suggestedBooks = (result.suggestedBooks || [])
.map(book => `<li>${book.title} by ${book.author}</li>`)
.join('');

responseDiv.innerHTML = `
<strong>Order status: ${result.status}</strong><br>
Order ID: ${result.orderId}<br>
${result.status === "Order Approved" ?
`Suggested Books:
<ul class="list-disc pl-5 mt-2 space-y-1">${suggestedBooks}</ul>`
: ``
${result.status === "Order Approved"
? `Suggested Books:
<ul class="list-disc pl-5 mt-2 space-y-1">${suggestedBooks}</ul>`
: ``
}
`;
color = result.status === 'Order Approved' ? 'green' : 'red';

const color = result.status === 'Order Approved' ? 'green' : 'red';
responseDiv.className = `mt-6 p-4 border rounded-lg bg-${color}-100 text-${color}-700`;
} else {
responseDiv.textContent = `Error: ${result.error.message}`;
const message = result?.error?.message || 'Unknown error';
responseDiv.textContent = `Error: ${message}`;
responseDiv.className = "mt-6 p-4 border rounded-lg bg-red-100 text-red-700";
}

responseDiv.style.display = 'block';
// scroll to the bottom of the page
window.scrollTo(0, document.body.scrollHeight);
} catch (error) {
const responseDiv = document.getElementById('response');
Expand All @@ -153,4 +173,4 @@ <h2 class="text-lg font-semibold text-gray-700">Items</h2>
});
</script>
</body>
</html>
</html>
Loading