From e811b5edb605d3dd054af6ba439562c5e301598e Mon Sep 17 00:00:00 2001
From: Paul Wright <paul@pauljwright.co.uk>
Date: Mon, 5 Aug 2024 18:19:15 +0100
Subject: [PATCH] Feature/menu implementation (#14)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This MR addresses several key updates:

1. FastAPI Application (app.py): Introduced endpoints for retrieving a list of menu items, details for specific items, and availability information. This enhances the API’s capability to serve detailed menu and stock data to clients.

```
/menu/
/menu/{item_id}
/menu/{item_id}/availability
```

2. Schemas: Added new and updated existing schemas to include detailed information on menu items, ingredients, and their availability. This ensures that the API responses are comprehensive and aligned with the new backend logic.
Inventory Service (inventory_service.py):

3. Enhanced Functionality: Added methods for calculating ingredient availability, converting units, and fetching stock data. These improvements enable more accurate and flexible management of menu item stock and availability.
---
 docker/Dockerfile                             |   7 +-
 docker/docker-compose.yml                     |   3 +-
 streamlit_app/pages/menu.py                   |  85 ++++++++++
 weird_salads/api/app.py                       |  58 ++++++-
 weird_salads/api/schemas.py                   | 125 ++++++++++++++-
 .../inventory/inventory_service/exceptions.py |   6 +
 .../inventory/inventory_service/inventory.py  | 129 +++++++++++++++
 .../inventory_service/inventory_service.py    | 151 ++++++++++++++++++
 .../repository/inventory_repository.py        | 131 +++++++++++++++
 weird_salads/utils/database/seed_db.py        |   4 +-
 10 files changed, 687 insertions(+), 12 deletions(-)
 create mode 100644 streamlit_app/pages/menu.py
 create mode 100644 weird_salads/inventory/inventory_service/exceptions.py
 create mode 100644 weird_salads/inventory/inventory_service/inventory.py
 create mode 100644 weird_salads/inventory/inventory_service/inventory_service.py
 create mode 100644 weird_salads/inventory/repository/inventory_repository.py

diff --git a/docker/Dockerfile b/docker/Dockerfile
index cf761d6..ca44632 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -18,10 +18,11 @@ COPY ../database/ /app/database
 # https://setuptools-scm.readthedocs.io/en/latest/usage/
 # https://stackoverflow.com/questions/77572077/using-setuptools-scm-pretend-version-for-package-version-inside-docker-with-git
 ENV SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MY_PACKAGE=0.0
-RUN pip install --root-user-action=ignore --no-cache-dir .
+RUN pip install --root-user-action=ignore .
 
-ARG LOCATION_ID
+ARG SEED_LOCATION_ID
+ARG SEED_QUANTITY
 # Command to run FastAPI using Uvicorn
 # CMD ["uvicorn", "weird_salads.api.app:app", "--host", "0.0.0.0", "--port", "8000"]
-CMD ["sh", "-c", "alembic upgrade head && python weird_salads/utils/database/seed_db.py --location_id ${LOCATION_ID} --base_path data/ && uvicorn weird_salads.api.app:app --host 0.0.0.0 --port 8000"]
+CMD ["sh", "-c", "alembic upgrade head && python weird_salads/utils/database/seed_db.py --location_id ${SEED_LOCATION_ID} --quantity ${SEED_QUANTITY} --base_path data/ && uvicorn weird_salads.api.app:app --host 0.0.0.0 --port 8000"]
 # CMD ["sleep", "365d"]
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index b6b28c5..0f8ec18 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -9,7 +9,8 @@ services:
       - ../data/:/app/data/ # Mount the data
     environment:
       - DATABASE_URL=sqlite:///data/orders.db
-      - LOCATION_ID=1 # put as an environment variable
+      - SEED_LOCATION_ID=1 # location id for DB seeding
+      - SEED_QUANTITY=7.4 # quantity of ingredients for DB seeding
 
   streamlit:
     build:
diff --git a/streamlit_app/pages/menu.py b/streamlit_app/pages/menu.py
new file mode 100644
index 0000000..cd47b98
--- /dev/null
+++ b/streamlit_app/pages/menu.py
@@ -0,0 +1,85 @@
+import pandas as pd
+import requests
+import streamlit as st
+
+
+# Function to fetch menu items
+def fetch_menu_items():
+    try:
+        response = requests.get("http://fastapi:8000/menu/")
+        response.raise_for_status()
+        data = response.json()
+        return data.get("items", [])
+    except requests.exceptions.RequestException as e:
+        st.write("Failed to connect to FastAPI:", e)
+        return []
+    except Exception as e:
+        st.write("An error occurred:", e)
+        return []
+
+
+# Function to fetch availability of a specific menu item
+def fetch_item_availability(item_id):
+    try:
+        response = requests.get(
+            f"http://fastapi:8000/menu/{item_id}/availability"
+        )  # Update endpoint if necessary
+        response.raise_for_status()
+        return response.json()
+    except requests.exceptions.RequestException as e:
+        st.write("Failed to connect to FastAPI:", e)
+        return None
+    except Exception as e:
+        st.write("An error occurred:", e)
+        return None
+
+
+# Function to display menu items
+def display_menu():
+    st.header("Menu")
+
+    menu_items = fetch_menu_items()
+
+    # Initialize session state for tracking the current order and status
+    if "current_order" not in st.session_state:
+        st.session_state.current_order = None
+        st.session_state.order_status = ""
+
+    if menu_items:
+        # Create a DataFrame to display menu items
+        df = pd.DataFrame(menu_items)
+
+        # Filter out items that are not on the 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']})")
+
+            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"
+
+            with cols[2]:
+                if st.session_state.current_order == row["name"]:
+                    st.write(f"Ordered: {row['name']}")
+                    st.write(st.session_state.order_status)
+
+    else:
+        st.write("No menu items found.")
+
+
+if __name__ == "__main__":
+    display_menu()
diff --git a/weird_salads/api/app.py b/weird_salads/api/app.py
index d0c5a70..a4313b3 100644
--- a/weird_salads/api/app.py
+++ b/weird_salads/api/app.py
@@ -1,8 +1,17 @@
-from fastapi import FastAPI
+from fastapi import FastAPI, HTTPException
 from starlette import status
 
-from weird_salads.api.schemas import CreateOrderSchema  # noqa
-from weird_salads.api.schemas import GetOrderSchema, GetOrdersSchema
+from weird_salads.api.schemas import (
+    CreateOrderSchema,
+    GetMenuItemAvailabilitySchema,
+    GetMenuItemSchema,
+    GetOrderSchema,
+    GetOrdersSchema,
+    GetSimpleMenuSchema,
+)
+from weird_salads.inventory.inventory_service.exceptions import MenuItemNotFoundError
+from weird_salads.inventory.inventory_service.inventory_service import MenuService
+from weird_salads.inventory.repository.inventory_repository import MenuRepository
 from weird_salads.orders.orders_service.orders_service import OrdersService
 from weird_salads.orders.repository.orders_repository import OrdersRepository
 from weird_salads.utils.unit_of_work import UnitOfWork
@@ -10,6 +19,49 @@
 app = FastAPI()
 
 
+# Menu
+@app.get("/menu", response_model=GetSimpleMenuSchema, tags=["Menu"])
+def get_menu():
+    with UnitOfWork() as unit_of_work:
+        repo = MenuRepository(unit_of_work.session)
+        inventory_service = MenuService(repo)
+        results = inventory_service.list_menu()
+    return {"items": [result.dict() for result in results]}
+
+
+@app.get("/menu/{item_id}", response_model=GetMenuItemSchema, tags=["Menu"])
+def get_order(item_id: int):
+    try:
+        with UnitOfWork() as unit_of_work:
+            repo = MenuRepository(unit_of_work.session)
+            inventory_service = MenuService(repo)
+            order = inventory_service.get_item(item_id=item_id)
+        return order
+    except MenuItemNotFoundError:
+        raise HTTPException(
+            status_code=404, detail=f"Menu Item with ID {item_id} not found"
+        )
+
+
+@app.get(
+    "/menu/{item_id}/availability",
+    response_model=GetMenuItemAvailabilitySchema,
+    tags=["Menu"],
+)
+def get_availability(item_id: int):
+    try:
+        with UnitOfWork() as unit_of_work:
+            repo = MenuRepository(unit_of_work.session)
+            inventory_service = MenuService(repo)
+            order = inventory_service.get_recipe_item_availability(item_id=item_id)
+        return order  # Ensure `order` is an instance of `GetRecipeItemSchema`
+    except MenuItemNotFoundError:
+        raise HTTPException(
+            status_code=404, detail=f"Menu Item with ID {item_id} not found"
+        )
+
+
+# Orders
 @app.get(
     "/order",
     response_model=GetOrdersSchema,
diff --git a/weird_salads/api/schemas.py b/weird_salads/api/schemas.py
index d159885..821529e 100644
--- a/weird_salads/api/schemas.py
+++ b/weird_salads/api/schemas.py
@@ -2,15 +2,134 @@
 Pydantic Schemas for API
 """
 
-from datetime import datetime
-from typing import List
+from datetime import datetime, timezone
+from enum import Enum
+from typing import List, Optional
 
-from pydantic import BaseModel
+from pydantic import BaseModel, Field, field_validator
+from typing_extensions import Annotated
 
 # from typing_extensions import Annotated
 
 
+# =================================
+# Menu-related Schema for the API
+# =================================
+class UnitOfMeasure(str, Enum):
+    """
+    Enum for units of measure
+    """
+
+    liter = "liter"
+    deciliter = "deciliter"
+    centiliter = "centiliter"
+    milliliter = "milliliter"
+
+
+class SimpleMenuItemSchema(BaseModel):
+    """
+    Simple Menu Item (an overview).
+    """
+
+    name: str
+    description: Optional[str] = None
+    price: Annotated[float, Field(ge=0.0, strict=True)]
+    created_on: datetime = datetime.now(timezone.utc)
+    on_menu: bool = True
+
+    class Config:
+        extra = "forbid"
+
+    @field_validator("price")
+    def quantity_non_nullable(cls, value):
+        assert value is not None, "price may not be None"
+        return value
+
+
+# GET, now includes id...
+class GetSimpleMenuItemSchema(SimpleMenuItemSchema):
+    id: int
+
+
+# GET Menu (list of Menu items)
+class GetSimpleMenuSchema(BaseModel):
+    """
+    Menu (GET)
+    """
+
+    items: List[GetSimpleMenuItemSchema]
+
+    class Config:
+        extra = "forbid"
+
+
+# Ingredient-related things
+# -------------------------
+
+
+class IngredientItemSchema(BaseModel):
+    name: str
+    description: Optional[str] = None
+
+    class Config:
+        extra = "forbid"
+
+
+class GetIngredientItemSchema(IngredientItemSchema):
+    id: int
+
+    class Config:
+        extra = "forbid"
+
+
+# Schema for MenuItemIngredient
+class MenuItemIngredientSchema(BaseModel):
+    quantity: float
+    unit: UnitOfMeasure
+    ingredient: GetIngredientItemSchema
+
+    class Config:
+        extra = "forbid"
+
+
+# GET, now includes id and ingredients...
+class GetMenuItemSchema(GetSimpleMenuItemSchema):
+    """
+    Menu Item detail (GET)
+    """
+
+    id: int
+    ingredients: List[
+        MenuItemIngredientSchema
+    ]  # Include ingredients with the menu item
+
+
+# Stock-related things
+# --------------------
+
+
+class MenuItemAvailabilitySchema(BaseModel):
+    ingredient: GetIngredientItemSchema
+    required_quantity: Annotated[float, Field(ge=0.0, strict=True)]
+    available_quantity: Annotated[float, Field(ge=0.0, strict=True)]
+    unit: UnitOfMeasure
+
+    class Config:
+        extra = "forbid"
+
+
+# Schema for menu item availability
+class GetMenuItemAvailabilitySchema(GetSimpleMenuItemSchema):
+    available_portions: int = 0
+    ingredient_availability: List[MenuItemAvailabilitySchema]
+
+    class Config:
+        extra = "forbid"
+
+
+# =================================
 # Orders-related Schema for the API
+# =================================
 class CreateOrderSchema(BaseModel):
     menu_id: int
 
diff --git a/weird_salads/inventory/inventory_service/exceptions.py b/weird_salads/inventory/inventory_service/exceptions.py
new file mode 100644
index 0000000..baafa43
--- /dev/null
+++ b/weird_salads/inventory/inventory_service/exceptions.py
@@ -0,0 +1,6 @@
+class MenuItemNotFoundError(Exception):
+    pass
+
+
+class UnitConversionError(Exception):
+    pass
diff --git a/weird_salads/inventory/inventory_service/inventory.py b/weird_salads/inventory/inventory_service/inventory.py
new file mode 100644
index 0000000..a7ddaa5
--- /dev/null
+++ b/weird_salads/inventory/inventory_service/inventory.py
@@ -0,0 +1,129 @@
+"""
+Classes
+"""
+
+__all__ = [
+    "SimpleMenuItem",
+    "MenuItem",
+    "MenuItemIngredient",
+    "IngredientItem",
+    "StockItem",
+]
+
+
+# - MenuItem holds MenuItemIngredients
+# - SimpleMenuItem <- a simplified version of MenuItem sans ingredients
+# - MenuAvailabilityItem <- more complex MenuItem with availability info
+class SimpleMenuItem:
+    def __init__(self, id, name, description, price, created_on, on_menu):
+        self.name = name
+        self.id = id
+        self.description = description
+        self.price = price
+        self.created_on = created_on
+        self.on_menu = on_menu
+
+    def dict(self):
+        return {
+            "id": self.id,
+            "name": self.name,
+            "description": self.description,
+            "price": self.price,
+            "created_on": self.created_on,
+            "on_menu": self.on_menu,
+        }
+
+
+# RecipeItem holds a set of RecipeIngredient objects
+class MenuItem:
+    def __init__(
+        self, id, name, description, price, created_on, on_menu, ingredients=None
+    ):
+        self.id = id
+        self.name = name
+        self.description = description
+        self.price = price
+        self.created_on = created_on
+        self.on_menu = on_menu
+        # Initialize ingredients as MenuItemIngredient instances from dictionaries
+        self.ingredients = [MenuItemIngredient(**item) for item in (ingredients or [])]
+
+    def dict(self):
+        result = {
+            "id": self.id,
+            "name": self.name,
+            "description": self.description,
+            "price": self.price,
+            "created_on": self.created_on.isoformat() if self.created_on else None,
+            "on_menu": self.on_menu,
+            "ingredients": [ingredient.dict() for ingredient in self.ingredients],
+        }
+        return result
+
+
+class MenuItemIngredient:
+    def __init__(self, quantity, unit, ingredient=None):
+        self.quantity = quantity
+        self.unit = unit
+        # Initialize ingredient as IngredientItem from dictionary
+        self.ingredient = IngredientItem(**(ingredient or {}))
+
+    def dict(self):
+        result = {
+            "quantity": self.quantity,
+            "unit": self.unit,
+            "ingredient": self.ingredient.dict() if self.ingredient else None,
+        }
+        return result
+
+
+class IngredientItem:
+    def __init__(self, id, name, description):
+        self.id = id
+        self.name = name
+        self.description = description
+
+    def dict(self):
+        result = {
+            "id": self.id,
+            "name": self.name,
+            "description": self.description,
+        }
+        return result
+
+
+class StockItem:
+    def __init__(
+        self,
+        id,
+        ingredient_id,
+        unit,
+        quantity,
+        cost,
+        delivery_date,
+        created_on,
+        order_=None,
+    ):
+        self._order = order_
+        self._id = id
+        self.ingredient_id = ingredient_id
+        self.unit = unit
+        self.quantity = quantity
+        self.cost = cost
+        self.delivery_date = delivery_date
+        self.created_on = created_on
+
+    @property
+    def id(self):
+        return self._id or self._order.id
+
+    def dict(self):
+        return {
+            "id": self.id,
+            "ingredient_id": self.ingredient_id,
+            "unit": self.unit,
+            "quantity": self.quantity,
+            "cost": self.cost,
+            "delivery_date": self.delivery_date,
+            "created_on": self.created_on,
+        }
diff --git a/weird_salads/inventory/inventory_service/inventory_service.py b/weird_salads/inventory/inventory_service/inventory_service.py
new file mode 100644
index 0000000..aded268
--- /dev/null
+++ b/weird_salads/inventory/inventory_service/inventory_service.py
@@ -0,0 +1,151 @@
+"""
+Services
+"""
+
+from typing import Any, Dict, List
+
+from weird_salads.api.schemas import UnitOfMeasure
+from weird_salads.inventory.inventory_service.exceptions import MenuItemNotFoundError
+from weird_salads.inventory.inventory_service.inventory import (
+    MenuItem,
+    MenuItemIngredient,
+)
+from weird_salads.inventory.repository.inventory_repository import MenuRepository
+
+__all__ = ["MenuService", "UNIT_CONVERSIONS_TO_LITRE"]
+
+# How many of unit in one litre
+UNIT_CONVERSIONS_TO_LITRE = {
+    UnitOfMeasure.liter: 1,
+    UnitOfMeasure.deciliter: 10,
+    UnitOfMeasure.centiliter: 100,
+    UnitOfMeasure.milliliter: 1000,
+}
+
+
+class MenuService:
+    def __init__(self, menu_repository: MenuRepository):
+        self.menu_repository = menu_repository
+
+    # for a simple representation
+    def get(self, item_id):
+        menu_item = self.menu_repository.get(item_id)
+        if menu_item is not None:
+            return menu_item
+        raise MenuItemNotFoundError(f"Menu item with id {item_id} not found")
+
+    def get_item(self, item_id: int) -> MenuItem:
+        menu_item = self.menu_repository.get_tree(item_id)
+        if menu_item is not None:
+            return menu_item
+        raise MenuItemNotFoundError(f"Menu item with id {item_id} not found")
+
+    def list_menu(self):
+        return self.menu_repository.list()
+
+    # Fetch stock data for an ingredient
+    def _fetch_stock_data(
+        self, ingredient_id: int, required_unit: UnitOfMeasure
+    ) -> float:
+        """
+        Fetch the stock data for an ingredient_id and convert to the required unit.
+        """
+        try:
+            stock_items = self.get_ingredient(ingredient_id)
+            total_quantity_in_required_unit = sum(
+                self._convert_to_unit(item.quantity, item.unit, required_unit)
+                for item in stock_items
+            )
+            return total_quantity_in_required_unit
+        except Exception as e:
+            print(f"Error fetching stock data: {e}")
+            return 0
+
+    # Convert quantity from one unit to another
+    def _convert_to_unit(
+        self, quantity: float, from_unit: UnitOfMeasure, to_unit: UnitOfMeasure
+    ) -> float:
+        if from_unit == to_unit:
+            return quantity
+        quantity_in_litres = quantity / UNIT_CONVERSIONS_TO_LITRE[from_unit]
+        return quantity_in_litres * UNIT_CONVERSIONS_TO_LITRE[to_unit]
+
+    # Calculate available portions based on recipe ingredients
+    def _calculate_available_portions(
+        self, recipe_ingredients: List[MenuItemIngredient]
+    ) -> float:
+        available_portions = float("inf")
+        for ri in recipe_ingredients:
+            ingredient_id = ri.ingredient.id
+            required_quantity = ri.quantity
+            required_unit = ri.unit
+            try:
+                # !TODO duplicated code
+                total_quantity_in_stock = self._fetch_stock_data(
+                    ingredient_id, required_unit
+                )
+                if required_quantity > 0:
+                    portions_based_on_ingredient = (
+                        total_quantity_in_stock // required_quantity
+                    )
+                    available_portions = min(
+                        available_portions, portions_based_on_ingredient
+                    )
+            except Exception as e:
+                print(f"Error processing ingredient ID {ingredient_id}: {e}")
+                available_portions = 0
+                break
+        return available_portions
+
+    # Get availability of a recipe item
+    def get_recipe_item_availability(self, item_id: int) -> Dict[str, Any]:
+        menu_item_with_ingredients = self.get_item(item_id)
+        ingredients = menu_item_with_ingredients.ingredients
+
+        ingredient_availability = [
+            {
+                "ingredient": {
+                    "id": ri.ingredient.id,
+                    "name": ri.ingredient.name,
+                    "description": ri.ingredient.description,
+                },
+                "required_quantity": ri.quantity,
+                "available_quantity": self._fetch_stock_data(ri.ingredient.id, ri.unit),
+                "unit": ri.unit,
+            }
+            for ri in ingredients
+        ]
+
+        available_portions = self._calculate_available_portions(ingredients)
+
+        return {
+            "id": menu_item_with_ingredients.id,
+            "name": menu_item_with_ingredients.name,
+            "description": menu_item_with_ingredients.description or "",
+            "price": menu_item_with_ingredients.price,
+            "created_on": menu_item_with_ingredients.created_on,
+            "on_menu": menu_item_with_ingredients.on_menu,
+            "available_portions": int(available_portions),
+            "ingredient_availability": ingredient_availability,
+        }
+
+    # - Stock-related
+    # ---- ingredient_id queries
+    def get_ingredient(self, ingredient_id: int):
+        ingredient_item = self.menu_repository.get_ingredient(ingredient_id)
+        if ingredient_item is not None:
+            return ingredient_item
+        raise ValueError(f"items with id {ingredient_id} not found")  # fix
+
+    def ingest_stock(self, item):
+        return self.stock_repository.add_stock(item)
+
+    # ---- stock_id queries
+    def get_stock_item(self, stock_id: str):
+        stock_item = self.menu_repository.get_stock(stock_id)
+        if stock_item is not None:
+            return stock_item
+        raise ValueError(f"stock with id {stock_id} not found")  # fix
+
+    def list_stock(self):  # needs options for filtering
+        return self.menu_repository.list_stock()
diff --git a/weird_salads/inventory/repository/inventory_repository.py b/weird_salads/inventory/repository/inventory_repository.py
new file mode 100644
index 0000000..de5c3da
--- /dev/null
+++ b/weird_salads/inventory/repository/inventory_repository.py
@@ -0,0 +1,131 @@
+"""
+Building on a Repository Pattern
+"""
+
+
+from typing import List
+
+from sqlalchemy.orm import joinedload
+
+from weird_salads.inventory.inventory_service.inventory import (
+    MenuItem,
+    SimpleMenuItem,
+    StockItem,
+)
+from weird_salads.inventory.repository.models import (
+    MenuModel,
+    RecipeIngredientModel,
+    StockModel,
+)
+
+__all__ = ["MenuRepository"]
+
+
+class MenuRepository:
+    def __init__(self, session):
+        self.session = session
+
+    def add(self):
+        pass
+
+    def _get(self, id):
+        return (
+            self.session.query(MenuModel).filter(MenuModel.id == str(id)).first()
+        )  # noqa: E501
+
+    def get(self, id):
+        order = self._get(id)
+        if order is not None:
+            # do we want to return an instance of a SQLalchemy model?
+            return SimpleMenuItem(**order.dict())
+
+    def _get_tree(self, id):
+        return (
+            self.session.query(MenuModel)
+            .options(
+                joinedload(MenuModel.ingredients).joinedload(
+                    RecipeIngredientModel.ingredient
+                )
+            )
+            .filter(MenuModel.id == id)
+            .first()
+        )
+
+    def get_tree(self, id: int) -> MenuItem:
+        # Fetch the tree data
+        tree = self._get_tree(id)
+
+        # !TODO tidy this up, it's not elegant
+        if tree is not None:
+            # Prepare ingredients data as dictionaries for MenuItemIngredient instances
+            ingredients = [
+                {
+                    "quantity": ri.quantity,
+                    "unit": ri.unit,
+                    "ingredient": {
+                        "id": ri.ingredient.id,
+                        "name": ri.ingredient.name,
+                        "description": ri.ingredient.description or "",
+                    },
+                }
+                for ri in tree.ingredients
+            ]
+
+            # Create a MenuItem instance
+            menu_item = MenuItem(
+                id=tree.id,
+                name=tree.name,
+                description=tree.description,
+                price=tree.price,
+                created_on=tree.created_on,
+                on_menu=tree.on_menu,
+                ingredients=ingredients,
+            )
+
+            # Convert MenuItem to a dictionary and return it
+            return menu_item
+
+    def list(self, limit=None):
+        query = self.session.query(MenuModel)
+        records = query.limit(limit).all()
+        return [SimpleMenuItem(**record.dict()) for record in records]
+
+    def update(self, id):
+        pass
+
+    def delete(self, id):
+        pass
+
+    # - Stock-related
+    def _get_ingredient(self, id: int):
+        return (
+            self.session.query(StockModel)
+            .filter(StockModel.ingredient_id == int(id))
+            .all()
+        )  # noqa: E501
+
+    def get_ingredient(self, id: int) -> List[StockItem]:
+        ingredients = self._get_ingredient(id)
+        if ingredients is not None:
+            return [StockItem(**ingredient.dict()) for ingredient in ingredients]
+
+    def _get_stock(self, id: str):
+        return self.session.query(StockModel).filter(StockModel.id == id).first()
+
+    def get_stock(self, id_: str):
+        order = self._get(id)
+        if order is not None:
+            return StockItem(**order.dict())
+
+    def list_stock(self, limit=None):
+        query = self.session.query(StockModel)
+        records = query.all()
+        return [StockItem(**record.dict()) for record in records]
+        # return [Ingredient(**record.dict()) for record in records]
+        # should this return an IngredientItemsModel?
+
+    def add_stock(self, item):
+        print(item)
+        record = StockModel(**item)
+        self.session.add(record)
+        return StockItem(**record.dict(), order_=record)
diff --git a/weird_salads/utils/database/seed_db.py b/weird_salads/utils/database/seed_db.py
index ce96f02..5d86d25 100644
--- a/weird_salads/utils/database/seed_db.py
+++ b/weird_salads/utils/database/seed_db.py
@@ -211,7 +211,7 @@ def main(location_id: int, quantity: int, base_path: Path) -> None:
                 )
 
                 logger.info(
-                    f"Seeding completed successfully for location {location_id}."
+                    f"Seeding completed successfully for location {location_id} and quantity {quantity}."  # noqa: E501
                 )
             else:
                 logger.info("Database already contains data. Skipping seeding.")
@@ -229,7 +229,7 @@ def main(location_id: int, quantity: int, base_path: Path) -> None:
     )
     parser.add_argument(
         "--quantity",
-        type=int,
+        type=float,
         default=0,
         help="The quantity of data to seed (in respective units).",
     )