Subversion Repositories SmartDukaan

Rev

Rev 36321 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36321 vikas 1
package com.spice.profitmandi.web.v2.controller;
2
 
36376 aman 3
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
4
import com.spice.profitmandi.common.model.ProfitMandiConstants;
5
import com.spice.profitmandi.common.model.UserInfo;
6
import com.spice.profitmandi.dao.cart.CartService;
7
import com.spice.profitmandi.dao.cart.v2.CartValidationService;
8
import com.spice.profitmandi.dao.cart.v2.CheckoutValidationResult;
9
import com.spice.profitmandi.dao.cart.v2.OpenCartValidationResult;
10
import com.spice.profitmandi.dao.cart.v2.SaleType;
11
import com.spice.profitmandi.dao.entity.catalog.Item;
12
import com.spice.profitmandi.dao.entity.catalog.TagListing;
13
import com.spice.profitmandi.dao.entity.user.CartLine;
36321 vikas 14
import com.spice.profitmandi.dao.enumuration.catalog.ByPassRequestStatus;
15
import com.spice.profitmandi.dao.model.AddCartRequest;
36376 aman 16
import com.spice.profitmandi.dao.model.CartItem;
17
import com.spice.profitmandi.dao.model.UserCart;
18
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
19
import com.spice.profitmandi.dao.repository.catalog.TagListingRepository;
20
import com.spice.profitmandi.dao.repository.dtr.UserAccountRepository;
21
import com.spice.profitmandi.dao.repository.user.CartLineRepository;
22
import com.spice.profitmandi.service.scheme.SchemeService;
36321 vikas 23
import com.spice.profitmandi.web.controller.CartController;
24
import com.spice.profitmandi.web.v2.response.ApiResponse;
25
import org.springframework.beans.factory.annotation.Autowired;
36376 aman 26
import org.springframework.http.HttpStatus;
36321 vikas 27
import org.springframework.http.ResponseEntity;
28
import org.springframework.ui.Model;
29
import org.springframework.web.bind.annotation.*;
30
 
31
import javax.servlet.http.HttpServletRequest;
36376 aman 32
import java.util.ArrayList;
33
import java.util.Arrays;
34
import java.util.HashSet;
35
import java.util.List;
36
import java.util.Map;
37
import java.util.Set;
38
import java.util.stream.Collectors;
36321 vikas 39
 
36376 aman 40
import org.springframework.transaction.annotation.Transactional;
41
 
36321 vikas 42
@RestController
43
@RequestMapping("/v2")
36376 aman 44
@Transactional(rollbackFor = Throwable.class)
36321 vikas 45
public class V2CartController extends V2BaseController {
46
 
47
    @Autowired
48
    private CartController cartController;
49
 
36376 aman 50
    @Autowired
51
    private CartValidationService cartValidationService;
52
 
53
    @Autowired
54
    private UserAccountRepository userAccountRepository;
55
 
56
    @Autowired
57
    private CartService cartService;
58
 
59
    @Autowired
60
    private CartLineRepository cartLineRepository;
61
 
62
    @Autowired
63
    private ItemRepository itemRepository;
64
 
65
    @Autowired
66
    private TagListingRepository tagListingRepository;
67
 
68
    @Autowired
69
    private SchemeService schemeService;
70
 
71
    @Autowired
72
    private com.spice.profitmandi.dao.repository.user.CartRepository cartRepository;
73
 
74
    private static final int MRP_TAG_ID = 4;
75
    private static final double CARRY_BAG_THRESHOLD = 12000d;
76
    private static final float CARRY_BAG_PRICE = 0.01f;
77
 
36321 vikas 78
    @GetMapping("/cart")
79
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
80
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode,
81
                                                      @RequestParam int bucketId) throws Throwable {
82
        return wrapResponse(cartController.validateCart(request, pincode, bucketId));
83
    }
84
 
85
    @PostMapping("/cart")
86
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
87
                                                      @RequestBody AddCartRequest addCartRequest,
88
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode) throws Throwable {
89
        return wrapResponse(cartController.validateCart(request, addCartRequest, pincode));
90
    }
91
 
92
    @PostMapping("/cart/changeAddress")
93
    public ResponseEntity<ApiResponse<?>> changeAddress(HttpServletRequest request,
94
                                                        @RequestParam(value = "addressId") long addressId) throws Throwable {
95
        return wrapResponse(cartController.changeAddress(request, addressId));
96
    }
97
 
98
    @GetMapping("/byPassRequests")
99
    public ResponseEntity<ApiResponse<?>> byPassRequests(HttpServletRequest request, Model model) throws Throwable {
100
        return wrapResponse(cartController.byPassRequests(request, model));
101
    }
102
 
