fix stripe payment flow
This commit is contained in:
1
backend/.env.example
Normal file
1
backend/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<CartItem> items = cartItemRepository.findByCartCartId(cart.getCartId());
|
||||
|
||||
|
||||
1
web/.env.example
Normal file
1
web/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user