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
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
4 changes: 2 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 @@ -28,4 +28,4 @@ class CartItemDeleteResponse(BaseModel):


class CartItemQuantityUpdateRequest(BaseModel):
quantity: int = Field(ge=1, le=999)
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
9 changes: 9 additions & 0 deletions apps/api/backend/services/cart_item_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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 @@ -135,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
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
17 changes: 17 additions & 0 deletions apps/api/backend/services/payment_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def simulate_payment(payload: PaymentSimulationRequest) -> dict[str, Any]:
order_query = text("""
SELECT
order_id,
cart_id,
order_status,
total_amount,
currency
Expand Down Expand Up @@ -81,6 +82,16 @@ def simulate_payment(payload: PaymentSimulationRequest) -> dict[str, Any]:
WHERE order_id = :order_id
""")

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

with engine.begin() as connection:
order = connection.execute(
order_query,
Expand Down Expand Up @@ -109,10 +120,16 @@ def simulate_payment(payload: PaymentSimulationRequest) -> dict[str, Any]:
paid_at = datetime.now()
pg_provider = "mock_pg"
transaction_id = f"tx-{payload.order_id}"

connection.execute(
update_order_success_query,
{"order_id": payload.order_id},
)

connection.execute(
update_cart_checked_out_query,
{"cart_id": order["cart_id"]},
)
else:
payment_status = "failed"
paid_amount = Decimal("0")
Expand Down
8 changes: 4 additions & 4 deletions apps/api/tests/test_checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_checkout_returns_200_for_valid_cart() -> None:
cart_id = _create_cart(user_id)
_add_item(cart_id, "33333333-3333-3333-3333-000000000001", 2)

response = client.post(f"/carts/{cart_id}/checkout")
response = client.get(f"/checkout/{cart_id}")

assert response.status_code == 200

Expand All @@ -47,7 +47,7 @@ def test_checkout_returns_expected_fields() -> None:
cart_id = _create_cart(user_id)
_add_item(cart_id, "33333333-3333-3333-3333-000000000001", 2)

response = client.post(f"/carts/{cart_id}/checkout")
response = client.get(f"/checkout/{cart_id}")
data = response.json()

assert "cart_id" in data
Expand All @@ -74,14 +74,14 @@ def test_checkout_returns_400_for_empty_cart() -> None:
user_id = _signup_user(f"checkout_empty_{unique_suffix}@example.com")
cart_id = _create_cart(user_id)

response = client.post(f"/carts/{cart_id}/checkout")
response = client.get(f"/checkout/{cart_id}")

assert response.status_code == 400
assert response.json()["detail"] == "Cart is empty"


def test_checkout_returns_404_for_missing_cart() -> None:
response = client.post("/carts/99999999-9999-9999-9999-999999999999/checkout")
response = client.get("/checkout/99999999-9999-9999-9999-999999999999")

assert response.status_code == 404
assert response.json()["detail"] == "Active cart not found"
8 changes: 4 additions & 4 deletions apps/api/tests/test_coupon_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def test_apply_coupon_returns_200_for_valid_coupon() -> None:
_add_item(cart_id, "33333333-3333-3333-3333-000000000001", 2)

response = client.post(
f"/carts/{cart_id}/coupon",
f"/carts/{cart_id}/apply-coupon",
json={"coupon_name": "WELCOME10"},
)

Expand All @@ -51,7 +51,7 @@ def test_apply_coupon_returns_expected_fields() -> None:
_add_item(cart_id, "33333333-3333-3333-3333-000000000001", 2)

response = client.post(
f"/carts/{cart_id}/coupon",
f"/carts/{cart_id}/apply-coupon",
json={"coupon_name": "WELCOME10"},
)
data = response.json()
Expand All @@ -75,7 +75,7 @@ def test_apply_coupon_returns_404_for_missing_coupon() -> None:
_add_item(cart_id, "33333333-3333-3333-3333-000000000001", 2)

response = client.post(
f"/carts/{cart_id}/coupon",
f"/carts/{cart_id}/apply-coupon",
json={"coupon_name": "NOT_EXIST_COUPON"},
)

Expand All @@ -89,7 +89,7 @@ def test_apply_coupon_returns_400_for_empty_cart() -> None:
cart_id = _create_cart(user_id)

response = client.post(
f"/carts/{cart_id}/coupon",
f"/carts/{cart_id}/apply-coupon",
json={"coupon_name": "WELCOME10"},
)

Expand Down
12 changes: 9 additions & 3 deletions apps/web/src/features/cart/CartPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import {
removeCartItem,
updateCartItemQuantity,
} from "../../services/cartApi";
import { getStoredCartId, getStoredUser } from "../../stores/userStore";
import {
clearStoredCartId,
getStoredCartId,
getStoredUser,
} from "../../stores/userStore";
import type { CartDetail, CartItem } from "../../types/cart";

type CartItemWithTotals = CartItem & {
Expand Down Expand Up @@ -105,7 +109,9 @@ export function CartPage() {
const cartData = await getCart(storedCartId);
setCart(cartData);
} catch {
setErrorMessage("장바구니 정보를 불러오지 못했습니다.");
clearStoredCartId();
setCart(null);
setErrorMessage(null);
} finally {
setIsLoading(false);
}
Expand Down Expand Up @@ -326,7 +332,7 @@ export function CartPage() {
key={`${item.cart_item_id}-${item.quantity}`}
type="number"
min="1"
max="999"
max="99"
defaultValue={item.quantity}
disabled={updatingItemId === item.cart_item_id}
onBlur={(event) => {
Expand Down
Loading
Loading