103
    @PostMapping("/byPassRequestAction")
104
    public ResponseEntity<ApiResponse<?>> addAmountToWalletRequestRejected(HttpServletRequest request,
105
                                                                           @RequestParam(name = "id", defaultValue = "0") int id,
106
                                                                           @RequestParam ByPassRequestStatus status,
107
                                                                           @RequestParam String reason,
108
                                                                           Model model) throws Throwable {
109
        return wrapResponse(cartController.addAmountToWalletRequestRejected(request, id, status, reason, model));
110
    }
111
 
112
    @GetMapping("/cart/payment")
113
    public ResponseEntity<ApiResponse<?>> validateCartPayment(HttpServletRequest request,
114
                                                              @RequestParam(defaultValue = "0") int paymentId,
115
                                                              Model model) throws Throwable {
116
        return wrapResponse(cartController.validateCartPayment(request, paymentId, model));
117
    }
118
 
119
    @GetMapping("/partner/hidAllocation")
120
    public ResponseEntity<ApiResponse<?>> getItemHidAllocation(HttpServletRequest request) throws Throwable {
121
        return wrapResponse(cartController.getItemHidAllocation(request));
122
    }
36376 aman 123
 
124
    // =================================================================
125
    // Cart v2 redesign — two-step validation (open + checkout)
126
    // All endpoints require authentication. Guests are rejected with 401.
127
    // =================================================================
128
 
129
    /** STEP 1 — soft validation. Never blocks. Surfaces warnings for price drift,
130
     *  qty downgrade, OOS, unavailability. Refreshes last-seen price baseline
131
     *  used by Step 2 drift detection.
132
     *  Pincode is derived from the cart's bound address server-side; clients
133
     *  do not pass it. */
134
    @GetMapping("/cart/open")
135
    public ResponseEntity<?> openCart(HttpServletRequest request)
136
            throws ProfitMandiBusinessException {
137
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
138
        if (!auth.isPresent()) return unauthorized();
139
        AuthCtx ctx = auth.get();
140
        OpenCartValidationResult result = cartValidationService.validateForOpen(
141
                ctx.cartId, ctx.storeId);
142
        return ResponseEntity.ok(ApiResponse.success(result));
143
    }
144
 
145
    /** STEP 2 — hard validation immediately before payment. Returns
146
     *  {@code valid=false} on any drift; user must hit {@code /cart/open}
147
     *  to re-confirm, then retry this endpoint. On pass, creates a 240s
148
     *  stock reservation and returns a {@code reservationId}.
149
     *  Procurement only — tertiary billing drafts check out via
150
     *  /v2/billing/drafts/{cartId}/commit. */
151
    @PostMapping("/cart/validate")
152
    public ResponseEntity<?> validateForCheckout(HttpServletRequest request,
153
                                                 @RequestParam(value = "addressId") long addressId)
154
            throws ProfitMandiBusinessException {
155
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
156
        if (!auth.isPresent()) return unauthorized();
157
        AuthCtx ctx = auth.get();
158
        CheckoutValidationResult result = cartValidationService.validateForCheckout(
159
                ctx.cartId, ctx.userId, ctx.storeId, addressId, SaleType.PARTNER_PROCUREMENT);
160
        return ResponseEntity.ok(ApiResponse.success(result));
161
    }
162
 
163
    // -----------------------------------------------------------------
164
    // Cart mutations — productId-only payload. All return the freshly
165
    // hydrated cart (Step 1 result) so the client never keeps stale state.
166
    // -----------------------------------------------------------------
167
 
168
    /** Add a single product to the cart. Increments qty if already present. */
169
    @PostMapping("/cart/items")
170
    public ResponseEntity<?> addItem(HttpServletRequest request,
171
                                     @RequestBody AddCartItemRequest body)
172
            throws ProfitMandiBusinessException {
173
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
174
        if (!auth.isPresent()) return unauthorized();
175
        AuthCtx ctx = auth.get();
176
        if (body == null || body.productId <= 0 || body.quantity <= 0) {
177
            throw new ProfitMandiBusinessException("body", body, "CART_ITEM_INVALID_PAYLOAD");
178
        }
179
        List<CartItem> desired = currentLinesAsItems(ctx.cartId);
180
        boolean found = false;
181
        for (CartItem existing : desired) {
182
            if (existing.getItemId() == body.productId) {
183
                existing.setQuantity(existing.getQuantity() + body.quantity);
184
                found = true;
185
                break;
186
            }
187
        }
188
        if (!found) {
189
            desired.add(new CartItem(body.quantity, body.productId));
190
        }
191
        return applyAndHydrate(ctx, desired);
192
    }
193
 
194
    /** Set a specific line's quantity. quantity=0 removes the line. */
