feat: Full-stack StickerStore application (Spring Boot + React/Vite)#212
feat: Full-stack StickerStore application (Spring Boot + React/Vite)#212devin-ai-integration[bot] wants to merge 2 commits into
Conversation
Backend (Spring Boot 3 + Java 17): - JPA entities: User, Product, Cart, CartItem, Order, OrderItem - Spring Security with JWT authentication (HS256) - REST controllers for auth, products, cart, and orders - H2 in-memory database with data seeder (15 products, 2 users) - Global exception handling Frontend (React 18 + Vite + TypeScript): - Tailwind CSS styling with purple/indigo theme - Auth and Cart contexts with localStorage persistence - Pages: Home, Login, Register, ProductDetail, Cart, Checkout, Orders - Protected routes, responsive design, toast notifications - Axios interceptor for JWT token management
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
…ersist shipping info on checkout
| if (product.getStockQuantity() < cartItem.getQuantity()) { | ||
| throw new BadRequestException("Insufficient stock for: " + product.getName()); | ||
| } | ||
|
|
||
| product.setStockQuantity(product.getStockQuantity() - cartItem.getQuantity()); | ||
| productRepository.save(product); |
There was a problem hiding this comment.
🔴 Checkout stock decrement has no concurrency protection, allowing overselling
In OrderService.checkout(), the stock check and decrement (product.getStockQuantity() < cartItem.getQuantity() followed by product.setStockQuantity(...)) is not protected by any database-level locking. Two concurrent checkout transactions can both read the same stock value, both pass the check, and both save decremented values, resulting in stock going negative. For example: stock=10, Thread A wants 7, Thread B wants 5. Both read stock=10, both pass the check, A saves 3, B saves 5 — final stock is 5 instead of the correct -2 (which should have been rejected). The @Transactional annotation alone with the default READ_COMMITTED isolation level does not prevent this lost-update race condition. A pessimistic lock (@Lock(PESSIMISTIC_WRITE)) on the Product query or optimistic locking (@Version on the Product entity) is needed.
Prompt for agents
The checkout method in OrderService.java has a race condition on stock quantity. Between the stock check at line 57 and the decrement at line 61, another concurrent transaction can read the same stock value. The @Transactional annotation with default READ_COMMITTED isolation does not prevent this lost-update anomaly.
To fix this, add optimistic locking to the Product entity by adding a @Version field:
@Version
private Long version;
This will cause JPA to automatically check the version during updates and throw an OptimisticLockException if another transaction modified the same product concurrently. The checkout method should catch this exception and return an appropriate error to the user.
Alternatively, use pessimistic locking by adding a custom query in ProductRepository with @Lock(LockModeType.PESSIMISTIC_WRITE) to lock the product rows during checkout, or use a native SQL UPDATE with a WHERE clause that includes the expected stock quantity.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (cartItems.length === 0) { | ||
| navigate('/cart'); | ||
| return null; |
There was a problem hiding this comment.
🟡 CheckoutPage calls navigate() during render, violating React rules
At CheckoutPage.tsx:39-41, navigate('/cart') is called directly in the render body when cartItems.length === 0. Calling navigate() during render is a side effect that violates React's rendering rules — side effects should be performed in useEffect or event handlers, not during render. This can produce React warnings in development and cause unpredictable behavior with React Strict Mode's double-rendering. In practice, if the component ever renders with an empty cart before the intended navigation takes effect (e.g., due to a re-render timing issue), this navigate('/cart') could compete with a legitimate navigate('/orders/${id}') from the checkout handler.
Prompt for agents
In CheckoutPage.tsx, the navigate('/cart') call at lines 39-41 happens during the render phase, which is a React anti-pattern. Side effects like navigation must happen inside useEffect or event handlers, not during render.
Replace the imperative navigate call with either:
1. A useEffect that watches cartItems and navigates when empty:
useEffect(() => { if (cartItems.length === 0 && !loading) navigate('/cart'); }, [cartItems, loading, navigate]);
2. Or use React Router's <Navigate> component which properly handles navigation via useEffect internally:
if (cartItems.length === 0) return <Navigate to="/cart" replace />;
Note: you may also want to add a loading state check to avoid redirecting before the cart data has loaded.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Adds a complete full-stack sticker shop e-commerce application from scratch, organized as a monorepo:
Backend (
backend/) — Spring Boot 3.2 + Java 17:Frontend (
frontend/) — React 18 + Vite + TypeScript:Verified locally: Backend compiles, frontend passes TypeScript check and Vite production build. Full end-to-end testing completed (login → browse → add to cart → checkout → view orders).
Updates since last revision
Addressed Devin Review feedback in
c58ce02:frontend/src/api.ts): The axios response interceptor now skips the redirect-to-login behavior for/auth/endpoints, so failed login attempts correctly show error toasts instead of triggering a full page reload.shippingName,shippingAddress,shippingCity,shippingZipfields to theOrderentity.OrderController.checkout()now accepts@RequestBody CheckoutRequestandOrderService.checkout()persists the shipping data with the order.Review & Testing Checklist for Human
cd backend && ./mvnw spring-boot:run), start frontend (cd frontend && npm run dev), then: login withuser/user123→ browse products → add to cart → adjust quantities → checkout with shipping form → verify order appears in order history. Confirm shipping info is saved (check H2 console at/h2-consoleif needed, sinceOrderResponsedoes not currently return shipping fields back to the UI).SecurityConfig.java): CORS is locked tolocalhost:5173only, CSRF is disabled (standard for stateless JWT). The JWT secret is hardcoded inapplication.yml— fine for demo/dev, but flag if this will be deployed.productId: 0on update (CartContext.tsxline ~68): TheupdateQuantitycall sends{ productId: 0, quantity }as a dummy value. The backend ignoresproductIdon PUT (it resolves byitemIdfrom the path), so it works, but it's a code smell worth cleaning up.navigate()during render when cart is empty — this can cause React warnings. Consider wrapping in auseEffect.Notes
OrderResponsedoes not include shipping fields, so shipping data is persisted but not surfaced back to the UI. This is a known gap — add shipping fields toOrderResponseandOrderDetailPageif needed.tsconfig.jsonandtsconfig.app.jsonare identical — standard Vite scaffolding would havetsconfig.jsonreferencetsconfig.app.jsonvia"references". No functional impact but slightly non-standard.DataInitializerre-seeds on every startup.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/1d5815112b2a41f4b94bb716edb0cf92