Subversion Repositories SmartDukaan

Rev

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