195
    @PatchMapping("/cart/items/{productId}")
196
    public ResponseEntity<?> updateQuantity(HttpServletRequest request,
197
                                            @PathVariable int productId,
198
                                            @RequestParam int quantity)
199
            throws ProfitMandiBusinessException {
200
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
201
        if (!auth.isPresent()) return unauthorized();
202
        AuthCtx ctx = auth.get();
203
        if (quantity < 0) {
204
            throw new ProfitMandiBusinessException("quantity", quantity, "CART_ITEM_NEGATIVE_QTY");
205
        }
206
        List<CartItem> desired = currentLinesAsItems(ctx.cartId);
207
        if (quantity == 0) {
208
            desired = desired.stream()
209
                    .filter(ci -> ci.getItemId() != productId)
210
                    .collect(Collectors.toList());
211
        } else {
212
            boolean found = false;
213
            for (CartItem existing : desired) {
214
                if (existing.getItemId() == productId) {
215
                    existing.setQuantity(quantity);
216
                    found = true;
217
                    break;
218
                }
219
            }
220
            if (!found) {
221
                desired.add(new CartItem(quantity, productId));
222
            }
223
        }
224
        return applyAndHydrate(ctx, desired);
225
    }
226
 
227
    /** Remove a single line by productId. */
228
    @DeleteMapping("/cart/items/{productId}")
229
    public ResponseEntity<?> removeItem(HttpServletRequest request,
230
                                        @PathVariable int productId)
231
            throws ProfitMandiBusinessException {
232
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
233
        if (!auth.isPresent()) return unauthorized();
234
        AuthCtx ctx = auth.get();
235
        List<CartItem> desired = currentLinesAsItems(ctx.cartId).stream()
236
                .filter(ci -> ci.getItemId() != productId)
237
                .collect(Collectors.toList());
238
        return applyAndHydrate(ctx, desired);
239
    }
240
 
241
    /** Clear the entire cart. */
242
    @DeleteMapping("/cart")
243
    public ResponseEntity<?> clearCart(HttpServletRequest request)
244
            throws ProfitMandiBusinessException {
245
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
246
        if (!auth.isPresent()) return unauthorized();
247
        AuthCtx ctx = auth.get();
248
        cartService.clearCart(ctx.cartId);
249
        return ResponseEntity.ok(ApiResponse.success(
250
                cartValidationService.validateForOpen(ctx.cartId, ctx.storeId)));
251
    }
252
 
253
    /**
254
     * Enriches each desired CartItem with the live selling price, runs the
255
     * procurement carry-bag rebalance, applies the change, then returns a
256
     * hydrated Step-1 view. /v2/cart/* is procurement-only; tertiary drafts
257
     * have their own controller.
258
     */
259
    private ResponseEntity<?> applyAndHydrate(AuthCtx ctx, List<CartItem> desired)
260
            throws ProfitMandiBusinessException {
261
        enrichPrices(ctx, desired);
262
        rebalanceCarryBag(desired);
263
        cartService.addItemsToCart(ctx.cartId, desired);
264
        return ResponseEntity.ok(ApiResponse.success(
265
                cartValidationService.validateForOpen(ctx.cartId, ctx.storeId)));
266
    }
267
 
268
    /**
269
     * Populates {@link CartItem#setSellingPrice(double)} for each non-carry-bag item
270
     * with the live (MOP - scheme cashback) figure. Preserves the existing value on
271
     * lookup miss so a partial-Solr outage doesn't zero out prices.
272
     */
273
    private void enrichPrices(AuthCtx ctx, List<CartItem> desired) throws ProfitMandiBusinessException {
274
        Set<Integer> itemIds = desired.stream()
275
                .filter(ci -> ci.getItemId() != ProfitMandiConstants.ITEM_CARRY_BAG)
276
                .map(CartItem::getItemId)
277
                .collect(Collectors.toSet());
278
        if (itemIds.isEmpty()) return;
279
 
280
        Map<Integer, TagListing> tagByItem = tagListingRepository
281
                .selectByItemIdsAndTagIds(itemIds, new HashSet<>(Arrays.asList(MRP_TAG_ID)))
282
                .stream()
283
                .collect(Collectors.toMap(TagListing::getItemId, x -> x, (a, b) -> a));
284
        Map<Integer, Item> items = itemRepository.selectByIds(itemIds).stream()
285
                .collect(Collectors.toMap(Item::getId, x -> x, (a, b) -> a));
286
        List<Integer> catalogIds = items.values().stream()
287
                .map(Item::getCatalogItemId).distinct().collect(Collectors.toList());
288
        Map<Integer, Float> cashbackByCatalog = schemeService.getCatalogSchemeCashBack(ctx.storeId, catalogIds);
289
        if (cashbackByCatalog == null) cashbackByCatalog = java.util.Collections.emptyMap();
290
 
291
        for (CartItem ci : desired) {
292
            if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;
293
            TagListing tl = tagByItem.get(ci.getItemId());
294
            Item it = items.get(ci.getItemId());
295
            if (tl == null || it == null) continue;
296
            float cashback = cashbackByCatalog.getOrDefault(it.getCatalogItemId(), 0f);
297
            ci.setSellingPrice(tl.getMop() - cashback);
298
        }
299
    }
