Skip to content

Commit

Permalink
Initial Ordering System (#16)
Browse files Browse the repository at this point in the history
* working ordering through API
* buggy streamlit app
  • Loading branch information
PaulJWright authored Aug 5, 2024
1 parent 7706671 commit c36d85e
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 145 deletions.
2 changes: 1 addition & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ services:
environment:
- DATABASE_URL=sqlite:///data/orders.db
- SEED_LOCATION_ID=1 # location id for DB seeding
- SEED_QUANTITY=7.4 # quantity of ingredients for DB seeding
- SEED_QUANTITY=1000 # quantity of ingredients for DB seeding

streamlit:
build:
Expand Down
63 changes: 0 additions & 63 deletions streamlit_app/pages/create_stock.py

This file was deleted.

41 changes: 0 additions & 41 deletions streamlit_app/pages/ingredients.py

This file was deleted.

48 changes: 28 additions & 20 deletions streamlit_app/pages/menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import streamlit as st


# Function to fetch menu items
def fetch_menu_items():
"""
GET menu items
"""
try:
response = requests.get("http://fastapi:8000/menu/")
response.raise_for_status()
Expand All @@ -18,20 +20,32 @@ def fetch_menu_items():
return []


# Function to fetch availability of a specific menu item
def fetch_item_availability(item_id):
def place_order(item_id):
"""
Place order, get order_id
"""
try:
response = requests.get(
f"http://fastapi:8000/menu/{item_id}/availability"
) # Update endpoint if necessary
response = requests.post(
"http://fastapi:8000/order/", json={"menu_id": item_id}
)
response.raise_for_status()
return response.json()
data = response.json()
order_id = data.get("id", "unknown")
return f"Success, your order number is {order_id}, don't forget it!"
except requests.exceptions.HTTPError as http_err:
if response.status_code == 400:
# Handle specific client error (e.g., insufficient stock)
error_mesage = response.json().get("detail", "Sorry, out of stock")
if "InsufficientStockError" in error_mesage:
return "Sorry, out of stock"
return f"Order failed: {error_mesage}"
return f"HTTP error occurred: {http_err}"
except requests.exceptions.RequestException as e:
st.write("Failed to connect to FastAPI:", e)
return None
st.write("Failed to place the order:", e)
return "Failed to place the order."
except Exception as e:
st.write("An error occurred:", e)
return None
return "An error occurred while placing the order."


# Function to display menu items
Expand All @@ -53,24 +67,18 @@ def display_menu():
df = df[df["on_menu"]]
df["price"] = df["price"].apply(lambda x: f"${x:.2f}")

st.write("### Menu Items")

for idx, row in df.iterrows():
cols = st.columns([3, 1, 2])

with cols[0]:
st.write(f"{row['name']} ({row['price']})")
st.write(f"{row['id']}. \t {row['name']} ({row['price']})")

with cols[1]:
button_key = f"order_{row['id']}"
if st.button("Order", key=button_key):
# Fetch availability when button is clicked
availability = fetch_item_availability(row["id"])
if availability and availability.get("available_portions", 0) >= 1:
st.session_state.current_order = row["name"]
st.session_state.order_status = "Order success!"
else:
st.session_state.order_status = "Sorry, that's out of stock"
# Place the order and get the response
st.session_state.order_status = place_order(row["id"])
st.session_state.current_order = row["name"]

with cols[2]:
if st.session_state.current_order == row["name"]:
Expand Down
4 changes: 1 addition & 3 deletions streamlit_app/pages/report_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@


def display_order_report():
st.title("Weird Salads")

st.header("Order Report")
st.title("Report: Orders")

try:
response = requests.get("http://fastapi:8000/order/")
Expand Down
2 changes: 1 addition & 1 deletion streamlit_app/pages/report_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def display_stock_items():
st.title("Stock Items Report")
st.title("Report: Stock")

try:
# Fetch stock data from the FastAPI endpoint
Expand Down
67 changes: 59 additions & 8 deletions weird_salads/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
GetSimpleMenuSchema,
GetStockItemSchema,
GetStockSchema,
UpdateStockSchema,
)
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
InsufficientStockError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
Expand Down Expand Up @@ -138,6 +140,44 @@ def create_stock(payload: CreateStockSchema):
return return_payload


