Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions apps/api/backend/api/routes/cart_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
from backend.schemas.cart_item import (
CartItemCreateRequest,
CartItemDeleteResponse,
CartItemQuantityUpdateRequest,
CartItemResponse,
)
from backend.services.cart_item_service import add_item_to_cart, remove_item_from_cart
from backend.services.cart_item_service import (
add_item_to_cart,
remove_item_from_cart,
update_cart_item_quantity,
)

router = APIRouter(prefix="/carts", tags=["carts"])

Expand All @@ -29,4 +34,18 @@ def add_cart_item(cart_id: UUID, payload: CartItemCreateRequest) -> CartItemResp
)
def delete_cart_item(cart_id: UUID, cart_item_id: UUID) -> CartItemDeleteResponse:
result = remove_item_from_cart(cart_id, cart_item_id)
return CartItemDeleteResponse(**result)
return CartItemDeleteResponse(**result)


@router.patch(
"/{cart_id}/items/{cart_item_id}",
response_model=CartItemResponse,
status_code=status.HTTP_200_OK,
)
def patch_cart_item_quantity(
cart_id: UUID,
cart_item_id: UUID,
payload: CartItemQuantityUpdateRequest,
) -> CartItemResponse:
item = update_cart_item_quantity(cart_id, cart_item_id, payload)
return CartItemResponse(**item)
6 changes: 3 additions & 3 deletions apps/api/backend/api/routes/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from backend.schemas.checkout import CheckoutResponse
from backend.services.checkout_service import prepare_checkout

router = APIRouter(prefix="/carts", tags=["checkout"])
router = APIRouter(prefix="/checkout", tags=["checkout"])


