diff --git a/apps/api/backend/api/routes/coupons.py b/apps/api/backend/api/routes/coupons.py new file mode 100644 index 0000000..b32f307 --- /dev/null +++ b/apps/api/backend/api/routes/coupons.py @@ -0,0 +1,18 @@ +from uuid import UUID + +from fastapi import APIRouter, status + +from backend.schemas.coupon import UserCouponWalletResponse +from backend.services.coupon_service import list_user_coupons + +router = APIRouter(prefix="/users", tags=["coupons"]) + + +@router.get( + "/{user_id}/coupons", + response_model=UserCouponWalletResponse, + status_code=status.HTTP_200_OK, +) +def get_user_coupons(user_id: UUID) -> UserCouponWalletResponse: + result = list_user_coupons(user_id) + return UserCouponWalletResponse(**result) \ No newline at end of file diff --git a/apps/api/backend/main.py b/apps/api/backend/main.py index 00e6cf5..a9273d8 100644 --- a/apps/api/backend/main.py +++ b/apps/api/backend/main.py @@ -7,6 +7,7 @@ from backend.api.routes.categories import router as categories_router from backend.api.routes.checkout import router as checkout_router from backend.api.routes.coupon_apply import router as coupon_apply_router +from backend.api.routes.coupons import router as coupons_router from backend.api.routes.health import router as health_router from backend.api.routes.order_history import router as order_history_router from backend.api.routes.orders import router as orders_router @@ -39,6 +40,7 @@ app.include_router(cart_items_router) app.include_router(checkout_router) app.include_router(coupon_apply_router) +app.include_router(coupons_router) app.include_router(orders_router) app.include_router(order_history_router) app.include_router(payments_router) diff --git a/apps/api/backend/schemas/coupon.py b/apps/api/backend/schemas/coupon.py new file mode 100644 index 0000000..b7e7387 --- /dev/null +++ b/apps/api/backend/schemas/coupon.py @@ -0,0 +1,38 @@ +from datetime import datetime +from decimal import Decimal +from uuid import UUID + +from pydantic import BaseModel + + +class UserAvailableCouponResponse(BaseModel): + coupon_id: UUID + campaign_id: UUID | None = None + coupon_name: str + coupon_type: str + discount_value: Decimal + minimum_order_amount: Decimal + coupon_status: str + valid_start_at: datetime + valid_end_at: datetime + + +class UserUsedCouponResponse(BaseModel): + coupon_id: UUID + campaign_id: UUID | None = None + coupon_name: str + coupon_type: str + discount_value: Decimal + minimum_order_amount: Decimal + coupon_status: str + valid_start_at: datetime + valid_end_at: datetime + used_order_id: UUID + used_at: datetime + payment_id: UUID | None = None + + +class UserCouponWalletResponse(BaseModel): + user_id: UUID + available_coupons: list[UserAvailableCouponResponse] + used_coupons: list[UserUsedCouponResponse] \ No newline at end of file diff --git a/apps/api/backend/services/coupon_service.py b/apps/api/backend/services/coupon_service.py index 6c8c25d..57c23e9 100644 --- a/apps/api/backend/services/coupon_service.py +++ b/apps/api/backend/services/coupon_service.py @@ -154,4 +154,101 @@ def apply_coupon_to_cart(cart_id: UUID, payload: CouponApplyRequest) -> dict[str "final_amount": final_amount, "currency": currency, "message": "Coupon applied successfully", + } + +def list_user_coupons(user_id: UUID) -> dict[str, Any]: + user_query = text(""" + SELECT + user_id + FROM users + WHERE user_id = :user_id + AND user_status = 'active' + LIMIT 1 + """) + + available_coupons_query = text(""" + SELECT + c.coupon_id, + c.campaign_id, + c.coupon_name, + c.coupon_type, + c.discount_value, + c.minimum_order_amount, + c.coupon_status, + c.valid_start_at, + c.valid_end_at + FROM coupons c + WHERE c.coupon_status = 'active' + AND c.valid_start_at <= CURRENT_TIMESTAMP + AND c.valid_end_at >= CURRENT_TIMESTAMP + AND NOT EXISTS ( + SELECT 1 + FROM orders o + WHERE o.user_id = :user_id + AND o.coupon_id = c.coupon_id + AND o.order_status = 'paid' + ) + ORDER BY c.valid_end_at ASC, c.coupon_name ASC + """) + + used_coupons_query = text(""" + SELECT + c.coupon_id, + c.campaign_id, + c.coupon_name, + c.coupon_type, + c.discount_value, + c.minimum_order_amount, + c.coupon_status, + c.valid_start_at, + c.valid_end_at, + o.order_id AS used_order_id, + COALESCE(p.paid_at, p.created_at, o.ordered_at) AS used_at, + p.payment_id + FROM orders o + JOIN coupons c + ON c.coupon_id = o.coupon_id + LEFT JOIN LATERAL ( + SELECT + payment_id, + paid_at, + created_at + FROM payments + WHERE order_id = o.order_id + AND payment_status = 'paid' + ORDER BY COALESCE(paid_at, created_at) DESC + LIMIT 1 + ) p ON TRUE + WHERE o.user_id = :user_id + AND o.order_status = 'paid' + AND o.coupon_id IS NOT NULL + ORDER BY COALESCE(p.paid_at, p.created_at, o.ordered_at) DESC + """) + + with engine.connect() as connection: + user = connection.execute( + user_query, + {"user_id": user_id}, + ).mappings().first() + + if user is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) + + available_coupons = connection.execute( + available_coupons_query, + {"user_id": user_id}, + ).mappings().all() + + used_coupons = connection.execute( + used_coupons_query, + {"user_id": user_id}, + ).mappings().all() + + return { + "user_id": user_id, + "available_coupons": [dict(coupon) for coupon in available_coupons], + "used_coupons": [dict(coupon) for coupon in used_coupons], } \ No newline at end of file diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx index b2ebd05..283ff8d 100644 --- a/apps/web/src/app/router.tsx +++ b/apps/web/src/app/router.tsx @@ -9,6 +9,7 @@ import { CartPage } from "../features/cart/CartPage"; import { CheckoutPage } from "../features/checkout/CheckoutPage"; import { OrderHistoryPage } from "../features/orders/OrderHistoryPage"; import { ReviewCreatePage } from "../features/reviews/ReviewCreatePage"; +import { CouponWalletPage } from "../features/coupons/CouponWalletPage"; export const router = createBrowserRouter([ { @@ -47,6 +48,10 @@ export const router = createBrowserRouter([ path: "orders", element: }, + { + path: "coupons", + element: + }, { path: "reviews/new", element: diff --git a/apps/web/src/components/layout/MainLayout.tsx b/apps/web/src/components/layout/MainLayout.tsx index a02e2d3..0d97749 100644 --- a/apps/web/src/components/layout/MainLayout.tsx +++ b/apps/web/src/components/layout/MainLayout.tsx @@ -43,6 +43,7 @@ export function MainLayout() { {user ? ( <> 주문 내역 + 쿠폰함 {user.user_name}님