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
PaulJWright authored Aug 5, 2024
1 parent 7706671 commit c36d85e
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
@@ -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:
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
@@ -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()
@@ -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
@@ -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"]:
4 changes: 1 addition & 3 deletions streamlit_app/pages/report_order.py
Original file line number Diff line number Diff line change
@@ -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/")
2 changes: 1 addition & 1 deletion streamlit_app/pages/report_stock.py
Original file line number Diff line number Diff line change
@@ -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
67 changes: 59 additions & 8 deletions weird_salads/api/app.py
Original file line number Diff line number Diff line change
@@ -11,9 +11,11 @@
GetSimpleMenuSchema,
GetStockItemSchema,
GetStockSchema,
UpdateStockSchema,
)
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
InsufficientStockError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
@@ -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",
@@ -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
@@ -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
@@ -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
@@ -7,6 +7,7 @@
from weird_salads.api.schemas import UnitOfMeasure
from weird_salads.inventory.inventory_service.exceptions import (
IngredientNotFoundError,
InsufficientStockError,
MenuItemNotFoundError,
StockItemNotFoundError,
)
@@ -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
28 changes: 26 additions & 2 deletions weird_salads/inventory/repository/inventory_repository.py
Original file line number Diff line number Diff line change
@@ -97,10 +97,10 @@ def delete(self, id):
pass

# - Stock-related
def _get_ingredient(self, id: int):
def _get_ingredient(self, ingredient_id: int):
return (
self.session.query(StockModel)
.filter(StockModel.ingredient_id == int(id))
.filter(StockModel.ingredient_id == int(ingredient_id))
.all()
) # noqa: E501

@@ -129,3 +129,27 @@ def add_stock(self, item):
record = StockModel(**item)
self.session.add(record)
return StockItem(**record.dict(), order_=record)

def _convert_to_model(self, stock_item: StockItem) -> StockModel:
return StockModel(
id=stock_item.id,
ingredient_id=stock_item.ingredient_id,
unit=stock_item.unit,
quantity=stock_item.quantity,
cost=stock_item.cost,
delivery_date=stock_item.delivery_date,
created_on=stock_item.created_on,
)

def update_ingredient(self, stock_items: List[StockItem]) -> None:
"""
Update stock items in the session.
"""
merged_records = []
for stock_item in stock_items:
record = StockModel(**stock_item.dict())

# Merge record with the session
merged_records.append(self.session.merge(record))

return merged_records
70 changes: 67 additions & 3 deletions weird_salads/orders/orders_service/orders_service.py
Original file line number Diff line number Diff line change
@@ -2,6 +2,11 @@
Services
"""

import requests
from fastapi import HTTPException

from weird_salads.api.schemas import UnitOfMeasure
from weird_salads.inventory.inventory_service.exceptions import InsufficientStockError
from weird_salads.orders.orders_service.exceptions import OrderNotFoundError
from weird_salads.orders.repository.orders_repository import OrdersRepository

@@ -12,11 +17,38 @@ class OrdersService:
def __init__(self, orders_repository: OrdersRepository):
self.orders_repository = orders_repository

def place_order(self, item):
def place_order(self, order_data):
"""
place order
Place an order after checking inventory and updating stock levels.
"""
return self.orders_repository.add(item)
menu_id = int(order_data["menu_id"])

# Fetch availability information for the menu item
available_to_order, availability_response = self._get_menu_item_availability(
menu_id
)

if not available_to_order:
raise InsufficientStockError("Sorry, this is out of stock.")

# Place the order, then deduct stock (uow deals with the committing later)
order = self.orders_repository.add(order_data)

for ingredient in availability_response["ingredient_availability"]:
ingredient_id = int(ingredient["ingredient"]["id"])
required_quantity = float(ingredient["required_quantity"])
unit_string = str(ingredient["unit"])

# Convert the unit string to the UnitOfMeasure Enum
try:
unit = UnitOfMeasure(unit_string)
except ValueError:
raise HTTPException(
status_code=400, detail=f"Invalid unit: {unit_string}"
)
self._update_stock(ingredient_id, -1 * required_quantity, unit)

return order

def get_order(self, order_id: str):
"""
@@ -32,3 +64,35 @@ def list_orders(self):
get all orders
"""
return self.orders_repository.list()

def _get_menu_item_availability(self, menu_id: int):
"""
Fetch availability details for the given menu item,
including ingredient availability.
"""
response = requests.get(f"http://localhost:8000/menu/{menu_id}/availability")
if response.status_code == 200:
availability = response.json()
return availability["available_portions"] >= 0, response.json()
else:
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to get availability for menu ID {menu_id}",
)

def _update_stock(
self, ingredient_id: int, quantity: float, unit: UnitOfMeasure
) -> None:
response = requests.post(
"http://localhost:8000/inventory/update",
json={
"ingredient_id": ingredient_id,
"quantity": quantity,
"unit": unit.value,
},
)
if response.status_code != 200:
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to update stock for Ingredient ID {ingredient_id}",
)
3 changes: 0 additions & 3 deletions weird_salads/orders/repository/orders_repository.py
Original file line number Diff line number Diff line change
@@ -32,8 +32,5 @@ def list(self):
records = query.all()
return [Order(**record.dict()) for record in records]

def update(self, id):
pass

def delete(self, id):
pass

0 comments on commit c36d85e

Please sign in to comment.