@router.post(
"/{cart_id}/checkout",
@router.get(
"/{cart_id}",
response_model=CheckoutResponse,
status_code=status.HTTP_200_OK,
)
Expand Down
4 changes: 2 additions & 2 deletions apps/api/backend/api/routes/coupon_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from backend.schemas.coupon_apply import CouponApplyRequest, CouponApplyResponse
from backend.services.coupon_service import apply_coupon_to_cart

router = APIRouter(prefix="/carts", tags=["checkout"])
router = APIRouter(prefix="/carts", tags=["coupons"])


@router.post(
"/{cart_id}/coupon",
"/{cart_id}/apply-coupon",
response_model=CouponApplyResponse,
status_code=status.HTTP_200_OK,
)
Expand Down
1 change: 1 addition & 0 deletions apps/api/backend/schemas/cart.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class CartItemSummaryResponse(BaseModel):
cart_item_id: UUID
product_id: UUID
product_name: str
brand_name: str | None = None
quantity: int
unit_price: Decimal
currency: str
Expand Down
8 changes: 6 additions & 2 deletions apps/api/backend/schemas/cart_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

class CartItemCreateRequest(BaseModel):
product_id: UUID
quantity: int = Field(gt=0, le=100)
quantity: int = Field(ge=1, le=99)


class CartItemResponse(BaseModel):
Expand All @@ -24,4 +24,8 @@ class CartItemResponse(BaseModel):
class CartItemDeleteResponse(BaseModel):
cart_item_id: UUID
cart_id: UUID
message: str
message: str


class CartItemQuantityUpdateRequest(BaseModel):
quantity: int = Field(ge=1, le=99)
1 change: 1 addition & 0 deletions apps/api/backend/schemas/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CheckoutCartItemResponse(BaseModel):
cart_item_id: UUID
product_id: UUID
product_name: str
brand_name: str | None = None
quantity: int
unit_price: Decimal
currency: str
Expand Down
115 changes: 113 additions & 2 deletions apps/api/backend/services/cart_item_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
from sqlalchemy import text

from backend.db.connection import engine
from backend.schemas.cart_item import CartItemCreateRequest
from backend.schemas.cart_item import (
CartItemCreateRequest,
CartItemQuantityUpdateRequest,
)

MAX_CART_ITEM_QUANTITY = 99

def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str, Any]:
cart_query = text("""
Expand Down Expand Up @@ -132,6 +136,14 @@ def add_item_to_cart(cart_id: UUID, payload: CartItemCreateRequest) -> dict[str,
).mappings().first()

if existing_item is not None:
next_quantity = existing_item["quantity"] + payload.quantity

if next_quantity > MAX_CART_ITEM_QUANTITY:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cart item quantity cannot exceed 99",
)

updated_item = connection.execute(
update_item_query,
{
Expand Down Expand Up @@ -235,4 +247,103 @@ def remove_item_from_cart(cart_id: UUID, cart_item_id: UUID) -> dict[str, Any]:
"cart_item_id": deleted_item["cart_item_id"],
"cart_id": deleted_item["cart_id"],
"message": "Cart item removed successfully",
}
}

def update_cart_item_quantity(
cart_id: UUID,
cart_item_id: UUID,
payload: CartItemQuantityUpdateRequest,
) -> dict[str, Any]:
cart_query = text("""
SELECT
cart_id,
cart_status
FROM carts
WHERE cart_id = :cart_id
AND cart_status = 'active'
LIMIT 1
""")

item_query = text("""
SELECT
cart_item_id,
cart_id,
product_id,
unit_price,
currency
FROM cart_items
WHERE cart_id = :cart_id
AND cart_item_id = :cart_item_id
LIMIT 1
""")

update_query = text("""
UPDATE cart_items
SET
quantity = :quantity,
updated_at = CURRENT_TIMESTAMP
WHERE cart_id = :cart_id
AND cart_item_id = :cart_item_id
RETURNING
cart_item_id,
cart_id,
product_id,
quantity,
unit_price,
currency,
added_at,
updated_at
""")

with engine.begin() as connection:
cart = connection.execute(
cart_query,
{"cart_id": cart_id},
).mappings().first()

if cart is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Active cart not found",
)

item = connection.execute(
item_query,
{
"cart_id": cart_id,
"cart_item_id": cart_item_id,
},
).mappings().first()

if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Cart item not found",
)

updated_item = connection.execute(
update_query,
{
"cart_id": cart_id,
"cart_item_id": cart_item_id,
"quantity": payload.quantity,
},
).mappings().first()

if updated_item is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update cart item quantity",
)

return {
"cart_item_id": updated_item["cart_item_id"],
"cart_id": updated_item["cart_id"],
"product_id": updated_item["product_id"],
"quantity": updated_item["quantity"],
"unit_price": updated_item["unit_price"],
"currency": updated_item["currency"],
"added_at": updated_item["added_at"],
"updated_at": updated_item["updated_at"],
"message": "Cart item quantity updated successfully",
}
2 changes: 2 additions & 0 deletions apps/api/backend/services/cart_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def get_cart_detail(cart_id: UUID) -> dict[str, Any]:
ci.cart_item_id,
ci.product_id,
p.product_name,
p.brand_name,
ci.quantity,
ci.unit_price,
ci.currency,
Expand Down Expand Up @@ -154,6 +155,7 @@ def get_cart_detail(cart_id: UUID) -> dict[str, Any]:
"cart_item_id": row["cart_item_id"],
"product_id": row["product_id"],
"product_name": row["product_name"],
"brand_name": row["brand_name"],
"quantity": row["quantity"],
"unit_price": row["unit_price"],
"currency": row["currency"],
Expand Down
2 changes: 2 additions & 0 deletions apps/api/backend/services/checkout_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def prepare_checkout(cart_id: UUID) -> dict[str, Any]:
ci.cart_item_id,
ci.product_id,
p.product_name,
p.brand_name,
ci.quantity,
ci.unit_price,
ci.currency,
Expand Down Expand Up @@ -109,6 +110,7 @@ def prepare_checkout(cart_id: UUID) -> dict[str, Any]:
"cart_item_id": row["cart_item_id"],
"product_id": row["product_id"],
"product_name": row["product_name"],
"brand_name": row["brand_name"],
"quantity": row["quantity"],
"unit_price": row["unit_price"],
"currency": row["currency"],
Expand Down
25 changes: 25 additions & 0 deletions apps/api/backend/services/coupon_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str
cart_query = text("""
SELECT
c.cart_id,
c.user_id,
c.cart_status
FROM carts c
WHERE c.cart_id = :cart_id
Expand Down Expand Up @@ -47,6 +48,16 @@ def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str
LIMIT 1
""")

used_coupon_query = text("""
SELECT
order_id
FROM orders
WHERE user_id = :user_id
AND coupon_id = :coupon_id
AND order_status = 'paid'
LIMIT 1
""")

with engine.connect() as connection:
cart = connection.execute(
cart_query,
Expand Down Expand Up @@ -80,6 +91,20 @@ def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str
status_code=status.HTTP_404_NOT_FOUND,
detail="Coupon not found",
)

used_coupon = connection.execute(
used_coupon_query,
{
"user_id": cart["user_id"],
"coupon_id": coupon["coupon_id"],
},
).mappings().first()

if used_coupon is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Coupon has already been used",
)

total_amount = Decimal(str(total_row["total_amount"]))
currency = total_row["currency"]
Expand Down
38 changes: 24 additions & 14 deletions apps/api/backend/services/order_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ def create_order_from_cart(payload: OrderCreateRequest) -> dict[str, Any]:
LIMIT 1
""")

used_coupon_query = text("""
SELECT
order_id
FROM orders
WHERE user_id = :user_id
AND coupon_id = :coupon_id
AND order_status = 'paid'
LIMIT 1
""")

insert_order_query = text("""
INSERT INTO orders (
user_id,
Expand Down Expand Up @@ -129,15 +139,6 @@ def create_order_from_cart(payload: OrderCreateRequest) -> dict[str, Any]:
currency
""")

update_cart_query = text("""
UPDATE carts
SET
cart_status = 'checked_out',
checked_out_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE cart_id = :cart_id
""")

with engine.begin() as connection:
cart = connection.execute(
cart_query,
Expand Down Expand Up @@ -200,6 +201,20 @@ def create_order_from_cart(payload: OrderCreateRequest) -> dict[str, Any]:
detail="Coupon not found",
)

used_coupon = connection.execute(
used_coupon_query,
{
"user_id": cart["user_id"],
"coupon_id": coupon["coupon_id"],
},
).mappings().first()

if used_coupon is not None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Coupon has already been used",
)

minimum_order_amount = Decimal(str(coupon["minimum_order_amount"]))
discount_value = Decimal(str(coupon["discount_value"]))

Expand Down Expand Up @@ -283,11 +298,6 @@ def create_order_from_cart(payload: OrderCreateRequest) -> dict[str, Any]:
}
)

connection.execute(
update_cart_query,
{"cart_id": payload.cart_id},
)

return {
"order_id": created_order["order_id"],
"user_id": created_order["user_id"],
Expand Down
Loading
Loading