diff --git a/frontend/src/components/pages/Orders.tsx b/frontend/src/components/pages/Orders.tsx index 0a51f493..4aeba6d7 100644 --- a/frontend/src/components/pages/Orders.tsx +++ b/frontend/src/components/pages/Orders.tsx @@ -22,9 +22,7 @@ const OrdersPage: React.FC = () => { if (isAuthenticated && currentUser) { setLoadingOrders(true); try { - const { data, error } = await api.client.GET("/orders/me", { - params: { query: { include_products: true } }, - }); + const { data, error } = await api.client.GET("/orders/me"); if (error) { const apiError = error as ApiError; diff --git a/frontend/src/components/stripe/CheckoutButton.tsx b/frontend/src/components/stripe/CheckoutButton.tsx index d680a1b7..f1191941 100644 --- a/frontend/src/components/stripe/CheckoutButton.tsx +++ b/frontend/src/components/stripe/CheckoutButton.tsx @@ -59,7 +59,7 @@ const CheckoutButton: React.FC = ({ try { const { data, error } = await auth.client.POST( - "/stripe/create-checkout-session", + "/stripe/checkout-session", { body: { listing_id: listingId, diff --git a/frontend/src/gen/api.ts b/frontend/src/gen/api.ts index d35074df..95790bfe 100644 --- a/frontend/src/gen/api.ts +++ b/frontend/src/gen/api.ts @@ -900,7 +900,7 @@ export interface paths { patch?: never; trace?: never; }; - "/stripe/create-checkout-session": { + "/stripe/checkout-session": { parameters: { query?: never; header?: never; @@ -910,7 +910,7 @@ export interface paths { get?: never; put?: never; /** Create Checkout Session */ - post: operations["create_checkout_session_stripe_create_checkout_session_post"]; + post: operations["create_checkout_session_stripe_checkout_session_post"]; delete?: never; options?: never; head?: never; @@ -1354,6 +1354,15 @@ export interface components { /** Order Id */ order_id?: string | null; }; + /** DeleteTestAccountsResponse */ + DeleteTestAccountsResponse: { + /** Success */ + success: boolean; + /** Deleted Accounts */ + deleted_accounts: string[]; + /** Count */ + count: number; + }; /** DeleteTokenResponse */ DeleteTokenResponse: { /** Message */ @@ -1745,6 +1754,13 @@ export interface components { order: components["schemas"]["Order"]; product: components["schemas"]["ProductInfo"] | null; }; + /** ProcessPreorderResponse */ + ProcessPreorderResponse: { + /** Status */ + status: string; + /** Checkout Session */ + checkout_session: Record | null; + }; /** ProductInfo */ ProductInfo: { /** Id */ @@ -1759,6 +1775,25 @@ export interface components { metadata: { [key: string]: string; }; + /** Active */ + active: boolean; + }; + /** ProductResponse */ + ProductResponse: { + /** Id */ + id: string; + /** Name */ + name: string; + /** Description */ + description: string | null; + /** Images */ + images: string[]; + /** Metadata */ + metadata: { + [key: string]: string; + }; + /** Active */ + active: boolean; }; /** PublicUserInfoResponseItem */ PublicUserInfoResponseItem: { @@ -3264,9 +3299,7 @@ export interface operations { }; get_order_orders__order_id__get: { parameters: { - query?: { - include_product?: boolean; - }; + query?: never; header?: never; path: { order_id: string; @@ -3297,9 +3330,7 @@ export interface operations { }; get_user_orders_orders_me_get: { parameters: { - query?: { - include_products?: boolean; - }; + query?: never; header?: never; path?: never; cookie?: never; @@ -3315,15 +3346,6 @@ export interface operations { "application/json": components["schemas"]["OrderWithProduct"][]; }; }; - /** @description Validation Error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["HTTPValidationError"]; - }; - }; }; }; update_order_shipping_address_orders__order_id__shipping_address_put: { @@ -3650,7 +3672,7 @@ export interface operations { }; }; }; - create_checkout_session_stripe_create_checkout_session_post: { + create_checkout_session_stripe_checkout_session_post: { parameters: { query?: never; header?: never; @@ -3700,7 +3722,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": Record; + "application/json": components["schemas"]["ProductResponse"]; }; }; /** @description Validation Error */ @@ -3784,7 +3806,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": Record; + "application/json": components["schemas"]["DeleteTestAccountsResponse"]; }; }; }; @@ -3806,7 +3828,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": Record; + "application/json": components["schemas"]["ProcessPreorderResponse"]; }; }; /** @description Validation Error */ diff --git a/store/app/routers/orders.py b/store/app/routers/orders.py index b75290e0..a453919f 100644 --- a/store/app/routers/orders.py +++ b/store/app/routers/orders.py @@ -26,6 +26,7 @@ class ProductInfo(BaseModel): description: str | None images: list[str] metadata: dict[str, str] + active: bool class OrderWithProduct(BaseModel): @@ -36,51 +37,57 @@ class OrderWithProduct(BaseModel): @router.get("/{order_id}", response_model=OrderWithProduct) async def get_order( order_id: str, - include_product: bool = False, user: User = Depends(get_session_user_with_read_permission), - crud: Crud = Depends(Crud.get), -) -> Order | OrderWithProduct: - order = await crud.get_order(order_id) - if order is None or order.user_id != user.id: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Order not found") - - if not include_product: - return order - - if order.stripe_product_id is None: - raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Order has no associated product") + crud: Crud = Depends(), +) -> OrderWithProduct: + async with crud: + order = await crud.get_order(order_id) + if not order or order.user_id != user.id: + raise HTTPException(status_code=404, detail="Order not found") + + # Get product info from Stripe + product = await stripe.get_product(order.stripe_product_id, crud) + + # Convert ProductResponse to ProductInfo + product_info = ProductInfo( + id=product.id, + name=product.name, + description=product.description, + images=product.images, + metadata=product.metadata, + active=product.active, + ) - product = await stripe.get_product(order.stripe_product_id, crud) - return OrderWithProduct(order=order, product=ProductInfo(**product)) + return OrderWithProduct(order=order, product=product_info) @router.get("/me", response_model=list[OrderWithProduct]) async def get_user_orders( - include_products: bool = False, user: User = Depends(get_session_user_with_read_permission), - crud: Crud = Depends(Crud.get), + crud: Crud = Depends(), ) -> list[OrderWithProduct]: + async def get_product_info(order: Order) -> OrderWithProduct: + if not order.stripe_product_id: + return OrderWithProduct(order=order, product=None) + + try: + product = await stripe.get_product(order.stripe_product_id, crud) + product_info = ProductInfo( + id=product.id, + name=product.name, + description=product.description, + images=product.images, + metadata=product.metadata, + active=product.active, + ) + return OrderWithProduct(order=order, product=product_info) + except Exception as e: + logger.error("Error processing order", extra={"order_id": order.id, "error": str(e), "user_id": user.id}) + return OrderWithProduct(order=order, product=None) + try: orders = await crud.get_orders_by_user_id(user.id) - - if not include_products: - return [OrderWithProduct(order=order, product=None) for order in orders] - - orders_with_products = [] - for order in orders: - try: - if order.stripe_product_id is None: - orders_with_products.append(OrderWithProduct(order=order, product=None)) - continue - product = await stripe.get_product(order.stripe_product_id, crud) - orders_with_products.append(OrderWithProduct(order=order, product=ProductInfo(**product))) - except Exception as e: - logger.error( - "Error processing order", extra={"order_id": order.id, "error": str(e), "user_id": user.id} - ) - orders_with_products.append(OrderWithProduct(order=order, product=None)) - continue - return orders_with_products + return [await get_product_info(order) for order in orders] except ItemNotFoundError: return [] except Exception as e: diff --git a/store/app/routers/stripe.py b/store/app/routers/stripe.py index 851d9712..e885c57a 100644 --- a/store/app/routers/stripe.py +++ b/store/app/routers/stripe.py @@ -4,7 +4,7 @@ import time from datetime import datetime from enum import Enum -from typing import Annotated, Literal +from typing import Annotated, Any, Literal import stripe from fastapi import APIRouter, Body, Depends, HTTPException, Request, status @@ -159,6 +159,7 @@ async def stripe_connect_webhook(request: Request, crud: Crud = Depends(Crud.get is_fully_onboarded = bool( account.get("details_submitted") and account.get("payouts_enabled") + and account.get("charges_enabled") and capabilities.get("card_payments") == "active" and capabilities.get("transfers") == "active" ) @@ -209,7 +210,7 @@ async def stripe_connect_webhook(request: Request, crud: Crud = Depends(Crud.get raise HTTPException(status_code=500, detail=str(e)) -async def handle_checkout_session_completed(session: stripe.checkout.Session, crud: Crud) -> None: +async def handle_checkout_session_completed(session: dict[str, Any], crud: Crud) -> None: logger.info("Processing checkout session: %s", session["id"]) try: # Retrieve full session details from the connected account @@ -295,7 +296,7 @@ class CreateCheckoutSessionResponse(BaseModel): stripe_connect_account_id: str -@router.post("/create-checkout-session", response_model=CreateCheckoutSessionResponse) +@router.post("/checkout-session", response_model=CreateCheckoutSessionResponse) async def create_checkout_session( request: CreateCheckoutSessionRequest, user: User = Depends(get_session_user_with_read_permission), @@ -436,6 +437,7 @@ async def create_checkout_session( "application_fee_amount": application_fee, }, "stripe_account": seller.stripe_connect.account_id, + "metadata": {**metadata, "stripe_price_id": listing.stripe_price_id}, } ) @@ -452,8 +454,17 @@ async def create_checkout_session( raise HTTPException(status_code=400, detail=str(e)) -@router.get("/get-product/{product_id}") -async def get_product(product_id: str, crud: Annotated[Crud, Depends(Crud.get)]) -> stripe.Product: +class ProductResponse(BaseModel): + id: str + name: str + description: str | None + images: list[str] + metadata: dict[str, str] + active: bool + + +@router.get("/get-product/{product_id}", response_model=ProductResponse) +async def get_product(product_id: str, crud: Annotated[Crud, Depends(Crud.get)]) -> ProductResponse: try: # First get the listing by stripe_product_id listing = await crud.get_listing_by_stripe_product_id(product_id) @@ -468,7 +479,15 @@ async def get_product(product_id: str, crud: Annotated[Crud, Depends(Crud.get)]) # Retrieve the product using the seller's connected account product = stripe.Product.retrieve(product_id, stripe_account=seller.stripe_connect.account_id) - return product + # Convert Stripe Product to our ProductResponse model + return ProductResponse( + id=product.id, + name=product.name, + description=product.description, + images=product.images, + metadata=product.metadata, + active=product.active, + ) except stripe.StripeError as e: logger.error(f"Stripe error retrieving product: {str(e)}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) @@ -662,7 +681,7 @@ async def create_listing_product( class ProcessPreorderResponse(BaseModel): status: str - checkout_session: stripe.checkout.Session | None + checkout_session: dict[str, Any] | None @router.post("/process-preorder/{order_id}", response_model=ProcessPreorderResponse)