diff --git a/app/backend/database.py b/app/backend/database.py index 340a31feb280d4c31374a3f97629d4ac2dc4bac6..51f1c51e33690b2efbda063586678188c91e6dfa 100644 --- a/app/backend/database.py +++ b/app/backend/database.py @@ -7,8 +7,8 @@ engine = create_engine(settings.database_url, echo=settings.debug) def init_db(): SQLModel.metadata.create_all(engine, checkfirst=True) - with Session(engine) as session: - insert_dummy_data(session) + # with Session(engine) as session: + # insert_dummy_data(session) def get_session(): diff --git a/app/backend/models/models.py b/app/backend/models/models.py index 8397f9f6a4dc9c51166c40ef9d6584c58dffaadc..77b9b590cf60683d884d81632073388953082281 100644 --- a/app/backend/models/models.py +++ b/app/backend/models/models.py @@ -21,6 +21,10 @@ class Shop(SQLModel, table=True): name: str = Field(unique=True, index=True) description: Optional[str] = None image_url: Optional[str] = None # Image URL for shop + # location coordinates + address: str + latitude: float + longitude: float created_at: datetime = Field(default_factory=datetime.utcnow) owner: User = Relationship(back_populates="shops") @@ -62,7 +66,12 @@ class Order(SQLModel, table=True): user_id: int = Field(foreign_key="user.id") shop_id: int = Field(foreign_key="shop.id") total_price: float + shipping_price: float status: str = Field(default="pending") + # delivery location coordinates + delivery_address: str + delivery_latitude: float + delivery_longitude: float created_at: datetime = Field(default_factory=datetime.utcnow) user: User = Relationship(back_populates="orders") diff --git a/app/backend/routes/order.py b/app/backend/routes/order.py index 14b55f9a926e42f4397c760aa1ab378c7900a38b..c2eefde4e63ae7fe5b6f18772dfa033a7b5ba463 100644 --- a/app/backend/routes/order.py +++ b/app/backend/routes/order.py @@ -1,9 +1,11 @@ from fastapi import APIRouter, Depends, HTTPException from sqlmodel import Session, select, delete +from geopy.geocoders import Nominatim +from geopy.distance import geodesic from backend.database import get_session from backend.routes.auth import get_current_user -from backend.models.models import Order, OrderItem, User, Product -from backend.schemas.order import OrderCreate, OrderRead +from backend.models.models import Order, OrderItem, User, Product, Shop +from backend.schemas.order import OrderCreate, OrderRead, OrderUpdate router = APIRouter() @@ -14,7 +16,26 @@ def create_order( session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): - total_price = 0 + # Fetch the shop details + shop = session.get(Shop, order_data.shop_id) + if not shop: + raise HTTPException(status_code=404, detail="Shop not found") + + # Geocode the delivery address + geolocator = Nominatim(user_agent="order_locator") + delivery_location = geolocator.geocode(order_data.delivery_address) + if not delivery_location: + raise HTTPException(status_code=400, detail="Invalid delivery address provided") + + # Calculate the distance between the shop and the delivery location + shop_location = (shop.latitude, shop.longitude) + delivery_coordinates = (delivery_location.latitude, delivery_location.longitude) + distance_km = geodesic(shop_location, delivery_coordinates).kilometers + + # Calculate the shipping price ($1 per km) + shipping_price = distance_km * 1.0 + + total_price = shipping_price for item in order_data.items: product = session.get(Product, item.product_id) if not product or product.stock < item.quantity: @@ -23,13 +44,22 @@ def create_order( ) total_price += item.quantity * product.price + # Create the order new_order = Order( - user_id=current_user.id, shop_id=order_data.shop_id, total_price=total_price + user_id=current_user.id, + shop_id=order_data.shop_id, + total_price=total_price, + shipping_price=shipping_price, + status="pending", + delivery_address=order_data.delivery_address, + delivery_latitude=delivery_location.latitude, + delivery_longitude=delivery_location.longitude, ) session.add(new_order) session.commit() session.refresh(new_order) + # Create order items for item in order_data.items: order_item = OrderItem( order_id=new_order.id, @@ -71,19 +101,50 @@ def get_order( @router.put("/status/{order_id}", response_model=OrderRead) def update_order( order_id: int, - status: str, + order_update: OrderUpdate, session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): order = session.get(Order, order_id) if not order or order.user_id != current_user.id: raise HTTPException(status_code=404, detail="Order not found or unauthorized") - if order.status != "pending" or status != "completed": - raise HTTPException( - status_code=400, - detail="Order can only be updated from pending to completed", + + # Update the delivery address if provided + if order_update.delivery_address: + geolocator = Nominatim(user_agent="order_locator") + delivery_location = geolocator.geocode(order_update.delivery_address) + if not delivery_location: + raise HTTPException(status_code=400, detail="Invalid delivery address") + order.delivery_address = order_update.delivery_address + order.delivery_latitude = delivery_location.latitude + order.delivery_longitude = delivery_location.longitude + + # Recalculate the shipping price + shop = session.get(Shop, order.shop_id) + if not shop: + raise HTTPException(status_code=404, detail="Shop not found") + shop_location = (shop.latitude, shop.longitude) + delivery_coordinates = (delivery_location.latitude, delivery_location.longitude) + distance_km = geodesic(shop_location, delivery_coordinates).kilometers + shipping_price = distance_km * 1.0 + + # Update the total price + product_total = sum( + item.quantity * session.get(Product, item.product_id).price + for item in order.order_items ) - order.status = status + order.shipping_price = shipping_price + order.total_price = product_total + shipping_price + + # Update the order status if provided + if order_update.status: + if order.status != "pending" or order_update.status != "completed": + raise HTTPException( + status_code=400, + detail="Order can only be updated from pending to completed", + ) + order.status = order_update.status + session.commit() session.refresh(order) return order diff --git a/app/backend/routes/shop.py b/app/backend/routes/shop.py index 643b8b2ef82f4cfa0d669dbf5ae004dcc5e125ee..2ce59c7996df14c40ed37f926e64349a85cb6c88 100644 --- a/app/backend/routes/shop.py +++ b/app/backend/routes/shop.py @@ -1,7 +1,8 @@ from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form from sqlmodel import Session +from geopy.geocoders import Nominatim from backend.models.models import Shop, User -from backend.schemas.shop import ShopCreate, ShopRead +from backend.schemas.shop import ShopRead from backend.database import get_session from backend.routes.auth import get_current_user from core.config import settings @@ -15,31 +16,44 @@ router = APIRouter() def create_shop( name: str = Form(...), description: str = Form(None), + address: str = Form(...), file: UploadFile = File(None), session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): - shop = ShopCreate(name=name, description=description, owner_id=current_user.id) - db_shop = Shop.from_orm(shop) - - session.add(db_shop) + # Get latitude and longitude from address + geolocator = Nominatim(user_agent="shop_locator") + location = geolocator.geocode(address) + if not location: + raise HTTPException(status_code=400, detail="Invalid address") + + shop = Shop( + name=name, + description=description, + address=address, + latitude=location.latitude, + longitude=location.longitude, + owner_id=current_user.id, + ) + session.add(shop) session.commit() - session.refresh(db_shop) + session.refresh(shop) - shop_dir = os.path.join(settings.static_dir, f"shop_{db_shop.name}") + shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) if file and file.filename: file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - db_shop.image_url = file_location + shop.image_url = file_location else: - db_shop.image_url = os.path.join( - settings.static_dir, "default/default_shop.png" - ) + shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") - return db_shop + session.commit() + session.refresh(shop) + + return shop @router.get("/list", response_model=list[ShopRead]) @@ -61,41 +75,47 @@ def update_shop( shop_id: int, name: str = Form(None), description: str = Form(None), + address: str = Form(None), file: UploadFile = File(None), session: Session = Depends(get_session), current_user: User = Depends(get_current_user), ): - db_shop = session.get(Shop, shop_id) - if not db_shop: + shop = session.get(Shop, shop_id) + if not shop: raise HTTPException(status_code=404, detail="Shop not found") # Ensure the current user is the shop owner - if db_shop.owner_id != current_user.id: + if shop.owner_id != current_user.id: raise HTTPException(status_code=403, detail="Unauthorized to update this shop") - if name: - db_shop.name = name + shop.name = name if description: - db_shop.description = description - - shop_dir = os.path.join(settings.static_dir, f"shop_{db_shop.name}") + shop.description = description + if address: + geolocator = Nominatim(user_agent="shop_locator") + location = geolocator.geocode(address) + if not location: + raise HTTPException(status_code=400, detail="Invalid address") + shop.address = address + shop.latitude = location.latitude + shop.longitude = location.longitude + + shop_dir = os.path.join(settings.static_dir, f"shop_{shop.name}") os.makedirs(shop_dir, exist_ok=True) if file and file.filename: file_location = os.path.join(shop_dir, file.filename) with open(file_location, "wb") as buffer: shutil.copyfileobj(file.file, buffer) - db_shop.image_url = file_location + shop.image_url = file_location else: - db_shop.image_url = os.path.join( - settings.static_dir, "default/default_shop.png" - ) + shop.image_url = os.path.join(settings.static_dir, "default/default_shop.png") - session.add(db_shop) + session.add(shop) session.commit() - session.refresh(db_shop) + session.refresh(shop) - return db_shop + return shop @router.delete("/delete/{shop_id}") diff --git a/app/backend/schemas/order.py b/app/backend/schemas/order.py index 37fdda72ec31777d18347c516714ac9275a14abb..ca5e2ce1dbe2a38530794397f98fdd90cdc38e3a 100644 --- a/app/backend/schemas/order.py +++ b/app/backend/schemas/order.py @@ -11,6 +11,7 @@ class OrderItemCreate(BaseModel): class OrderCreate(BaseModel): shop_id: int items: List[OrderItemCreate] + delivery_address: str class OrderItemRead(BaseModel): @@ -28,9 +29,15 @@ class OrderRead(BaseModel): user_id: int shop_id: int total_price: float + shipping_price: float status: str created_at: datetime order_items: List[OrderItemRead] class Config: from_attributes = True + + +class OrderUpdate(BaseModel): + delivery_address: Optional[str] = None + status: Optional[str] = None diff --git a/app/backend/schemas/shop.py b/app/backend/schemas/shop.py index 71763bb8b375e6051849d32a9ad5efad889a106b..8cfb87fc29627f7ade3fd7c0b119995f728d8dec 100644 --- a/app/backend/schemas/shop.py +++ b/app/backend/schemas/shop.py @@ -14,6 +14,7 @@ class ShopCreate(ShopBase): class ShopRead(ShopBase): id: int owner_id: int + address: str image_url: Optional[str] = None class Config: diff --git a/requirements.txt b/requirements.txt index 098a8e9d014d631acb415fded564d5774243a0a0..2383f12bc661c32a3b8eac0fcf9146bd60a0f3fe 100644 Binary files a/requirements.txt and b/requirements.txt differ