fix stripe payment flow

This commit is contained in:
2026-04-09 22:52:57 -06:00
parent 4d91d8b331
commit ae0ccfd45b
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) { public ResponseEntity<CheckoutResponse> checkout(@Valid @RequestBody CheckoutRequest request) {
Long userId = AuthenticationHelper.getAuthenticatedUserId(); 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; package com.petshop.backend.dto.cart;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
public class CheckoutRequest { public class CheckoutRequest {
@@ -8,17 +7,6 @@ public class CheckoutRequest {
@NotNull @NotNull
private Long storeId; private Long storeId;
@NotBlank
private String paymentMethodId;
public Long getStoreId() { return storeId; } public Long getStoreId() { return storeId; }
public void setStoreId(Long storeId) { this.storeId = 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 @Transactional
public CheckoutResponse checkout(Long userId, Long storeId, String paymentMethodId) { public CheckoutResponse checkout(Long userId, Long storeId) {
Cart cart = cartRepository Cart cart = cartRepository
.findActiveCartByUserAndStore(userId, storeId, "ACTIVE") .findActiveCartByUserAndStore(userId, storeId, "ACTIVE")
.orElseThrow(() -> new BusinessException("No active cart found")); .orElseThrow(() -> new BusinessException("No active cart found"));
@@ -213,21 +213,12 @@ public class CartService {
PaymentIntentCreateParams params = PaymentIntentCreateParams.builder() PaymentIntentCreateParams params = PaymentIntentCreateParams.builder()
.setAmount(amountInCents) .setAmount(amountInCents)
.setCurrency("usd") .setCurrency("usd")
.setPaymentMethod(paymentMethodId)
.setConfirm(true)
.setReturnUrl("http://localhost:3000/cart/confirmation")
.putMetadata("cartId", String.valueOf(cart.getCartId())) .putMetadata("cartId", String.valueOf(cart.getCartId()))
.putMetadata("userId", String.valueOf(userId)) .putMetadata("userId", String.valueOf(userId))
.build(); .build();
PaymentIntent intent = PaymentIntent.create(params); 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( return new CheckoutResponse(
cart.getCartId(), cart.getCartId(),
intent.getClientSecret(), intent.getClientSecret(),
@@ -235,13 +226,38 @@ public class CartService {
intent.getStatus() intent.getStatus()
); );
} }
catch (StripeException e) { catch (StripeException e) {
throw new BusinessException("Payment processing failed: " + e.getMessage()); 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) { private void recalculate(Cart cart) {
List<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId()); 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, useStripe,
useElements, useElements,
} from "@stripe/react-stripe-js"; } from "@stripe/react-stripe-js";
import { apiCompleteCheckout } from "@/lib/cartApi";
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || ""); const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "");
function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) { function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) {
const stripe = useStripe(); const stripe = useStripe();
const elements = useElements(); const elements = useElements();
const { token } = useAuth();
const [paying, setPaying] = useState(false); const [paying, setPaying] = useState(false);
const [payError, setPayError] = useState(null); const [payError, setPayError] = useState(null);
@@ -34,9 +36,14 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) {
if (error) { if (error) {
setPayError(error.message); setPayError(error.message);
setPaying(false); setPaying(false);
} }
else { else {
const paymentIntentId = clientSecret.split("_secret_")[0];
try {
await apiCompleteCheckout(token, paymentIntentId);
} catch {
}
onSuccess(); onSuccess();
} }
} }
@@ -159,7 +166,7 @@ export default function CartPage() {
setCheckoutLoading(true); setCheckoutLoading(true);
setCheckoutError(null); setCheckoutError(null);
try { try {
const result = await checkout("pm_card_visa"); const result = await checkout();
if (result?.clientSecret) { if (result?.clientSecret) {
setClientSecret(result.clientSecret); setClientSecret(result.clientSecret);
setCheckoutTotal(result.totalAmount); setCheckoutTotal(result.totalAmount);

View File

@@ -124,16 +124,10 @@ export function CartProvider({ children }) {
); );
const checkout = useCallback( const checkout = useCallback(
async (paymentMethodId) => { async () => {
if (!token || !selectedStoreId) throw new Error("Select a store first"); if (!token || !selectedStoreId) throw new Error("Select a store first");
const result = await apiCheckout(token, { const result = await apiCheckout(token, { storeId: selectedStoreId });
storeId: selectedStoreId,
paymentMethodId,
});
if (result?.status === "succeeded") {
setCart(null);
}
return result; return result;
}, },
[token, selectedStoreId] [token, selectedStoreId]

View File

@@ -73,12 +73,21 @@ export async function apiApplyCoupon(token, storeId, couponCode) {
return handleResponse(res); return handleResponse(res);
} }
export async function apiCheckout(token, { storeId, paymentMethodId }) { export async function apiCheckout(token, { storeId }) {
const res = await fetch(`${BASE}/checkout`, { const res = await fetch(`${BASE}/checkout`, {
method: "POST", method: "POST",
headers: authHeaders(token), 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); return handleResponse(res);
} }