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) {
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
@@ -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) {
|
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
1
web/.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -37,6 +39,11 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|||||||
@@ -124,15 +124,9 @@ 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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,11 +73,20 @@ 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);
|
||||||
|
|||||||
Reference in New Issue
Block a user