fix stripe payment flow

This commit is contained in:
2026-04-09 22:52:57 -06:00
parent 1010d57b79
commit 39e4a3896e
8 changed files with 66 additions and 40 deletions

1
backend/.env.example Normal file
View File

@@ -0,0 +1 @@
STRIPE_SECRET_KEY=sk_test_...

View File

@@ -80,5 +80,15 @@ public class CartController {
public ResponseEntity<CheckoutResponse> 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<Void> completeCheckout(@RequestParam String paymentIntentId) {
Long userId = AuthenticationHelper.getAuthenticatedUserId();
cartService.completeCheckout(userId, paymentIntentId);
return ResponseEntity.noContent().build();
}
}

View File

@@ -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;
}
}

View File

@@ -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(),
@@ -242,6 +233,31 @@ public class CartService {
}
}
@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<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());

1
web/.env.example Normal file
View File

@@ -0,0 +1 @@
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...

View File

@@ -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);
@@ -37,6 +39,11 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) {
}
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);

View File

@@ -124,15 +124,9 @@ 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;
},

View File

@@ -73,11 +73,20 @@ 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);