From 3bc2a695badd7524b841ddf82da094e160d9184e Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:00:58 +0200 Subject: [PATCH 01/12] change created_at in order model --- src/database/models/orders.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/database/models/orders.py b/src/database/models/orders.py index 7a42fa7..3d6ec3f 100644 --- a/src/database/models/orders.py +++ b/src/database/models/orders.py @@ -2,7 +2,7 @@ from decimal import Decimal from enum import Enum -from sqlalchemy import DECIMAL, DateTime, ForeignKey, Integer, String +from sqlalchemy import DECIMAL, DateTime, ForeignKey, Integer, String, func from sqlalchemy.orm import Mapped, mapped_column, relationship from database.models.accounts import User @@ -27,7 +27,7 @@ class Order(Base): Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) created_at: Mapped[datetime] = mapped_column( - DateTime, nullable=False, default=datetime.now + DateTime(timezone=True), server_default=func.now(), nullable=False ) status: Mapped[OrderStatusEnum] = mapped_column( String(50), nullable=False, default=OrderStatusEnum.PENDING.value From 1aff802902d580402ed791cdd7b6d60a4a7beb5c Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:06:43 +0200 Subject: [PATCH 02/12] Implement order routes --- src/routes/orders.py | 147 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/routes/orders.py diff --git a/src/routes/orders.py b/src/routes/orders.py new file mode 100644 index 0000000..3a02a7e --- /dev/null +++ b/src/routes/orders.py @@ -0,0 +1,147 @@ +from datetime import datetime + +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session, joinedload + +from database import get_db +from database.models.accounts import User +from database.models.orders import Order, OrderItem, OrderStatusEnum +from routes.profiles import get_current_user +from schemas.orders import OrderItemResponseSchema, MovieItemResponse, MessageResponseSchema + +router = APIRouter() + + +@router.get("/") +def root() -> dict: + return {"message": "Hello Test"} + + +""" +1. Place an Order +Endpoint: POST /orders/ +Description: Allows users to place an order for movies in their cart. +""" +# @router.post("/orders/") +# def place_order(user_id: int, db: Session = Depends(get_db)): +# pass + + +""" +2. View User Orders (depending on whether it is an admin or a user...) +Endpoint: GET /orders/ +Description: Retrieves a list of all orders placed by a specific user. +""" +@router.get("/orders/", response_model=list[OrderItemResponseSchema], status_code=status.HTTP_200_OK) +def get_user_orders( + user_id: int | None = Query(default=None, description="Filter orders by user ID"), + date_from: datetime | None = Query(default=None, description="Filter orders from this date"), + date_to: datetime | None = Query(default=None, description="Filter orders until this date"), + order_status: OrderStatusEnum | None = Query(default=None, description="Filter orders by status"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + if current_user.group.name != "user": + filters = [] + if user_id: + filters.append(Order.user_id == user_id) + if date_from: + filters.append(Order.created_at >= date_from) + if date_to: + filters.append(Order.created_at <= date_to) + if order_status: + filters.append(Order.status == order_status) + orders = db.query(Order).filter(*filters) + else: + if user_id or date_from or date_to or order_status: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission." + ) + orders = db.query(Order).filter(Order.user_id == current_user.id) + + orders = orders.options(joinedload(Order.order_items).joinedload(OrderItem.movie)) + orders = orders.order_by(Order.created_at.desc()).all() + + if not orders: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No orders found.") + + order_list = [ + OrderItemResponseSchema( + created_at=order.created_at, + movies=[ # ToDo - change it in future (can be N+1) + MovieItemResponse( + id=item.movie.id, name=item.movie.name + ) for item in order.order_items + ], + total_amount=order.total_amount, + status=order.status + ) + for order in orders + ] + + return order_list + + +""" +3. Confirm an Order +Endpoint: POST /orders/{order_id}/confirm/ +Description: Confirms an order and redirects the user to a payment gateway. +""" +@router.post("/orders/{order_id}/confirm/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) +def confirm_order(order_id: int, db: Session = Depends(get_db)): + order = db.query(Order).filter(Order.id == order_id).first() + if not order: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") + + if order.status != OrderStatusEnum.PENDING.value: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only pending orders can be confirmed." + ) + + # ToDo - redirect to payment... + + order.status = OrderStatusEnum.PAID.value + db.commit() + db.refresh(order) + + return MessageResponseSchema( + message="Order successfully confirmed." + ) + + +""" +4. Cancel an Order +Endpoint: POST /orders/{order_id}/cancel/ +Description: Allows users to cancel an order before payment is completed. +""" +@router.post("/orders/{order_id}/cancel/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) +def cancel_order(order_id: int, db: Session = Depends(get_db)): + order = db.query(Order).filter(Order.id == order_id).first() + if not order: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") + + if order.status != OrderStatusEnum.PENDING.value: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Only pending orders can be canceled." + ) + + order.status = OrderStatusEnum.CANCELED.value + db.commit() + db.refresh(order) + + return MessageResponseSchema( + message="Order successfully canceled." + ) + + +""" +5. Request a Refund +Endpoint: POST /orders/{order_id}/refund_request/ +Description: Allows users to request a refund for a paid order. +""" +# @router.post("/orders/{order_id}/refund_request/", response_model=OrderRefundRequestSchema) +# def request_refund(order_id: int, db: Session = Depends(get_db)): +# pass From 5ba568670eb8df28c432395303303067378ba566 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:06:56 +0200 Subject: [PATCH 03/12] Include order routes to global app router --- src/main.py | 5 ++++- src/routes/__init__.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main.py b/src/main.py index f342271..415601b 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from fastapi_pagination import add_pagination -from routes import accounts_router, movies_router, payments_router, profiles_router +from routes import accounts_router, movies_router, payments_router, profiles_router, orders_router from routes.shopping_cart import router as shopping_carts_router app = FastAPI( @@ -22,6 +22,9 @@ app.include_router( payments_router, prefix=f"{api_version_prefix}/payments", tags=["payments"] ) +app.include_router( + orders_router, prefix=f"{api_version_prefix}", tags=["orders"] +) add_pagination(app) app.include_router( diff --git a/src/routes/__init__.py b/src/routes/__init__.py index f078db2..7db7a48 100644 --- a/src/routes/__init__.py +++ b/src/routes/__init__.py @@ -2,3 +2,4 @@ from routes.payments import router as payments_router from routes.profiles import router as profiles_router from routes.movies import router as movies_router +from routes.orders import router as orders_router From 549094722062ed19be9eca97abcc289f792629ca Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:07:11 +0200 Subject: [PATCH 04/12] Implement get_current_user --- src/routes/profiles.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/routes/profiles.py b/src/routes/profiles.py index ae3fb2d..7ebc12b 100644 --- a/src/routes/profiles.py +++ b/src/routes/profiles.py @@ -13,15 +13,37 @@ UserGroupEnum, UserProfile, ) -from exceptions import BaseSecurityError, S3FileUploadError +from exceptions import BaseSecurityError, S3FileUploadError, TokenExpiredError from schemas.profiles import ProfileCreateSchema, ProfileResponseSchema from security.http import get_token from security.interfaces import JWTAuthManagerInterface +from security.token_manager import JWTAuthManager from storages import S3StorageInterface router = APIRouter() +def get_current_user( + token: str = Depends(get_token), + db: Session = Depends(get_db), + jwt_manager: JWTAuthManager = Depends(get_jwt_auth_manager) +) -> User: + try: + token_data = jwt_manager.decode_access_token(token) + user = db.query(User).filter_by(id=token_data.get("user_id")).first() + if not user or not user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="User not found or not active." + ) + return user + except TokenExpiredError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Token has expired." + ) + + @router.post( "/users/{user_id}/profile/", response_model=ProfileResponseSchema, From 11ecd3fc253409d37d1522c1ff2dfb8b70693c96 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:07:24 +0200 Subject: [PATCH 05/12] Implement order schemas --- src/schemas/orders.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 src/schemas/orders.py diff --git a/src/schemas/orders.py b/src/schemas/orders.py new file mode 100644 index 0000000..1402d52 --- /dev/null +++ b/src/schemas/orders.py @@ -0,0 +1,31 @@ +from datetime import datetime + +from pydantic import BaseModel + +from database.models.orders import OrderStatusEnum + + +""" Message... """ +class MessageResponseSchema(BaseModel): + message: str + + +""" 2. View User Orders """ +class MovieItemResponse(BaseModel): # ToDo Test delete in future because it must be from movies schemas + id: int + name: str + + model_config = { + "from_attributes": True + } + + +class OrderItemResponseSchema(BaseModel): + created_at: datetime + movies: list[MovieItemResponse] # ToDo from movies schemas will be... + total_amount: float | None + status: OrderStatusEnum + + model_config = { + "from_attributes": True + } From f59330944f905f1c816f84fe63c4249c8eebaff1 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:15:00 +0200 Subject: [PATCH 06/12] Implement place_order route --- src/database/models/orders.py | 346 +++++++++++++++++++++++++++++----- src/schemas/orders.py | 36 ++++ 2 files changed, 333 insertions(+), 49 deletions(-) diff --git a/src/database/models/orders.py b/src/database/models/orders.py index 3d6ec3f..b3a8900 100644 --- a/src/database/models/orders.py +++ b/src/database/models/orders.py @@ -1,64 +1,312 @@ from datetime import datetime -from decimal import Decimal -from enum import Enum - -from sqlalchemy import DECIMAL, DateTime, ForeignKey, Integer, String, func -from sqlalchemy.orm import Mapped, mapped_column, relationship +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session, joinedload +from database import get_db, Cart from database.models.accounts import User -from database.models.base import Base -from database.models.movies import Movie -from database.models.payments import Payment, PaymentItem +from database.models.orders import Order, OrderItem, OrderStatusEnum +from routes.profiles import get_current_user +from schemas.orders import OrderItemResponseSchema, MovieItemResponse, MessageResponseSchema +router = APIRouter() +@router.get("/") +def root() -> dict: + return {"message": "Hello Test"} -class OrderStatusEnum(str, Enum): - PENDING = "pending" - PAID = "paid" - CANCELED = "canceled" +# Зробити detail view, створення order, і також можна update і delete; додати пагінацію до list, +# Написати тести і свагер -class Order(Base): - __tablename__ = "orders" +""" +1. Place an Order +Endpoint: POST /orders/ +Description: Allows users to place an order for movies in their cart. +""" +@router.post("/orders/") +def place_order(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + cart = db.query(Cart).filter(Cart.user_id == current_user.id).first() + if not cart or not cart.items: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cart is empty.") - id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True, nullable=False - ) - user_id: Mapped[int] = mapped_column( - Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False - ) - created_at: Mapped[datetime] = mapped_column( - DateTime(timezone=True), server_default=func.now(), nullable=False + order = Order( + user_id=current_user.id, + total_amount=sum(item.movie.price for item in cart.items) ) - status: Mapped[OrderStatusEnum] = mapped_column( - String(50), nullable=False, default=OrderStatusEnum.PENDING.value - ) - total_amount: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 2)) + # order create + db.add(order) + db.commit() + db.refresh(order) - order_items: Mapped[list["OrderItem"]] = relationship( - "OrderItem", back_populates="order" - ) - user: Mapped["User"] = relationship("User", back_populates="orders") - payments: Mapped[list["Payment"]] = relationship( - "Payment", back_populates="order", cascade="all, delete-orphan" + # order item create + for cart_item in cart.items: + order_item = OrderItem( + order_id=order.id, + movie_id=cart_item.movie_id, + price_at_order=cart_item.movie.price + ) + db.add(order_item) + db.delete(cart_item) + + db.commit() + + return MessageResponseSchema( + message=f"Order placed successfully, your order_id: {order.id}" ) -class OrderItem(Base): - __tablename__ = "order_items" +""" +2. View User Orders (depending on whether it is an admin or a user...) +Endpoint: GET /orders/ +Description: Retrieves a list of all orders placed by a specific user. +""" +@router.get("/orders/", response_model=list[OrderItemResponseSchema], status_code=status.HTTP_200_OK) +def get_user_orders( + user_id: int | None = Query(default=None, description="Filter orders by user ID"), + date_from: datetime | None = Query(default=None, description="Filter orders from this date"), + date_to: datetime | None = Query(default=None, description="Filter orders until this date"), + order_status: OrderStatusEnum | None = Query(default=None, description="Filter orders by status"), + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + if current_user.group.name != "user": + filters = [] + if user_id: + filters.append(Order.user_id == user_id) + if date_from: + filters.append(Order.created_at >= date_from) + if date_to: + filters.append(Order.created_at <= date_to) + if order_status: + filters.append(Order.status == order_status) + orders = db.query(Order).filter(*filters) + else: + if user_id or date_from or date_to or order_status: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission." + ) + orders = db.query(Order).filter(Order.user_id == current_user.id) + orders = orders.options(joinedload(Order.order_items).joinedload(OrderItem.movie)) + orders = orders.order_by(Order.created_at.desc()).all() + if not orders: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No orders found.") + order_list = [ + OrderItemResponseSchema( + created_at=order.created_at, + movies=[ + MovieItemResponse( + id=item.movie.id, name=item.movie.name + ) for item in order.order_items + ], + total_amount=order.total_amount, + status=order.status + ) + for order in orders + ] + return order_list - id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True, nullable=False - ) - order_id: Mapped[int] = mapped_column( - Integer, ForeignKey("orders.id", ondelete="CASCADE"), nullable=False - ) - movie_id: Mapped[int] = mapped_column( - Integer, ForeignKey("movies.id", ondelete="CASCADE"), nullable=False - ) - price_at_order: Mapped[float] = mapped_column(DECIMAL(10, 2), nullable=False) - order: Mapped["Order"] = relationship("Order", back_populates="order_items") - movie: Mapped["Movie"] = relationship("Movie", back_populates="order_items") - payment_items: Mapped[list["PaymentItem"]] = relationship( - "PaymentItem", back_populates="order_item", cascade="all, delete-orphan" - ) +""" +3. Detail view of order +Endpoint: GET /orders/{order_id}/ +Description: Allows user to view details about order. +""" + + +""" +4. Update order +Endpoint: PATCH /orders/{order_id}/ +Description: Allows user to update order. +""" + + +""" +4. Delete order +Endpoint: DELETE /orders/{order_id}/ +Description: Allows user to delete order. +""" + + + + + + + + + + + + + + + + + + + + + +# """ +# 3. Confirm an Order +# Endpoint: POST /orders/{order_id}/confirm/ +# Description: Confirms an order and redirects the user to a payment gateway. +# """ +# @router.post("/orders/{order_id}/confirm/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) +# def confirm_order(order_id: int, db: Session = Depends(get_db)): +# order = db.query(Order).filter(Order.id == order_id).first() +# if not order: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") +# +# if order.status != OrderStatusEnum.PENDING.value: +# raise HTTPException( +# status_code=status.HTTP_400_BAD_REQUEST, +# detail="Only pending orders can be confirmed." +# ) +# +# # ToDo - redirect to payment... +# +# order.status = OrderStatusEnum.PAID.value +# db.commit() +# db.refresh(order) +# +# return MessageResponseSchema( +# message="Order successfully confirmed." +# ) +# +# +# """ +# 4. Cancel an Order +# Endpoint: POST /orders/{order_id}/cancel/ +# Description: Allows users to cancel an order before payment is completed. +# """ +# @router.post("/orders/{order_id}/cancel/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) +# def cancel_order(order_id: int, db: Session = Depends(get_db)): +# order = db.query(Order).filter(Order.id == order_id).first() +# if not order: +# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") +# +# if order.status != OrderStatusEnum.PENDING.value: +# raise HTTPException( +# status_code=status.HTTP_400_BAD_REQUEST, +# detail="Only pending orders can be canceled." +# ) +# +# order.status = OrderStatusEnum.CANCELED.value +# db.commit() +# db.refresh(order) +# +# return MessageResponseSchema( +# message="Order successfully canceled." +# ) +# +# +# """ +# 5. Request a Refund +# Endpoint: POST /orders/{order_id}/refund_request/ +# Description: Allows users to request a refund for a paid order. +# """ +# # @router.post("/orders/{order_id}/refund_request/", response_model=OrderRefundRequestSchema) +# # def request_refund(order_id: int, db: Session = Depends(get_db)): +# # pass + + +# @router.post("/refund", status_code=status.HTTP_200_OK) +# def refund_order( +# order_id: int, +# db: Session = Depends(get_db), +# token: str = Depends(get_token), +# jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager) +# ): +# try: +# payload = jwt_manager.decode_access_token(token) +# user_id = payload.get("user_id") +# except BaseSecurityError as e: +# raise HTTPException( +# status_code=status.HTTP_401_UNAUTHORIZED, +# detail=str(e) +# ) +# +# # Отримуємо замовлення та платіж +# order = db.query(OrderModel).filter_by(id=order_id).first() +# payment = db.query(PaymentModel).filter_by(order_id=order_id).first() +# +# # Перевірка, чи належить це замовлення користувачу і чи не було воно вже повернене +# if order.user_id != user_id or payment.status == "refunded": +# raise HTTPException( +# status_code=status.HTTP_400_BAD_REQUEST, +# detail="Order is either not yours or has already been refunded." +# ) +# +# # Перевірка статусу платежу через Stripe Checkout +# try: +# session = stripe.checkout.Session.retrieve(payment.external_payment_id) +# +# # Якщо сесія не успішна, рефандити не можна +# if session.payment_status != "paid": +# raise HTTPException( +# status_code=status.HTTP_400_BAD_REQUEST, +# detail="Only successful payments can be refunded." +# ) +# +# # Отримуємо PaymentIntent з сесії Stripe +# payment_intent_id = session.payment_intent +# +# # Здійснюємо рефанд через Stripe API +# stripe.Refund.create(payment_intent=payment_intent_id) +# +# # Оновлюємо статус замовлення і платежу +# order.status = "canceled" +# payment.status = "refunded" +# +# # Видаляємо покупку (якщо є) +# movie_ids = [order_item.movie_id for order_item in order.order_items] +# db.query(Purchases).filter( +# and_(Purchases.movie_id.in_(movie_ids), Purchases.user_id == user_id) +# ).delete() +# +# db.commit() +# +# return {"message": "Your order was refunded successfully."} +# +# except stripe.error.StripeError as e: +# handle_stripe_error(e) +# +# except SQLAlchemyError: +# db.rollback() +# raise HTTPException( +# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, +# detail="Database error while processing the refund." +# ) + + + + + + + + + +""" +---------- DEPRECATED --------- +6. View All Orders with Filters (only for moderator & admin) +Endpoint: GET /orders/ +Description: Allows admins to view all user orders with filters for users, dates, and statuses. +""" +# @router.get("/orders/", response_model=list[OrderItemResponseSchema]) +# def get_all_orders(data: AllOrdersAdminRequestSchema, db: Session = Depends(get_db)): +# query = db.query(Order) +# user_id, date_from, date_to, status_filter = data.user_id, data.date_from, data.date_to, data.status_filter +# +# if user_id: +# query = query.filter(Order.user_id == user_id) +# if date_from: +# query = query.filter(Order.created_at >= date_from) +# if date_to: +# query = query.filter(Order.created_at <= date_to) +# if status_filter: +# query = query.filter(Order.status == status_filter) +# +# orders = query.order_by(Order.created_at.desc()).all() +# if not orders: +# raise HTTPException(status_code=404, detail="No orders found.") +# return orders diff --git a/src/schemas/orders.py b/src/schemas/orders.py index 1402d52..27918a0 100644 --- a/src/schemas/orders.py +++ b/src/schemas/orders.py @@ -8,6 +8,24 @@ """ Message... """ class MessageResponseSchema(BaseModel): message: str +# +# +# """ Cart schems (delete) """ +# class CartItemDetail(BaseModel): +# movie_id: int +# title: str +# price: float +# genre: str | None +# release_year: int +# +# +# class CartResponse(BaseModel): +# user_id: int +# movies: list[CartItemDetail] +# +# +# class PlaceOrderRequestSchema(BaseModel): +# ... """ 2. View User Orders """ @@ -29,3 +47,21 @@ class OrderItemResponseSchema(BaseModel): model_config = { "from_attributes": True } + + +# class AllOrdersRequestSchema(BaseModel): +# user_id: int +# +# +# class AllOrdersUserRequestSchema(AllOrdersRequestSchema): +# pass +# +# +# class AllOrdersAdminRequestSchema(AllOrdersRequestSchema): +# date_from: datetime +# date_to: datetime +# status_filter: OrderStatusEnum +# +# model_config = { +# "from_attributes": True +# } \ No newline at end of file From 8303a04c2faf0d4e83f804f563e810451d005243 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:21:40 +0200 Subject: [PATCH 07/12] Fix order models --- src/database/models/orders.py | 348 +++++----------------------------- 1 file changed, 51 insertions(+), 297 deletions(-) diff --git a/src/database/models/orders.py b/src/database/models/orders.py index b3a8900..e71d7ab 100644 --- a/src/database/models/orders.py +++ b/src/database/models/orders.py @@ -1,312 +1,66 @@ from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session, joinedload +from decimal import Decimal +from enum import Enum + +from sqlalchemy import DECIMAL, DateTime, ForeignKey, Integer, func, String +from sqlalchemy import Enum as SQLAlchemyEnum +from sqlalchemy.orm import Mapped, mapped_column, relationship -from database import get_db, Cart from database.models.accounts import User -from database.models.orders import Order, OrderItem, OrderStatusEnum -from routes.profiles import get_current_user -from schemas.orders import OrderItemResponseSchema, MovieItemResponse, MessageResponseSchema -router = APIRouter() -@router.get("/") -def root() -> dict: - return {"message": "Hello Test"} +from database.models.base import Base +from database.models.movies import Movie +from database.models.payments import Payment, PaymentItem -# Зробити detail view, створення order, і також можна update і delete; додати пагінацію до list, -# Написати тести і свагер +class OrderStatusEnum(str, Enum): + PENDING = "pending" + PAID = "paid" + CANCELED = "canceled" -""" -1. Place an Order -Endpoint: POST /orders/ -Description: Allows users to place an order for movies in their cart. -""" -@router.post("/orders/") -def place_order(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): - cart = db.query(Cart).filter(Cart.user_id == current_user.id).first() - if not cart or not cart.items: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Cart is empty.") +class Order(Base): + __tablename__ = "orders" - order = Order( - user_id=current_user.id, - total_amount=sum(item.movie.price for item in cart.items) + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True, nullable=False ) - # order create - db.add(order) - db.commit() - db.refresh(order) - - # order item create - for cart_item in cart.items: - order_item = OrderItem( - order_id=order.id, - movie_id=cart_item.movie_id, - price_at_order=cart_item.movie.price - ) - db.add(order_item) - db.delete(cart_item) - - db.commit() - - return MessageResponseSchema( - message=f"Order placed successfully, your order_id: {order.id}" + user_id: Mapped[int] = mapped_column( + Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + status: Mapped[OrderStatusEnum] = mapped_column( + SQLAlchemyEnum(OrderStatusEnum), nullable=False, default="PENDING" + ) + total_amount: Mapped[Decimal | None] = mapped_column(DECIMAL(10, 2)) + stripe_url: Mapped[str | None] = mapped_column(String) - -""" -2. View User Orders (depending on whether it is an admin or a user...) -Endpoint: GET /orders/ -Description: Retrieves a list of all orders placed by a specific user. -""" -@router.get("/orders/", response_model=list[OrderItemResponseSchema], status_code=status.HTTP_200_OK) -def get_user_orders( - user_id: int | None = Query(default=None, description="Filter orders by user ID"), - date_from: datetime | None = Query(default=None, description="Filter orders from this date"), - date_to: datetime | None = Query(default=None, description="Filter orders until this date"), - order_status: OrderStatusEnum | None = Query(default=None, description="Filter orders by status"), - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - if current_user.group.name != "user": - filters = [] - if user_id: - filters.append(Order.user_id == user_id) - if date_from: - filters.append(Order.created_at >= date_from) - if date_to: - filters.append(Order.created_at <= date_to) - if order_status: - filters.append(Order.status == order_status) - orders = db.query(Order).filter(*filters) - else: - if user_id or date_from or date_to or order_status: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="You don't have permission." - ) - orders = db.query(Order).filter(Order.user_id == current_user.id) - orders = orders.options(joinedload(Order.order_items).joinedload(OrderItem.movie)) - orders = orders.order_by(Order.created_at.desc()).all() - if not orders: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No orders found.") - order_list = [ - OrderItemResponseSchema( - created_at=order.created_at, - movies=[ - MovieItemResponse( - id=item.movie.id, name=item.movie.name - ) for item in order.order_items - ], - total_amount=order.total_amount, - status=order.status - ) - for order in orders - ] - return order_list - - -""" -3. Detail view of order -Endpoint: GET /orders/{order_id}/ -Description: Allows user to view details about order. -""" - - -""" -4. Update order -Endpoint: PATCH /orders/{order_id}/ -Description: Allows user to update order. -""" - - -""" -4. Delete order -Endpoint: DELETE /orders/{order_id}/ -Description: Allows user to delete order. -""" - - - - - - - - - - - - - - - - - - - - - -# """ -# 3. Confirm an Order -# Endpoint: POST /orders/{order_id}/confirm/ -# Description: Confirms an order and redirects the user to a payment gateway. -# """ -# @router.post("/orders/{order_id}/confirm/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) -# def confirm_order(order_id: int, db: Session = Depends(get_db)): -# order = db.query(Order).filter(Order.id == order_id).first() -# if not order: -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") -# -# if order.status != OrderStatusEnum.PENDING.value: -# raise HTTPException( -# status_code=status.HTTP_400_BAD_REQUEST, -# detail="Only pending orders can be confirmed." -# ) -# -# # ToDo - redirect to payment... -# -# order.status = OrderStatusEnum.PAID.value -# db.commit() -# db.refresh(order) -# -# return MessageResponseSchema( -# message="Order successfully confirmed." -# ) -# -# -# """ -# 4. Cancel an Order -# Endpoint: POST /orders/{order_id}/cancel/ -# Description: Allows users to cancel an order before payment is completed. -# """ -# @router.post("/orders/{order_id}/cancel/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) -# def cancel_order(order_id: int, db: Session = Depends(get_db)): -# order = db.query(Order).filter(Order.id == order_id).first() -# if not order: -# raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") -# -# if order.status != OrderStatusEnum.PENDING.value: -# raise HTTPException( -# status_code=status.HTTP_400_BAD_REQUEST, -# detail="Only pending orders can be canceled." -# ) -# -# order.status = OrderStatusEnum.CANCELED.value -# db.commit() -# db.refresh(order) -# -# return MessageResponseSchema( -# message="Order successfully canceled." -# ) -# -# -# """ -# 5. Request a Refund -# Endpoint: POST /orders/{order_id}/refund_request/ -# Description: Allows users to request a refund for a paid order. -# """ -# # @router.post("/orders/{order_id}/refund_request/", response_model=OrderRefundRequestSchema) -# # def request_refund(order_id: int, db: Session = Depends(get_db)): -# # pass - - -# @router.post("/refund", status_code=status.HTTP_200_OK) -# def refund_order( -# order_id: int, -# db: Session = Depends(get_db), -# token: str = Depends(get_token), -# jwt_manager: JWTAuthManagerInterface = Depends(get_jwt_auth_manager) -# ): -# try: -# payload = jwt_manager.decode_access_token(token) -# user_id = payload.get("user_id") -# except BaseSecurityError as e: -# raise HTTPException( -# status_code=status.HTTP_401_UNAUTHORIZED, -# detail=str(e) -# ) -# -# # Отримуємо замовлення та платіж -# order = db.query(OrderModel).filter_by(id=order_id).first() -# payment = db.query(PaymentModel).filter_by(order_id=order_id).first() -# -# # Перевірка, чи належить це замовлення користувачу і чи не було воно вже повернене -# if order.user_id != user_id or payment.status == "refunded": -# raise HTTPException( -# status_code=status.HTTP_400_BAD_REQUEST, -# detail="Order is either not yours or has already been refunded." -# ) -# -# # Перевірка статусу платежу через Stripe Checkout -# try: -# session = stripe.checkout.Session.retrieve(payment.external_payment_id) -# -# # Якщо сесія не успішна, рефандити не можна -# if session.payment_status != "paid": -# raise HTTPException( -# status_code=status.HTTP_400_BAD_REQUEST, -# detail="Only successful payments can be refunded." -# ) -# -# # Отримуємо PaymentIntent з сесії Stripe -# payment_intent_id = session.payment_intent -# -# # Здійснюємо рефанд через Stripe API -# stripe.Refund.create(payment_intent=payment_intent_id) -# -# # Оновлюємо статус замовлення і платежу -# order.status = "canceled" -# payment.status = "refunded" -# -# # Видаляємо покупку (якщо є) -# movie_ids = [order_item.movie_id for order_item in order.order_items] -# db.query(Purchases).filter( -# and_(Purchases.movie_id.in_(movie_ids), Purchases.user_id == user_id) -# ).delete() -# -# db.commit() -# -# return {"message": "Your order was refunded successfully."} -# -# except stripe.error.StripeError as e: -# handle_stripe_error(e) -# -# except SQLAlchemyError: -# db.rollback() -# raise HTTPException( -# status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, -# detail="Database error while processing the refund." -# ) - - - - - + order_items: Mapped[list["OrderItem"]] = relationship( + "OrderItem", back_populates="order" + ) + user: Mapped["User"] = relationship("User", back_populates="orders") + payments: Mapped[list["Payment"]] = relationship( + "Payment", back_populates="order", cascade="all, delete-orphan" + ) +class OrderItem(Base): + __tablename__ = "order_items" + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True, nullable=False + ) + order_id: Mapped[int] = mapped_column( + Integer, ForeignKey("orders.id", ondelete="CASCADE"), nullable=False + ) + movie_id: Mapped[int] = mapped_column( + Integer, ForeignKey("movies.id", ondelete="CASCADE"), nullable=False + ) + price_at_order: Mapped[float] = mapped_column(DECIMAL(10, 2), nullable=False) -""" ----------- DEPRECATED --------- -6. View All Orders with Filters (only for moderator & admin) -Endpoint: GET /orders/ -Description: Allows admins to view all user orders with filters for users, dates, and statuses. -""" -# @router.get("/orders/", response_model=list[OrderItemResponseSchema]) -# def get_all_orders(data: AllOrdersAdminRequestSchema, db: Session = Depends(get_db)): -# query = db.query(Order) -# user_id, date_from, date_to, status_filter = data.user_id, data.date_from, data.date_to, data.status_filter -# -# if user_id: -# query = query.filter(Order.user_id == user_id) -# if date_from: -# query = query.filter(Order.created_at >= date_from) -# if date_to: -# query = query.filter(Order.created_at <= date_to) -# if status_filter: -# query = query.filter(Order.status == status_filter) -# -# orders = query.order_by(Order.created_at.desc()).all() -# if not orders: -# raise HTTPException(status_code=404, detail="No orders found.") -# return orders + order: Mapped["Order"] = relationship("Order", back_populates="order_items") + movie: Mapped["Movie"] = relationship("Movie", back_populates="order_items") + payment_items: Mapped[list["PaymentItem"]] = relationship( + "PaymentItem", back_populates="order_item", cascade="all, delete-orphan" + ) From f48c62b241bb2af9f5c9a4bf2b5c84fe5b6bfe76 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:30:34 +0200 Subject: [PATCH 08/12] Separate routes and crud; add load_dotenv to setting; --- src/config/settings.py | 3 + src/database/crud/orders.py | 101 ++++++++++++++++++ src/database/models/__init__.py | 4 + src/routes/orders.py | 176 ++++++++++++-------------------- src/schemas/__init__.py | 7 ++ src/schemas/orders.py | 54 +--------- 6 files changed, 183 insertions(+), 162 deletions(-) create mode 100644 src/database/crud/orders.py diff --git a/src/config/settings.py b/src/config/settings.py index c89e386..8c5d5ef 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -2,8 +2,11 @@ from pathlib import Path from typing import Any +from dotenv import load_dotenv from pydantic_settings import BaseSettings +load_dotenv() + class BaseAppSettings(BaseSettings): BASE_DIR: Path = Path(__file__).parent.parent diff --git a/src/database/crud/orders.py b/src/database/crud/orders.py new file mode 100644 index 0000000..32477fc --- /dev/null +++ b/src/database/crud/orders.py @@ -0,0 +1,101 @@ +from datetime import datetime + +from fastapi import HTTPException, status +from sqlalchemy.orm import Session, joinedload + +from database import Order, OrderItem, Cart, User +from schemas import OrderItemResponseSchema, MovieListItemSchema + + +def create_order(user_id: int, db: Session) -> Order: + """Creates a new order for the current user.""" + cart = db.query(Cart).filter(Cart.user_id == user_id).first() + if not cart or not cart.items: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, detail="Cart is empty." + ) + + order = Order( + user_id=user_id, total_amount=sum(item.movie.price for item in cart.items) + ) + db.add(order) + db.commit() + db.refresh(order) + + for cart_item in cart.items: + order_item = OrderItem( + order_id=order.id, + movie_id=cart_item.movie_id, + price_at_order=cart_item.movie.price, + ) + db.add(order_item) + db.delete(cart_item) + + db.commit() + return order + + +def update_order_with_stripe_url(order: Order, stripe_url: str, db: Session) -> None: + """Updates the order with a Stripe URL.""" + order.stripe_url = stripe_url + db.commit() + + +def get_user_orders( + current_user: User, + db: Session, + user_id: int | None = None, + date_from: datetime | None = None, + date_to: datetime | None = None, + order_status: str | None = None, +) -> list[OrderItemResponseSchema]: + """Retrieves orders for a specific user or all users for admin.""" + if current_user.group.name != "user": + filters = [] + if user_id: + filters.append(Order.user_id == user_id) + if date_from: + filters.append(Order.created_at >= date_from) + if date_to: + filters.append(Order.created_at <= date_to) + if order_status: + filters.append(Order.status == order_status) + orders_query = db.query(Order).filter(*filters) + else: + if user_id or date_from or date_to or order_status: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission.", + ) + orders_query = db.query(Order).filter(Order.user_id == current_user.id) + + orders = orders_query.options( + joinedload(Order.order_items).joinedload(OrderItem.movie) + ) + orders = orders.order_by(Order.created_at.desc()).all() + + if not orders: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="No orders found." + ) + + order_list = [ + OrderItemResponseSchema( + created_at=order.created_at, + movies=[ + MovieListItemSchema( + id=item.movie.id, + name=item.movie.name, + year=item.movie.year, + time=item.movie.time, + description=item.movie.description, + ) + for item in order.order_items + ], + total_amount=order.total_amount, + status=order.status, + ) + for order in orders + ] + + return order_list diff --git a/src/database/models/__init__.py b/src/database/models/__init__.py index e69de29..01fd759 100644 --- a/src/database/models/__init__.py +++ b/src/database/models/__init__.py @@ -0,0 +1,4 @@ +from .accounts import User +from .orders import Order, OrderItem +from .movies import Movie +from .shopping_cart import Cart, CartItem diff --git a/src/routes/orders.py b/src/routes/orders.py index 3a02a7e..4a3de2f 100644 --- a/src/routes/orders.py +++ b/src/routes/orders.py @@ -1,30 +1,46 @@ from datetime import datetime -from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session, joinedload +from fastapi import APIRouter, Request, Depends, status, Query +from sqlalchemy.orm import Session from database import get_db +from database.crud.orders import ( + create_order, + update_order_with_stripe_url, + get_user_orders, +) from database.models.accounts import User -from database.models.orders import Order, OrderItem, OrderStatusEnum +from database.models.orders import OrderStatusEnum from routes.profiles import get_current_user -from schemas.orders import OrderItemResponseSchema, MovieItemResponse, MessageResponseSchema +from schemas.orders import OrderItemResponseSchema, MessageResponseSchema +from services import create_checkout_session router = APIRouter() -@router.get("/") -def root() -> dict: - return {"message": "Hello Test"} - - """ 1. Place an Order Endpoint: POST /orders/ Description: Allows users to place an order for movies in their cart. """ -# @router.post("/orders/") -# def place_order(user_id: int, db: Session = Depends(get_db)): -# pass + + +@router.post("/orders/") +def place_order( + request: Request, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +) -> MessageResponseSchema: + order = create_order(user_id=current_user.id, db=db) + + stripe_url = create_checkout_session( + request=request, order=order, user_id=current_user.id, db=db + ) + update_order_with_stripe_url(order, stripe_url, db=db) + + return MessageResponseSchema( + message=f"Order placed successfully, your order_id: {order.id}" + ) """ @@ -32,116 +48,54 @@ def root() -> dict: Endpoint: GET /orders/ Description: Retrieves a list of all orders placed by a specific user. """ -@router.get("/orders/", response_model=list[OrderItemResponseSchema], status_code=status.HTTP_200_OK) -def get_user_orders( + + +@router.get( + "/orders/", + response_model=list[OrderItemResponseSchema], + status_code=status.HTTP_200_OK, +) +def get_user_orders_route( user_id: int | None = Query(default=None, description="Filter orders by user ID"), - date_from: datetime | None = Query(default=None, description="Filter orders from this date"), - date_to: datetime | None = Query(default=None, description="Filter orders until this date"), - order_status: OrderStatusEnum | None = Query(default=None, description="Filter orders by status"), + date_from: datetime | None = Query( + default=None, description="Filter orders from this date" + ), + date_to: datetime | None = Query( + default=None, description="Filter orders until this date" + ), + order_status: OrderStatusEnum | None = Query( + default=None, description="Filter orders by status" + ), current_user: User = Depends(get_current_user), - db: Session = Depends(get_db) -): - if current_user.group.name != "user": - filters = [] - if user_id: - filters.append(Order.user_id == user_id) - if date_from: - filters.append(Order.created_at >= date_from) - if date_to: - filters.append(Order.created_at <= date_to) - if order_status: - filters.append(Order.status == order_status) - orders = db.query(Order).filter(*filters) - else: - if user_id or date_from or date_to or order_status: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="You don't have permission." - ) - orders = db.query(Order).filter(Order.user_id == current_user.id) - - orders = orders.options(joinedload(Order.order_items).joinedload(OrderItem.movie)) - orders = orders.order_by(Order.created_at.desc()).all() - - if not orders: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No orders found.") - - order_list = [ - OrderItemResponseSchema( - created_at=order.created_at, - movies=[ # ToDo - change it in future (can be N+1) - MovieItemResponse( - id=item.movie.id, name=item.movie.name - ) for item in order.order_items - ], - total_amount=order.total_amount, - status=order.status - ) - for order in orders - ] - - return order_list + db: Session = Depends(get_db), +) -> list[OrderItemResponseSchema]: + orders: list[OrderItemResponseSchema] = get_user_orders( + current_user=current_user, + db=db, + user_id=user_id, + date_from=date_from, + date_to=date_to, + order_status=order_status, + ) + return orders """ -3. Confirm an Order -Endpoint: POST /orders/{order_id}/confirm/ -Description: Confirms an order and redirects the user to a payment gateway. +3. Detail view of order +Endpoint: GET /orders/{order_id}/ +Description: Allows user to view details about order. """ -@router.post("/orders/{order_id}/confirm/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) -def confirm_order(order_id: int, db: Session = Depends(get_db)): - order = db.query(Order).filter(Order.id == order_id).first() - if not order: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") - - if order.status != OrderStatusEnum.PENDING.value: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Only pending orders can be confirmed." - ) - - # ToDo - redirect to payment... - - order.status = OrderStatusEnum.PAID.value - db.commit() - db.refresh(order) - - return MessageResponseSchema( - message="Order successfully confirmed." - ) """ -4. Cancel an Order -Endpoint: POST /orders/{order_id}/cancel/ -Description: Allows users to cancel an order before payment is completed. +4. Update order +Endpoint: PATCH /orders/{order_id}/ +Description: Allows user to update order. """ -@router.post("/orders/{order_id}/cancel/", response_model=MessageResponseSchema, status_code=status.HTTP_200_OK) -def cancel_order(order_id: int, db: Session = Depends(get_db)): - order = db.query(Order).filter(Order.id == order_id).first() - if not order: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found.") - - if order.status != OrderStatusEnum.PENDING.value: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Only pending orders can be canceled." - ) - - order.status = OrderStatusEnum.CANCELED.value - db.commit() - db.refresh(order) - - return MessageResponseSchema( - message="Order successfully canceled." - ) """ -5. Request a Refund -Endpoint: POST /orders/{order_id}/refund_request/ -Description: Allows users to request a refund for a paid order. +4. Delete order +Endpoint: DELETE /orders/{order_id}/ +Description: Allows user to delete order. """ -# @router.post("/orders/{order_id}/refund_request/", response_model=OrderRefundRequestSchema) -# def request_refund(order_id: int, db: Session = Depends(get_db)): -# pass diff --git a/src/schemas/__init__.py b/src/schemas/__init__.py index 836c11c..b6b79c4 100644 --- a/src/schemas/__init__.py +++ b/src/schemas/__init__.py @@ -3,3 +3,10 @@ PaymentHistoryResponse, PaymentSchema, ) +from schemas.orders import ( + MessageResponseSchema, + OrderItemResponseSchema, +) +from schemas.movies import ( + MovieListItemSchema, +) diff --git a/src/schemas/orders.py b/src/schemas/orders.py index 27918a0..cae26e6 100644 --- a/src/schemas/orders.py +++ b/src/schemas/orders.py @@ -3,65 +3,17 @@ from pydantic import BaseModel from database.models.orders import OrderStatusEnum +from schemas.movies import MovieListItemSchema -""" Message... """ class MessageResponseSchema(BaseModel): message: str -# -# -# """ Cart schems (delete) """ -# class CartItemDetail(BaseModel): -# movie_id: int -# title: str -# price: float -# genre: str | None -# release_year: int -# -# -# class CartResponse(BaseModel): -# user_id: int -# movies: list[CartItemDetail] -# -# -# class PlaceOrderRequestSchema(BaseModel): -# ... - - -""" 2. View User Orders """ -class MovieItemResponse(BaseModel): # ToDo Test delete in future because it must be from movies schemas - id: int - name: str - - model_config = { - "from_attributes": True - } class OrderItemResponseSchema(BaseModel): created_at: datetime - movies: list[MovieItemResponse] # ToDo from movies schemas will be... + movies: list[MovieListItemSchema] total_amount: float | None status: OrderStatusEnum - model_config = { - "from_attributes": True - } - - -# class AllOrdersRequestSchema(BaseModel): -# user_id: int -# -# -# class AllOrdersUserRequestSchema(AllOrdersRequestSchema): -# pass -# -# -# class AllOrdersAdminRequestSchema(AllOrdersRequestSchema): -# date_from: datetime -# date_to: datetime -# status_filter: OrderStatusEnum -# -# model_config = { -# "from_attributes": True -# } \ No newline at end of file + model_config = {"from_attributes": True} From 4de21a78a04251f56251259e7d4de6ea67d19761 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:32:33 +0200 Subject: [PATCH 09/12] Update crud for orders using transactions --- src/database/crud/orders.py | 53 ++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/database/crud/orders.py b/src/database/crud/orders.py index 32477fc..e0b5482 100644 --- a/src/database/crud/orders.py +++ b/src/database/crud/orders.py @@ -1,6 +1,7 @@ from datetime import datetime from fastapi import HTTPException, status +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm import Session, joinedload from database import Order, OrderItem, Cart, User @@ -15,30 +16,43 @@ def create_order(user_id: int, db: Session) -> Order: status_code=status.HTTP_400_BAD_REQUEST, detail="Cart is empty." ) - order = Order( - user_id=user_id, total_amount=sum(item.movie.price for item in cart.items) - ) - db.add(order) - db.commit() - db.refresh(order) - - for cart_item in cart.items: - order_item = OrderItem( - order_id=order.id, - movie_id=cart_item.movie_id, - price_at_order=cart_item.movie.price, + try: + order = Order( + user_id=user_id, total_amount=sum(item.movie.price for item in cart.items) ) - db.add(order_item) - db.delete(cart_item) + db.add(order) + db.flush() + + for cart_item in cart.items: + order_item = OrderItem( + order_id=order.id, + movie_id=cart_item.movie_id, + price_at_order=cart_item.movie.price, + ) + db.add(order_item) - db.commit() - return order + db.commit() + db.refresh(order) + return order + except SQLAlchemyError as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create order: {str(e)}" + ) def update_order_with_stripe_url(order: Order, stripe_url: str, db: Session) -> None: """Updates the order with a Stripe URL.""" - order.stripe_url = stripe_url - db.commit() + try: + order.stripe_url = stripe_url + db.commit() + except SQLAlchemyError as e: + db.rollback() + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update order with Stripe URL: {str(e)}" + ) def get_user_orders( @@ -68,17 +82,14 @@ def get_user_orders( detail="You don't have permission.", ) orders_query = db.query(Order).filter(Order.user_id == current_user.id) - orders = orders_query.options( joinedload(Order.order_items).joinedload(OrderItem.movie) ) orders = orders.order_by(Order.created_at.desc()).all() - if not orders: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No orders found." ) - order_list = [ OrderItemResponseSchema( created_at=order.created_at, From 2424b86d77a028c085d3502367d73be54cda8c84 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:34:36 +0200 Subject: [PATCH 10/12] Implement detail order route --- src/database/crud/orders.py | 37 +++++++++++++++++++++++++++++++++++++ src/routes/orders.py | 27 ++++++++++++++------------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/database/crud/orders.py b/src/database/crud/orders.py index e0b5482..a9060cb 100644 --- a/src/database/crud/orders.py +++ b/src/database/crud/orders.py @@ -110,3 +110,40 @@ def get_user_orders( ] return order_list + + +def get_order_by_id(db: Session, order_id: int, current_user_id: int | None = None) -> Order: + """Retrieve an order by ID and check permissions.""" + order = db.query(Order).filter(Order.id == order_id).first() + + if not order: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Order not found." + ) + + if current_user_id and order.user_id != current_user_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="You don't have permission to view this order.", + ) + + return order + + +def format_order_detail(order: Order) -> OrderItemResponseSchema: + """Format the order details for the response.""" + return OrderItemResponseSchema( + created_at=order.created_at, + movies=[ + MovieListItemSchema( + id=item.movie.id, + name=item.movie.name, + year=item.movie.year, + time=item.movie.time, + description=item.movie.description, + ) + for item in order.order_items + ], + total_amount=order.total_amount, + status=order.status, + ) diff --git a/src/routes/orders.py b/src/routes/orders.py index 4a3de2f..9eb4ed8 100644 --- a/src/routes/orders.py +++ b/src/routes/orders.py @@ -8,6 +8,8 @@ create_order, update_order_with_stripe_url, get_user_orders, + format_order_detail, + get_order_by_id, ) from database.models.accounts import User from database.models.orders import OrderStatusEnum @@ -85,17 +87,16 @@ def get_user_orders_route( Endpoint: GET /orders/{order_id}/ Description: Allows user to view details about order. """ +@router.get( + "/orders/{order_id}/", + response_model=OrderItemResponseSchema, + status_code=status.HTTP_200_OK, +) +def get_order_detail( + order_id: int, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +) -> OrderItemResponseSchema: + order = get_order_by_id(db, order_id, current_user.id) - -""" -4. Update order -Endpoint: PATCH /orders/{order_id}/ -Description: Allows user to update order. -""" - - -""" -4. Delete order -Endpoint: DELETE /orders/{order_id}/ -Description: Allows user to delete order. -""" + return format_order_detail(order) From 51893f28a164209c3ed7e478ca706c179772d2d3 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:36:57 +0200 Subject: [PATCH 11/12] ruff fix --- src/database/crud/orders.py | 4 +++- src/main.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/database/crud/orders.py b/src/database/crud/orders.py index a9060cb..5e70d9f 100644 --- a/src/database/crud/orders.py +++ b/src/database/crud/orders.py @@ -112,7 +112,9 @@ def get_user_orders( return order_list -def get_order_by_id(db: Session, order_id: int, current_user_id: int | None = None) -> Order: +def get_order_by_id( + db: Session, order_id: int, current_user_id: int | None = None +) -> Order: """Retrieve an order by ID and check permissions.""" order = db.query(Order).filter(Order.id == order_id).first() diff --git a/src/main.py b/src/main.py index 415601b..af2efa6 100644 --- a/src/main.py +++ b/src/main.py @@ -1,7 +1,9 @@ from fastapi import FastAPI from fastapi_pagination import add_pagination -from routes import accounts_router, movies_router, payments_router, profiles_router, orders_router +from routes import ( + accounts_router, movies_router, payments_router, profiles_router, orders_router +) from routes.shopping_cart import router as shopping_carts_router app = FastAPI( From d7256e8f9322032645e360e47178ea52f7879856 Mon Sep 17 00:00:00 2001 From: Den_k0 Date: Tue, 4 Feb 2025 00:43:36 +0200 Subject: [PATCH 12/12] edit --- src/database/crud/orders.py | 3 +++ src/routes/orders.py | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/database/crud/orders.py b/src/database/crud/orders.py index 5e70d9f..98b8918 100644 --- a/src/database/crud/orders.py +++ b/src/database/crud/orders.py @@ -82,14 +82,17 @@ def get_user_orders( detail="You don't have permission.", ) orders_query = db.query(Order).filter(Order.user_id == current_user.id) + orders = orders_query.options( joinedload(Order.order_items).joinedload(OrderItem.movie) ) orders = orders.order_by(Order.created_at.desc()).all() + if not orders: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="No orders found." ) + order_list = [ OrderItemResponseSchema( created_at=order.created_at, diff --git a/src/routes/orders.py b/src/routes/orders.py index 9eb4ed8..c4b537b 100644 --- a/src/routes/orders.py +++ b/src/routes/orders.py @@ -25,8 +25,6 @@ Endpoint: POST /orders/ Description: Allows users to place an order for movies in their cart. """ - - @router.post("/orders/") def place_order( request: Request, @@ -50,8 +48,6 @@ def place_order( Endpoint: GET /orders/ Description: Retrieves a list of all orders placed by a specific user. """ - - @router.get( "/orders/", response_model=list[OrderItemResponseSchema],