From ae0ccfd45be66165546c99a941dcd3c887a20be2 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Thu, 9 Apr 2026 22:52:57 -0600 Subject: [PATCH] fix stripe payment flow --- backend/.env.example | 1 + .../backend/controller/CartController.java | 12 +++++- .../backend/dto/cart/CheckoutRequest.java | 12 ------ .../petshop/backend/service/CartService.java | 40 +++++++++++++------ web/.env.example | 1 + web/app/cart/page.js | 13 ++++-- web/context/CartContext.js | 12 ++---- web/lib/cartApi.js | 15 +++++-- 8 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 backend/.env.example create mode 100644 web/.env.example diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..0e824bfb --- /dev/null +++ b/backend/.env.example @@ -0,0 +1 @@ +STRIPE_SECRET_KEY=sk_test_... diff --git a/backend/src/main/java/com/petshop/backend/controller/CartController.java b/backend/src/main/java/com/petshop/backend/controller/CartController.java index e43cce6e..49281666 100644 --- a/backend/src/main/java/com/petshop/backend/controller/CartController.java +++ b/backend/src/main/java/com/petshop/backend/controller/CartController.java @@ -80,5 +80,15 @@ public class CartController { public ResponseEntity checkout(@Valid @RequestBody CheckoutRequest request) { Long userId = AuthenticationHelper.getAuthenticatedUserId(); - return ResponseEntity.ok(cartService.checkout(userId, request.getStoreId(), request.getPaymentMethodId()));} + return ResponseEntity.ok(cartService.checkout(userId, request.getStoreId())); + } + + @PostMapping("/checkout/complete") + @PreAuthorize("isAuthenticated()") + public ResponseEntity completeCheckout(@RequestParam String paymentIntentId) { + Long userId = AuthenticationHelper.getAuthenticatedUserId(); + cartService.completeCheckout(userId, paymentIntentId); + + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/cart/CheckoutRequest.java b/backend/src/main/java/com/petshop/backend/dto/cart/CheckoutRequest.java index 084d4857..856582ed 100644 --- a/backend/src/main/java/com/petshop/backend/dto/cart/CheckoutRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/cart/CheckoutRequest.java @@ -1,6 +1,5 @@ package com.petshop.backend.dto.cart; -import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; public class CheckoutRequest { @@ -8,17 +7,6 @@ public class CheckoutRequest { @NotNull private Long storeId; - @NotBlank - private String paymentMethodId; - public Long getStoreId() { return storeId; } public void setStoreId(Long storeId) { this.storeId = storeId; } - - public String getPaymentMethodId() { - return paymentMethodId; - } - - public void setPaymentMethodId(String paymentMethodId) { - this.paymentMethodId = paymentMethodId; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 97819bee..df8879fa 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -190,7 +190,7 @@ public class CartService { } @Transactional - public CheckoutResponse checkout(Long userId, Long storeId, String paymentMethodId) { + public CheckoutResponse checkout(Long userId, Long storeId) { Cart cart = cartRepository .findActiveCartByUserAndStore(userId, storeId, "ACTIVE") .orElseThrow(() -> new BusinessException("No active cart found")); @@ -213,21 +213,12 @@ public class CartService { PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() .setAmount(amountInCents) .setCurrency("usd") - .setPaymentMethod(paymentMethodId) - .setConfirm(true) - .setReturnUrl("http://localhost:3000/cart/confirmation") .putMetadata("cartId", String.valueOf(cart.getCartId())) .putMetadata("userId", String.valueOf(userId)) .build(); PaymentIntent intent = PaymentIntent.create(params); - if ("succeeded".equals(intent.getStatus()) - || "requires_action".equals(intent.getStatus())) { - cart.setCartStatus("CHECKED_OUT"); - cartRepository.save(cart); - } - return new CheckoutResponse( cart.getCartId(), intent.getClientSecret(), @@ -235,13 +226,38 @@ public class CartService { intent.getStatus() ); - } - + } + catch (StripeException e) { throw new BusinessException("Payment processing failed: " + e.getMessage()); } } + @Transactional + public void completeCheckout(Long userId, String paymentIntentId) { + try { + PaymentIntent intent = PaymentIntent.retrieve(paymentIntentId); + + if (!"succeeded".equals(intent.getStatus())) { + throw new BusinessException("Payment has not been completed"); + } + + Long cartId = Long.parseLong(intent.getMetadata().get("cartId")); + Cart cart = cartRepository.findById(cartId) + .orElseThrow(() -> new BusinessException("Cart not found")); + + if (!cart.getUser().getUserId().equals(userId)) { + throw new BusinessException("Unauthorized"); + } + + cart.setCartStatus("CHECKED_OUT"); + cartRepository.save(cart); + + } catch (StripeException e) { + throw new BusinessException("Payment verification failed: " + e.getMessage()); + } + } + private void recalculate(Cart cart) { List items = cartItemRepository.findByCartCartId(cart.getCartId()); diff --git a/web/.env.example b/web/.env.example new file mode 100644 index 00000000..d74ca824 --- /dev/null +++ b/web/.env.example @@ -0,0 +1 @@ +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... diff --git a/web/app/cart/page.js b/web/app/cart/page.js index bbf4b94d..5be185a8 100644 --- a/web/app/cart/page.js +++ b/web/app/cart/page.js @@ -11,12 +11,14 @@ import { useStripe, useElements, } from "@stripe/react-stripe-js"; +import { apiCompleteCheckout } from "@/lib/cartApi"; const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || ""); function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) { const stripe = useStripe(); const elements = useElements(); + const { token } = useAuth(); const [paying, setPaying] = useState(false); const [payError, setPayError] = useState(null); @@ -34,9 +36,14 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) { if (error) { setPayError(error.message); setPaying(false); - } - + } + else { + const paymentIntentId = clientSecret.split("_secret_")[0]; + try { + await apiCompleteCheckout(token, paymentIntentId); + } catch { + } onSuccess(); } } @@ -159,7 +166,7 @@ export default function CartPage() { setCheckoutLoading(true); setCheckoutError(null); try { - const result = await checkout("pm_card_visa"); + const result = await checkout(); if (result?.clientSecret) { setClientSecret(result.clientSecret); setCheckoutTotal(result.totalAmount); diff --git a/web/context/CartContext.js b/web/context/CartContext.js index 3b604b09..d3dfc331 100644 --- a/web/context/CartContext.js +++ b/web/context/CartContext.js @@ -124,16 +124,10 @@ export function CartProvider({ children }) { ); const checkout = useCallback( - async (paymentMethodId) => { + async () => { if (!token || !selectedStoreId) throw new Error("Select a store first"); - const result = await apiCheckout(token, { - storeId: selectedStoreId, - paymentMethodId, - }); - if (result?.status === "succeeded") { - setCart(null); - } - + const result = await apiCheckout(token, { storeId: selectedStoreId }); + return result; }, [token, selectedStoreId] diff --git a/web/lib/cartApi.js b/web/lib/cartApi.js index bc6d8a22..b47049bc 100644 --- a/web/lib/cartApi.js +++ b/web/lib/cartApi.js @@ -73,12 +73,21 @@ export async function apiApplyCoupon(token, storeId, couponCode) { return handleResponse(res); } -export async function apiCheckout(token, { storeId, paymentMethodId }) { +export async function apiCheckout(token, { storeId }) { const res = await fetch(`${BASE}/checkout`, { method: "POST", headers: authHeaders(token), - body: JSON.stringify({ storeId, paymentMethodId }), + body: JSON.stringify({ storeId }), }); - + + return handleResponse(res); +} + +export async function apiCompleteCheckout(token, paymentIntentId) { + const res = await fetch(`${BASE}/checkout/complete?paymentIntentId=${encodeURIComponent(paymentIntentId)}`, { + method: "POST", + headers: authHeaders(token), + }); + return handleResponse(res); }