Subversion Repositories SmartDukaan

Rev

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 {

    @Autowired
    private CartController cartController;

    @Autowired
    private CartValidationService cartValidationService;

    @Autowired
    private UserAccountRepository userAccountRepository;

    @Autowired
    private CartService cartService;

    @Autowired
    private CartLineRepository cartLineRepository;

    @Autowired
    private ItemRepository itemRepository;

    @Autowired
    private TagListingRepository tagListingRepository;

    @Autowired
    private SchemeService schemeService;

    @Autowired
    private 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);
    }
}