Rev 36321 | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.spice.profitmandi.web.v2.controller;import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;import com.spice.profitmandi.common.model.ProfitMandiConstants;import com.spice.profitmandi.common.model.UserInfo;import com.spice.profitmandi.dao.cart.CartService;import com.spice.profitmandi.dao.cart.v2.CartValidationService;import com.spice.profitmandi.dao.cart.v2.CheckoutValidationResult;import com.spice.profitmandi.dao.cart.v2.OpenCartValidationResult;import com.spice.profitmandi.dao.cart.v2.SaleType;import com.spice.profitmandi.dao.entity.catalog.Item;import com.spice.profitmandi.dao.entity.catalog.TagListing;import com.spice.profitmandi.dao.entity.user.CartLine;import com.spice.profitmandi.dao.enumuration.catalog.ByPassRequestStatus;import com.spice.profitmandi.dao.model.AddCartRequest;import com.spice.profitmandi.dao.model.CartItem;import com.spice.profitmandi.dao.model.UserCart;import com.spice.profitmandi.dao.repository.catalog.ItemRepository;import com.spice.profitmandi.dao.repository.catalog.TagListingRepository;import com.spice.profitmandi.dao.repository.dtr.UserAccountRepository;import com.spice.profitmandi.dao.repository.user.CartLineRepository;import com.spice.profitmandi.service.scheme.SchemeService;import com.spice.profitmandi.web.controller.CartController;import com.spice.profitmandi.web.v2.response.ApiResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.ui.Model;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import java.util.ArrayList;import java.util.Arrays;import java.util.HashSet;import java.util.List;import java.util.Map;import java.util.Set;import java.util.stream.Collectors;import org.springframework.transaction.annotation.Transactional;@RestController@RequestMapping("/v2")@Transactional(rollbackFor = Throwable.class)public class V2CartController extends V2BaseController {@Autowiredprivate CartController cartController;@Autowiredprivate CartValidationService cartValidationService;@Autowiredprivate UserAccountRepository userAccountRepository;@Autowiredprivate CartService cartService;@Autowiredprivate CartLineRepository cartLineRepository;@Autowiredprivate ItemRepository itemRepository;@Autowiredprivate TagListingRepository tagListingRepository;@Autowiredprivate SchemeService schemeService;@Autowiredprivate com.spice.profitmandi.dao.repository.user.CartRepository cartRepository;private static final int MRP_TAG_ID = 4;private static final double CARRY_BAG_THRESHOLD = 12000d;private static final float CARRY_BAG_PRICE = 0.01f;@GetMapping("/cart")public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,@RequestParam(value = "pincode", defaultValue = "110001") String pincode,@RequestParam int bucketId) throws Throwable {return wrapResponse(cartController.validateCart(request, pincode, bucketId));}@PostMapping("/cart")public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,@RequestBody AddCartRequest addCartRequest,@RequestParam(value = "pincode", defaultValue = "110001") String pincode) throws Throwable {return wrapResponse(cartController.validateCart(request, addCartRequest, pincode));}@PostMapping("/cart/changeAddress")public ResponseEntity<ApiResponse<?>> changeAddress(HttpServletRequest request,@RequestParam(value = "addressId") long addressId) throws Throwable {return wrapResponse(cartController.changeAddress(request, addressId));}@GetMapping("/byPassRequests")public ResponseEntity<ApiResponse<?>> byPassRequests(HttpServletRequest request, Model model) throws Throwable {return wrapResponse(cartController.byPassRequests(request, model));}@PostMapping("/byPassRequestAction")public ResponseEntity<ApiResponse<?>> addAmountToWalletRequestRejected(HttpServletRequest request,@RequestParam(name = "id", defaultValue = "0") int id,@RequestParam ByPassRequestStatus status,@RequestParam String reason,Model model) throws Throwable {return wrapResponse(cartController.addAmountToWalletRequestRejected(request, id, status, reason, model));}@GetMapping("/cart/payment")public ResponseEntity<ApiResponse<?>> validateCartPayment(HttpServletRequest request,@RequestParam(defaultValue = "0") int paymentId,Model model) throws Throwable {return wrapResponse(cartController.validateCartPayment(request, paymentId, model));}@GetMapping("/partner/hidAllocation")public ResponseEntity<ApiResponse<?>> getItemHidAllocation(HttpServletRequest request) throws Throwable {return wrapResponse(cartController.getItemHidAllocation(request));}// =================================================================// Cart v2 redesign — two-step validation (open + checkout)// All endpoints require authentication. Guests are rejected with 401.// =================================================================/** STEP 1 — soft validation. Never blocks. Surfaces warnings for price drift,* qty downgrade, OOS, unavailability. Refreshes last-seen price baseline* used by Step 2 drift detection.* Pincode is derived from the cart's bound address server-side; clients* do not pass it. */@GetMapping("/cart/open")public ResponseEntity<?> openCart(HttpServletRequest request)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();OpenCartValidationResult result = cartValidationService.validateForOpen(ctx.cartId, ctx.storeId);return ResponseEntity.ok(ApiResponse.success(result));}/** STEP 2 — hard validation immediately before payment. Returns* {@code valid=false} on any drift; user must hit {@code /cart/open}* to re-confirm, then retry this endpoint. On pass, creates a 240s* stock reservation and returns a {@code reservationId}.* Procurement only — tertiary billing drafts check out via* /v2/billing/drafts/{cartId}/commit. */@PostMapping("/cart/validate")public ResponseEntity<?> validateForCheckout(HttpServletRequest request,@RequestParam(value = "addressId") long addressId)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();CheckoutValidationResult result = cartValidationService.validateForCheckout(ctx.cartId, ctx.userId, ctx.storeId, addressId, SaleType.PARTNER_PROCUREMENT);return ResponseEntity.ok(ApiResponse.success(result));}// -----------------------------------------------------------------// Cart mutations — productId-only payload. All return the freshly// hydrated cart (Step 1 result) so the client never keeps stale state.// -----------------------------------------------------------------/** Add a single product to the cart. Increments qty if already present. */@PostMapping("/cart/items")public ResponseEntity<?> addItem(HttpServletRequest request,@RequestBody AddCartItemRequest body)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();if (body == null || body.productId <= 0 || body.quantity <= 0) {throw new ProfitMandiBusinessException("body", body, "CART_ITEM_INVALID_PAYLOAD");}List<CartItem> desired = currentLinesAsItems(ctx.cartId);boolean found = false;for (CartItem existing : desired) {if (existing.getItemId() == body.productId) {existing.setQuantity(existing.getQuantity() + body.quantity);found = true;break;}}if (!found) {desired.add(new CartItem(body.quantity, body.productId));}return applyAndHydrate(ctx, desired);}/** Set a specific line's quantity. quantity=0 removes the line. */@PatchMapping("/cart/items/{productId}")public ResponseEntity<?> updateQuantity(HttpServletRequest request,@PathVariable int productId,@RequestParam int quantity)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();if (quantity < 0) {throw new ProfitMandiBusinessException("quantity", quantity, "CART_ITEM_NEGATIVE_QTY");}List<CartItem> desired = currentLinesAsItems(ctx.cartId);if (quantity == 0) {desired = desired.stream().filter(ci -> ci.getItemId() != productId).collect(Collectors.toList());} else {boolean found = false;for (CartItem existing : desired) {if (existing.getItemId() == productId) {existing.setQuantity(quantity);found = true;break;}}if (!found) {desired.add(new CartItem(quantity, productId));}}return applyAndHydrate(ctx, desired);}/** Remove a single line by productId. */@DeleteMapping("/cart/items/{productId}")public ResponseEntity<?> removeItem(HttpServletRequest request,@PathVariable int productId)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();List<CartItem> desired = currentLinesAsItems(ctx.cartId).stream().filter(ci -> ci.getItemId() != productId).collect(Collectors.toList());return applyAndHydrate(ctx, desired);}/** Clear the entire cart. */@DeleteMapping("/cart")public ResponseEntity<?> clearCart(HttpServletRequest request)throws ProfitMandiBusinessException {java.util.Optional<AuthCtx> auth = resolveAuth(request);if (!auth.isPresent()) return unauthorized();AuthCtx ctx = auth.get();cartService.clearCart(ctx.cartId);return ResponseEntity.ok(ApiResponse.success(cartValidationService.validateForOpen(ctx.cartId, ctx.storeId)));}/*** Enriches each desired CartItem with the live selling price, runs the* procurement carry-bag rebalance, applies the change, then returns a* hydrated Step-1 view. /v2/cart/* is procurement-only; tertiary drafts* have their own controller.*/private ResponseEntity<?> applyAndHydrate(AuthCtx ctx, List<CartItem> desired)throws ProfitMandiBusinessException {enrichPrices(ctx, desired);rebalanceCarryBag(desired);cartService.addItemsToCart(ctx.cartId, desired);return ResponseEntity.ok(ApiResponse.success(cartValidationService.validateForOpen(ctx.cartId, ctx.storeId)));}/*** Populates {@link CartItem#setSellingPrice(double)} for each non-carry-bag item* with the live (MOP - scheme cashback) figure. Preserves the existing value on* lookup miss so a partial-Solr outage doesn't zero out prices.*/private void enrichPrices(AuthCtx ctx, List<CartItem> desired) throws ProfitMandiBusinessException {Set<Integer> itemIds = desired.stream().filter(ci -> ci.getItemId() != ProfitMandiConstants.ITEM_CARRY_BAG).map(CartItem::getItemId).collect(Collectors.toSet());if (itemIds.isEmpty()) return;Map<Integer, TagListing> tagByItem = tagListingRepository.selectByItemIdsAndTagIds(itemIds, new HashSet<>(Arrays.asList(MRP_TAG_ID))).stream().collect(Collectors.toMap(TagListing::getItemId, x -> x, (a, b) -> a));Map<Integer, Item> items = itemRepository.selectByIds(itemIds).stream().collect(Collectors.toMap(Item::getId, x -> x, (a, b) -> a));List<Integer> catalogIds = items.values().stream().map(Item::getCatalogItemId).distinct().collect(Collectors.toList());Map<Integer, Float> cashbackByCatalog = schemeService.getCatalogSchemeCashBack(ctx.storeId, catalogIds);if (cashbackByCatalog == null) cashbackByCatalog = java.util.Collections.emptyMap();for (CartItem ci : desired) {if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;TagListing tl = tagByItem.get(ci.getItemId());Item it = items.get(ci.getItemId());if (tl == null || it == null) continue;float cashback = cashbackByCatalog.getOrDefault(it.getCatalogItemId(), 0f);ci.setSellingPrice(tl.getMop() - cashback);}}/*** Server-side port of the legacy Zustand carry-bag rule: any item with* sellingPrice > ₹12,000 needs one carry bag per unit. Runs BEFORE addItemsToCart* so the full-sync semantics keep the cart consistent in one round-trip.*/private void rebalanceCarryBag(List<CartItem> desired) {int bagsNeeded = 0;for (CartItem ci : desired) {if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;if (ci.getSellingPrice() > CARRY_BAG_THRESHOLD) {bagsNeeded += ci.getQuantity();}}desired.removeIf(ci -> ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG);if (bagsNeeded > 0) {CartItem bag = new CartItem(bagsNeeded, ProfitMandiConstants.ITEM_CARRY_BAG);bag.setSellingPrice(CARRY_BAG_PRICE);desired.add(bag);}}private List<CartItem> currentLinesAsItems(int cartId) {List<CartLine> lines = cartLineRepository.selectAllByCart(cartId);if (lines == null) return new ArrayList<>();return lines.stream().map(l -> {CartItem ci = new CartItem(l.getQuantity(), l.getItemId());ci.setSellingPrice(l.getActualPrice());return ci;}).collect(Collectors.toList());}public static class AddCartItemRequest {public int productId;public int quantity;}// -----------------------------------------------------------------// Auth helpers — login is mandatory per product decision (2026-04-21).// Guest carts are not supported; unauthenticated requests get 401.//// Returns the auth context as an Optional rather than throwing, because// GlobalExceptionHandler's @ExceptionHandler(Exception.class) would// otherwise wrap any RuntimeException as HTTP 500 regardless of// @ResponseStatus annotations on the exception class.// -----------------------------------------------------------------private static final class AuthCtx {final int userId;final int storeId;final int cartId;AuthCtx(int userId, int storeId, int cartId) {this.userId = userId; this.storeId = storeId; this.cartId = cartId;}}/*** Resolves the authenticated partner's PROCUREMENT cart. v2/cart/* is* procurement-only. Tertiary billing drafts live under /v2/billing/* and* are resolved per-cartId there.*/private java.util.Optional<AuthCtx> resolveAuth(HttpServletRequest request) {Object userIdAttr = request.getAttribute("userId");if (userIdAttr == null) {return java.util.Optional.empty();}int userId;try {userId = (int) userIdAttr;} catch (ClassCastException e) {return java.util.Optional.empty();}if (userId <= 0) {return java.util.Optional.empty();}Object userInfoAttr = request.getAttribute("userInfo");int storeId = userInfoAttr instanceof UserInfo ? ((UserInfo) userInfoAttr).getRetailerId() : 0;UserCart uc = userAccountRepository.getUserCart(userId);if (uc == null || uc.getCartId() <= 0) {return java.util.Optional.empty();}return java.util.Optional.of(new AuthCtx(userId, storeId, uc.getCartId()));}private ResponseEntity<?> unauthorized() {java.util.Map<String, Object> body = new java.util.HashMap<>();body.put("responseStatus", "FAILURE");body.put("statusCode", 401);body.put("statusMessage", "LOGIN_REQUIRED");body.put("message", "Authentication is required. Send a valid Auth-Token header.");return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);}}