# Approach:
# - HTTP Requests Between Services
#
# Other Options:
# - Mediator Pattern:
# This provides a clean interface between the services and maintains
# loose coupling by introducing a mediator that handles interactions.
# - Event-Driven Architecture:
# This allows the OrdersService and MenuService to be completely decoupled,
# with interactions handled via events and event handlers.
# -----


@app.post("/inventory/update", tags=["Inventory"])
def update_stock(payload: UpdateStockSchema):
# UpdateStockSchema enforces quantity < 0 (only allows for deductions)

try:
with UnitOfWork() as unit_of_work:
ingredient_id = payload.ingredient_id
quantity_to_deduct = abs(payload.quantity)
unit = payload.unit # Enum value, no need to convert
inventory_repo = MenuRepository(unit_of_work.session)
inventory_service = MenuService(inventory_repo)

# Update stock quantity
total_deducted = inventory_service.deduct_stock(
ingredient_id, quantity_to_deduct, unit
)

unit_of_work.commit()
return {"status": "success", "total_deducted": total_deducted}
except InsufficientStockError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception:
raise HTTPException(status_code=500, detail="An unexpected error occurred")


# Orders
@app.get(
"/order",
Expand All @@ -152,18 +192,29 @@ def get_orders():
return {"orders": [result.dict() for result in results]}


# Not sure if this is the best way or if we should be hitting endpoints.
@app.post(
"/order",
status_code=status.HTTP_201_CREATED,
response_model=GetOrderSchema,
tags=["Order"],
)
def create_order(payload: CreateOrderSchema):
with UnitOfWork() as unit_of_work:
repo = OrdersRepository(unit_of_work.session)
orders_service = OrdersService(repo)
order = payload.model_dump()
order = orders_service.place_order(order)
unit_of_work.commit() # this is when id and created are populated
return_payload = order.dict()
return return_payload
try:
with UnitOfWork() as unit_of_work:
orders_repo = OrdersRepository(unit_of_work.session)
orders_service = OrdersService(orders_repo)

order_data = payload.model_dump()
order = orders_service.place_order(order_data)

unit_of_work.commit() # Commit the order and stock deduction

return_payload = order.dict()
return return_payload
except InsufficientStockError as e:
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
raise HTTPException(
status_code=500, detail=f"An unexpected error occurred: {e}"
)
6 changes: 6 additions & 0 deletions weird_salads/api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,9 @@ class CreateStockSchema(BaseModel):

class Config:
extra = "forbid"


class UpdateStockSchema(BaseModel):
ingredient_id: int
quantity: Annotated[float, Field(le=0.0, strict=True)] # negative values only
unit: UnitOfMeasure
4 changes: 4 additions & 0 deletions weird_salads/inventory/inventory_service/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,7 @@ class StockItemNotFoundError(Exception):

class IngredientNotFoundError(Exception):
pass


class InsufficientStockError(Exception):
pass
42 changes: 42 additions & 0 deletions weird_salads/inventory/inventory_service/inventory_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from weird_salads.api.schemas import UnitOfMeasure
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
InsufficientStockError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
Expand Down Expand Up @@ -153,3 +154,44 @@ def get_stock_item(self, stock_id: str):

def list_stock(self): # needs options for filtering
return self.menu_repository.list_stock()

# -- stock deduction
# !TODO check units
def deduct_stock(self, ingredient_id: int, quantity: float, unit: UnitOfMeasure):
"""
Deduct a specific quantity of stock for a given item.
"""
# Fetch the stock item(s) for the given item_id
stock_items = self.menu_repository.get_ingredient(ingredient_id)

if not stock_items:
raise StockItemNotFoundError(
f"No stock found for ingredient ID {ingredient_id}"
)

total_deducted = 0.0
quantity_to_deduct = quantity

for stock_item in stock_items:
if quantity_to_deduct <= 0:
break

available_quantity = stock_item.quantity
# !TODO fix the assumption that the quantity is in the stocks unit

quantity_deducted = min(quantity_to_deduct, available_quantity)
# Deduct the quantity
stock_item.quantity -= quantity_deducted
total_deducted += quantity_deducted
quantity_to_deduct -= quantity_deducted

# !TODO If stock item is depleted, remove it

if quantity_to_deduct > 0:
raise InsufficientStockError(
f"Not enough stock available to deduct {quantity_to_deduct} units"
)

self.menu_repository.update_ingredient(stock_items)

return total_deducted
Loading

0 comments on commit c36d85e

Please sign in to comment.