300
 
301
    /**
302
     * Server-side port of the legacy Zustand carry-bag rule: any item with
303
     * sellingPrice > ₹12,000 needs one carry bag per unit. Runs BEFORE addItemsToCart
304
     * so the full-sync semantics keep the cart consistent in one round-trip.
305
     */
306
    private void rebalanceCarryBag(List<CartItem> desired) {
307
        int bagsNeeded = 0;
308
        for (CartItem ci : desired) {
309
            if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;
310
            if (ci.getSellingPrice() > CARRY_BAG_THRESHOLD) {
311
                bagsNeeded += ci.getQuantity();
312
            }
313
        }
314
        desired.removeIf(ci -> ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG);
315
        if (bagsNeeded > 0) {
316
            CartItem bag = new CartItem(bagsNeeded, ProfitMandiConstants.ITEM_CARRY_BAG);
317
            bag.setSellingPrice(CARRY_BAG_PRICE);
318
            desired.add(bag);
319
        }
320
    }
321
 
322
    private List<CartItem> currentLinesAsItems(int cartId) {
323
        List<CartLine> lines = cartLineRepository.selectAllByCart(cartId);
324
        if (lines == null) return new ArrayList<>();
325
        return lines.stream()
326
                .map(l -> {
327
                    CartItem ci = new CartItem(l.getQuantity(), l.getItemId());
328
                    ci.setSellingPrice(l.getActualPrice());
329
                    return ci;
330
                })
331
                .collect(Collectors.toList());
332
    }
333
 
334
    public static class AddCartItemRequest {
335
        public int productId;
336
        public int quantity;
337
    }
338
 
339
    // -----------------------------------------------------------------
340
    // Auth helpers — login is mandatory per product decision (2026-04-21).
341
    // Guest carts are not supported; unauthenticated requests get 401.
342
    //
343
    // Returns the auth context as an Optional rather than throwing, because
344
    // GlobalExceptionHandler's @ExceptionHandler(Exception.class) would
345
    // otherwise wrap any RuntimeException as HTTP 500 regardless of
346
    // @ResponseStatus annotations on the exception class.
347
    // -----------------------------------------------------------------
348
 
349
    private static final class AuthCtx {
350
        final int userId;
351
        final int storeId;
352
        final int cartId;
353
        AuthCtx(int userId, int storeId, int cartId) {
354
            this.userId = userId; this.storeId = storeId; this.cartId = cartId;
355
        }
356
    }
357
 
358
    /**
359
     * Resolves the authenticated partner's PROCUREMENT cart. v2/cart/* is
360
     * procurement-only. Tertiary billing drafts live under /v2/billing/* and
361
     * are resolved per-cartId there.
362
     */
363
    private java.util.Optional<AuthCtx> resolveAuth(HttpServletRequest request) {
364
        Object userIdAttr = request.getAttribute("userId");
365
        if (userIdAttr == null) {
366
            return java.util.Optional.empty();
367
        }
368
        int userId;
369
        try {
370
            userId = (int) userIdAttr;
371
        } catch (ClassCastException e) {
372
            return java.util.Optional.empty();
373
        }
374
        if (userId <= 0) {
375
            return java.util.Optional.empty();
376
        }
377
        Object userInfoAttr = request.getAttribute("userInfo");
378
        int storeId = userInfoAttr instanceof UserInfo ? ((UserInfo) userInfoAttr).getRetailerId() : 0;
379
 
380
        UserCart uc = userAccountRepository.getUserCart(userId);
381
        if (uc == null || uc.getCartId() <= 0) {
382
            return java.util.Optional.empty();
383
        }
384
        return java.util.Optional.of(new AuthCtx(userId, storeId, uc.getCartId()));
385
    }
386
 
387
    private ResponseEntity<?> unauthorized() {
388
        java.util.Map<String, Object> body = new java.util.HashMap<>();
389
        body.put("responseStatus", "FAILURE");
390
        body.put("statusCode", 401);
391
        body.put("statusMessage", "LOGIN_REQUIRED");
392
        body.put("message", "Authentication is required. Send a valid Auth-Token header.");
393
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
394
    }
36321 vikas 395
}