Subversion Repositories SmartDukaan

Rev

Rev 37305 | 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;
37084 ranu 9
import com.spice.profitmandi.dao.cart.v2.HydratedCart;
10
import com.spice.profitmandi.dao.cart.v2.HydratedLine;
11
import com.spice.profitmandi.dao.cart.v2.LineStatus;
36376 aman 12
import com.spice.profitmandi.dao.cart.v2.OpenCartValidationResult;
37084 ranu 13
import com.spice.profitmandi.dao.cart.v2.PricingBreakup;
36376 aman 14
import com.spice.profitmandi.dao.cart.v2.SaleType;
15
import com.spice.profitmandi.dao.entity.catalog.Item;
16
import com.spice.profitmandi.dao.entity.catalog.TagListing;
37014 ranu 17
import com.spice.profitmandi.dao.entity.fofo.PartnerType;
36376 aman 18
import com.spice.profitmandi.dao.entity.user.CartLine;
36321 vikas 19
import com.spice.profitmandi.dao.enumuration.catalog.ByPassRequestStatus;
37014 ranu 20
import com.spice.profitmandi.dao.repository.fofo.PartnerTypeChangeService;
36321 vikas 21
import com.spice.profitmandi.dao.model.AddCartRequest;
36376 aman 22
import com.spice.profitmandi.dao.model.CartItem;
23
import com.spice.profitmandi.dao.model.UserCart;
24
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
25
import com.spice.profitmandi.dao.repository.catalog.TagListingRepository;
26
import com.spice.profitmandi.dao.repository.dtr.UserAccountRepository;
27
import com.spice.profitmandi.dao.repository.user.CartLineRepository;
28
import com.spice.profitmandi.service.scheme.SchemeService;
36321 vikas 29
import com.spice.profitmandi.web.controller.CartController;
30
import com.spice.profitmandi.web.v2.response.ApiResponse;
31
import org.springframework.beans.factory.annotation.Autowired;
36376 aman 32
import org.springframework.http.HttpStatus;
36321 vikas 33
import org.springframework.http.ResponseEntity;
34
import org.springframework.ui.Model;
35
import org.springframework.web.bind.annotation.*;
36
 
37
import javax.servlet.http.HttpServletRequest;
37084 ranu 38
import java.math.BigDecimal;
39
import java.math.RoundingMode;
37014 ranu 40
import java.time.LocalDate;
36376 aman 41
import java.util.ArrayList;
42
import java.util.Arrays;
43
import java.util.HashSet;
44
import java.util.List;
45
import java.util.Map;
46
import java.util.Set;
47
import java.util.stream.Collectors;
36321 vikas 48
 
36376 aman 49
import org.springframework.transaction.annotation.Transactional;
50
 
36321 vikas 51
@RestController
52
@RequestMapping("/v2")
36376 aman 53
@Transactional(rollbackFor = Throwable.class)
36321 vikas 54
public class V2CartController extends V2BaseController {
55
 
56
    @Autowired
57
    private CartController cartController;
58
 
36376 aman 59
    @Autowired
60
    private CartValidationService cartValidationService;
61
 
62
    @Autowired
63
    private UserAccountRepository userAccountRepository;
64
 
65
    @Autowired
66
    private CartService cartService;
67
 
68
    @Autowired
69
    private CartLineRepository cartLineRepository;
70
 
71
    @Autowired
72
    private ItemRepository itemRepository;
73
 
74
    @Autowired
75
    private TagListingRepository tagListingRepository;
76
 
77
    @Autowired
78
    private SchemeService schemeService;
79
 
80
    @Autowired
81
    private com.spice.profitmandi.dao.repository.user.CartRepository cartRepository;
82
 
37014 ranu 83
    @Autowired
84
    private PartnerTypeChangeService partnerTypeChangeService;
85
 
36376 aman 86
    private static final int MRP_TAG_ID = 4;
87
    private static final double CARRY_BAG_THRESHOLD = 12000d;
37305 ranu 88
    // All tiers except Bronze get flat ₹1 (Platinum/Diamond/Gold/Silver/
89
    // Rising Star). Bronze pays MRP from TagListing. See resolveCarryBagPrice.
37014 ranu 90
    private static final float CARRY_BAG_PREMIUM_PRICE = 1f;
36376 aman 91
 
36321 vikas 92
    @GetMapping("/cart")
93
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
94
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode,
95
                                                      @RequestParam int bucketId) throws Throwable {
96
        return wrapResponse(cartController.validateCart(request, pincode, bucketId));
97
    }
98
 
99
    @PostMapping("/cart")
100
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
101
                                                      @RequestBody AddCartRequest addCartRequest,
102
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode) throws Throwable {
103
        return wrapResponse(cartController.validateCart(request, addCartRequest, pincode));
104
    }
105
 
106
    @PostMapping("/cart/changeAddress")
107
    public ResponseEntity<ApiResponse<?>> changeAddress(HttpServletRequest request,
108
                                                        @RequestParam(value = "addressId") long addressId) throws Throwable {
109
        return wrapResponse(cartController.changeAddress(request, addressId));
110
    }
111
 
112
    @GetMapping("/byPassRequests")
113
    public ResponseEntity<ApiResponse<?>> byPassRequests(HttpServletRequest request, Model model) throws Throwable {
114
        return wrapResponse(cartController.byPassRequests(request, model));
115
    }
116
 
117
    @PostMapping("/byPassRequestAction")
118
    public ResponseEntity<ApiResponse<?>> addAmountToWalletRequestRejected(HttpServletRequest request,
119
                                                                           @RequestParam(name = "id", defaultValue = "0") int id,
120
                                                                           @RequestParam ByPassRequestStatus status,
121
                                                                           @RequestParam String reason,
122
                                                                           Model model) throws Throwable {
123
        return wrapResponse(cartController.addAmountToWalletRequestRejected(request, id, status, reason, model));
124
    }
125
 
126
    @GetMapping("/partner/hidAllocation")
127
    public ResponseEntity<ApiResponse<?>> getItemHidAllocation(HttpServletRequest request) throws Throwable {
128
        return wrapResponse(cartController.getItemHidAllocation(request));
129
    }
36376 aman 130
 
131
    // =================================================================
132
    // Cart v2 redesign — two-step validation (open + checkout)
133
    // All endpoints require authentication. Guests are rejected with 401.
134
    // =================================================================
135
 
136
    /** STEP 1 — soft validation. Never blocks. Surfaces warnings for price drift,
137
     *  qty downgrade, OOS, unavailability. Refreshes last-seen price baseline
138
     *  used by Step 2 drift detection.
139
     *  Pincode is derived from the cart's bound address server-side; clients
140
     *  do not pass it. */
141
    @GetMapping("/cart/open")
142
    public ResponseEntity<?> openCart(HttpServletRequest request)
143
            throws ProfitMandiBusinessException {
144
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 145
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 146
        AuthCtx ctx = auth.get();
37084 ranu 147
        OpenCartValidationResult result = validateAndApplyCarryBagPrice(
36376 aman 148
                ctx.cartId, ctx.storeId);
149
        return ResponseEntity.ok(ApiResponse.success(result));
150
    }
151
 
152
    /** STEP 2 — hard validation immediately before payment. Returns
153
     *  {@code valid=false} on any drift; user must hit {@code /cart/open}
154
     *  to re-confirm, then retry this endpoint. On pass, creates a 240s
155
     *  stock reservation and returns a {@code reservationId}.
156
     *  Procurement only — tertiary billing drafts check out via
157
     *  /v2/billing/drafts/{cartId}/commit. */
158
    @PostMapping("/cart/validate")
159
    public ResponseEntity<?> validateForCheckout(HttpServletRequest request,
160
                                                 @RequestParam(value = "addressId") long addressId)
161
            throws ProfitMandiBusinessException {
162
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 163
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 164
        AuthCtx ctx = auth.get();
165
        CheckoutValidationResult result = cartValidationService.validateForCheckout(
166
                ctx.cartId, ctx.userId, ctx.storeId, addressId, SaleType.PARTNER_PROCUREMENT);
167
        return ResponseEntity.ok(ApiResponse.success(result));
168
    }
169
 
170
    // -----------------------------------------------------------------
171
    // Cart mutations — productId-only payload. All return the freshly
172
    // hydrated cart (Step 1 result) so the client never keeps stale state.
173
    // -----------------------------------------------------------------
174
 
175
    /** Add a single product to the cart. Increments qty if already present. */
176
    @PostMapping("/cart/items")
177
    public ResponseEntity<?> addItem(HttpServletRequest request,
178
                                     @RequestBody AddCartItemRequest body)
179
            throws ProfitMandiBusinessException {
180
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 181
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 182
        AuthCtx ctx = auth.get();
183
        if (body == null || body.productId <= 0 || body.quantity <= 0) {
184
            throw new ProfitMandiBusinessException("body", body, "CART_ITEM_INVALID_PAYLOAD");
185
        }
186
        List<CartItem> desired = currentLinesAsItems(ctx.cartId);
187
        boolean found = false;
188
        for (CartItem existing : desired) {
189
            if (existing.getItemId() == body.productId) {
190
                existing.setQuantity(existing.getQuantity() + body.quantity);
191
                found = true;
192
                break;
193
            }
194
        }
195
        if (!found) {
196
            desired.add(new CartItem(body.quantity, body.productId));
197
        }
198
        return applyAndHydrate(ctx, desired);
199
    }
200
 
201
    /** Set a specific line's quantity. quantity=0 removes the line. */
36479 ranu 202
    @PostMapping("/cart/items/{productId}")
36376 aman 203
    public ResponseEntity<?> updateQuantity(HttpServletRequest request,
204
                                            @PathVariable int productId,
205
                                            @RequestParam int quantity)
206
            throws ProfitMandiBusinessException {
207
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 208
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 209
        AuthCtx ctx = auth.get();
210
        if (quantity < 0) {
211
            throw new ProfitMandiBusinessException("quantity", quantity, "CART_ITEM_NEGATIVE_QTY");
212
        }
213
        List<CartItem> desired = currentLinesAsItems(ctx.cartId);
214
        if (quantity == 0) {
215
            desired = desired.stream()
216
                    .filter(ci -> ci.getItemId() != productId)
217
                    .collect(Collectors.toList());
218
        } else {
219
            boolean found = false;
220
            for (CartItem existing : desired) {
221
                if (existing.getItemId() == productId) {
222
                    existing.setQuantity(quantity);
223
                    found = true;
224
                    break;
225
                }
226
            }
227
            if (!found) {
228
                desired.add(new CartItem(quantity, productId));
229
            }
230
        }
36479 ranu 231
        // Skip carry bag rebalance if user explicitly changed carry bag qty
232
        if (productId == ProfitMandiConstants.ITEM_CARRY_BAG) {
233
            enrichPrices(ctx, desired);
234
            cartService.addItemsToCart(ctx.cartId, desired);
235
            return ResponseEntity.ok(ApiResponse.success(
37084 ranu 236
                    validateAndApplyCarryBagPrice(ctx.cartId, ctx.storeId)));
36479 ranu 237
        }
36376 aman 238
        return applyAndHydrate(ctx, desired);
239
    }
240
 
241
    /** Remove a single line by productId. */
242
    @DeleteMapping("/cart/items/{productId}")
243
    public ResponseEntity<?> removeItem(HttpServletRequest request,
244
                                        @PathVariable int productId)
245
            throws ProfitMandiBusinessException {
246
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 247
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 248
        AuthCtx ctx = auth.get();
249
        List<CartItem> desired = currentLinesAsItems(ctx.cartId).stream()
250
                .filter(ci -> ci.getItemId() != productId)
251
                .collect(Collectors.toList());
36479 ranu 252
        // Skip carry bag rebalance if user explicitly removed the carry bag
253
        if (productId == ProfitMandiConstants.ITEM_CARRY_BAG) {
254
            enrichPrices(ctx, desired);
255
            cartService.addItemsToCart(ctx.cartId, desired);
256
            return ResponseEntity.ok(ApiResponse.success(
37084 ranu 257
                    validateAndApplyCarryBagPrice(ctx.cartId, ctx.storeId)));
36479 ranu 258
        }
36376 aman 259
        return applyAndHydrate(ctx, desired);
260
    }
261
 
262
    /** Clear the entire cart. */
263
    @DeleteMapping("/cart")
264
    public ResponseEntity<?> clearCart(HttpServletRequest request)
265
            throws ProfitMandiBusinessException {
266
        java.util.Optional<AuthCtx> auth = resolveAuth(request);
37157 amit 267
        if (!auth.isPresent()) return authFailureResponse(request);
36376 aman 268
        AuthCtx ctx = auth.get();
269
        cartService.clearCart(ctx.cartId);
270
        return ResponseEntity.ok(ApiResponse.success(
37084 ranu 271
                validateAndApplyCarryBagPrice(ctx.cartId, ctx.storeId)));
36376 aman 272
    }
273
 
274
    /**
275
     * Enriches each desired CartItem with the live selling price, runs the
276
     * procurement carry-bag rebalance, applies the change, then returns a
277
     * hydrated Step-1 view. /v2/cart/* is procurement-only; tertiary drafts
278
     * have their own controller.
279
     */
280
    private ResponseEntity<?> applyAndHydrate(AuthCtx ctx, List<CartItem> desired)
281
            throws ProfitMandiBusinessException {
282
        enrichPrices(ctx, desired);
37014 ranu 283
        rebalanceCarryBag(desired, ctx.storeId);
36376 aman 284
        cartService.addItemsToCart(ctx.cartId, desired);
285
        return ResponseEntity.ok(ApiResponse.success(
37084 ranu 286
                validateAndApplyCarryBagPrice(ctx.cartId, ctx.storeId)));
36376 aman 287
    }
288
 
289
    /**
290
     * Populates {@link CartItem#setSellingPrice(double)} for each non-carry-bag item
291
     * with the live (MOP - scheme cashback) figure. Preserves the existing value on
292
     * lookup miss so a partial-Solr outage doesn't zero out prices.
293
     */
294
    private void enrichPrices(AuthCtx ctx, List<CartItem> desired) throws ProfitMandiBusinessException {
295
        Set<Integer> itemIds = desired.stream()
296
                .filter(ci -> ci.getItemId() != ProfitMandiConstants.ITEM_CARRY_BAG)
297
                .map(CartItem::getItemId)
298
                .collect(Collectors.toSet());
299
        if (itemIds.isEmpty()) return;
300
 
301
        Map<Integer, TagListing> tagByItem = tagListingRepository
302
                .selectByItemIdsAndTagIds(itemIds, new HashSet<>(Arrays.asList(MRP_TAG_ID)))
303
                .stream()
304
                .collect(Collectors.toMap(TagListing::getItemId, x -> x, (a, b) -> a));
305
        Map<Integer, Item> items = itemRepository.selectByIds(itemIds).stream()
306
                .collect(Collectors.toMap(Item::getId, x -> x, (a, b) -> a));
307
        List<Integer> catalogIds = items.values().stream()
308
                .map(Item::getCatalogItemId).distinct().collect(Collectors.toList());
309
        Map<Integer, Float> cashbackByCatalog = schemeService.getCatalogSchemeCashBack(ctx.storeId, catalogIds);
310
        if (cashbackByCatalog == null) cashbackByCatalog = java.util.Collections.emptyMap();
311
 
312
        for (CartItem ci : desired) {
313
            if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;
314
            TagListing tl = tagByItem.get(ci.getItemId());
315
            Item it = items.get(ci.getItemId());
316
            if (tl == null || it == null) continue;
317
            float cashback = cashbackByCatalog.getOrDefault(it.getCatalogItemId(), 0f);
318
            ci.setSellingPrice(tl.getMop() - cashback);
319
        }
320
    }
321
 
322
    /**
323
     * Server-side port of the legacy Zustand carry-bag rule: any item with
324
     * sellingPrice > ₹12,000 needs one carry bag per unit. Runs BEFORE addItemsToCart
325
     * so the full-sync semantics keep the cart consistent in one round-trip.
37015 ranu 326
     *
327
     * Pricing mirrors {@code CartServiceImpl.getCartValidation} (lines 271-278):
328
     *   PLATINUM / DIAMOND / GOLD / NEW (Rising Star) -> ₹1 flat
329
     *   SILVER / BRONZE                              -> MRP from carry-bag TagListing
36376 aman 330
     */
37015 ranu 331
    private void rebalanceCarryBag(List<CartItem> desired, int fofoId) {
36376 aman 332
        int bagsNeeded = 0;
333
        for (CartItem ci : desired) {
334
            if (ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;
335
            if (ci.getSellingPrice() > CARRY_BAG_THRESHOLD) {
336
                bagsNeeded += ci.getQuantity();
337
            }
338
        }
339
        desired.removeIf(ci -> ci.getItemId() == ProfitMandiConstants.ITEM_CARRY_BAG);
340
        if (bagsNeeded > 0) {
341
            CartItem bag = new CartItem(bagsNeeded, ProfitMandiConstants.ITEM_CARRY_BAG);
37015 ranu 342
            bag.setSellingPrice(resolveCarryBagPrice(fofoId));
36376 aman 343
            desired.add(bag);
344
        }
345
    }
346
 
37084 ranu 347
    /**
348
     * Wraps {@link CartValidationService#validateForOpen} and overrides the
349
     * carry-bag line's LivePrice + rolls up totals using the tier logic that
350
     * v1's {@code CartServiceImpl.getCartValidation} already applies (lines
351
     * 271-278). Needed because the shared v2 hydrator prices every line from
352
     * {@code TagListing.getMop()} without a tier check, so premium partners
353
     * would otherwise see MRP (₹15) instead of ₹1.
354
     */
355
    private OpenCartValidationResult validateAndApplyCarryBagPrice(int cartId, int fofoId)
356
            throws ProfitMandiBusinessException {
357
        OpenCartValidationResult result = cartValidationService.validateForOpen(cartId, fofoId);
358
        if (result == null || result.getCart() == null || result.getCart().getLines() == null) {
359
            return result;
360
        }
361
        HydratedCart cart = result.getCart();
37094 ranu 362
 
363
        // 1. Overwrite every line's price from TagListing.sellingPrice — this is
364
        //    the same source v1's CartServiceImpl.getCartValidation uses to set
365
        //    cart_line.actual_price, i.e. the price the order will actually be
366
        //    charged at. The v2 hydrator picks tagListing.mop instead, which
367
        //    diverges from sellingPrice on scheme-discounted models (e.g. Moto
368
        //    G67 showed ₹22,999 mop vs ₹19,907 sellingPrice). Sync client
369
        //    display to server-of-truth so the partner sees what they'll pay.
370
        Set<Integer> lineItemIds = cart.getLines().stream()
371
                .filter(l -> l.getProductId() != ProfitMandiConstants.ITEM_CARRY_BAG)
372
                .map(HydratedLine::getProductId)
373
                .collect(Collectors.toSet());
374
        Map<Integer, Float> sellingByItemId = new java.util.HashMap<>();
375
        if (!lineItemIds.isEmpty()) {
376
            List<TagListing> tls = tagListingRepository.selectByItemIdsAndTagIds(
377
                    lineItemIds, new HashSet<>(Arrays.asList(MRP_TAG_ID)));
378
            for (TagListing tl : tls) {
379
                sellingByItemId.putIfAbsent(tl.getItemId(), tl.getSellingPrice());
380
            }
381
        }
382
        boolean overridden = false;
383
        for (HydratedLine line : cart.getLines()) {
384
            if (line.getProductId() == ProfitMandiConstants.ITEM_CARRY_BAG) continue;
385
            if (line.getPrice() == null) continue;
386
            Float livePrice = sellingByItemId.get(line.getProductId());
387
            if (livePrice == null) continue;
388
            BigDecimal newPrice = BigDecimal.valueOf(livePrice)
389
                    .setScale(2, RoundingMode.HALF_UP);
390
            if (line.getPrice().getSelling() == null
391
                    || line.getPrice().getSelling().compareTo(newPrice) != 0) {
392
                line.getPrice().setMrp(newPrice);
393
                line.getPrice().setSelling(newPrice);
394
                line.getPrice().setSchemeCashback(BigDecimal.ZERO);
395
                overridden = true;
396
            }
397
        }
398
 
399
        // 2. Carry-bag tier logic (₹1 for premium, MOP for Silver/Bronze).
37084 ranu 400
        BigDecimal tierPrice = BigDecimal.valueOf(resolveCarryBagPrice(fofoId))
401
                .setScale(2, RoundingMode.HALF_UP);
402
        for (HydratedLine line : cart.getLines()) {
403
            if (line.getProductId() != ProfitMandiConstants.ITEM_CARRY_BAG) continue;
404
            if (line.getPrice() == null) continue;
405
            if (line.getPrice().getSelling() == null
406
                    || line.getPrice().getSelling().compareTo(tierPrice) != 0) {
407
                line.getPrice().setMrp(tierPrice);
408
                line.getPrice().setSelling(tierPrice);
409
                line.getPrice().setSchemeCashback(BigDecimal.ZERO);
410
                overridden = true;
411
            }
412
        }
37094 ranu 413
 
37084 ranu 414
        if (overridden && cart.getPricing() != null) {
37094 ranu 415
            // Mirrors CartHydrationServiceImpl.computePricing so the grand total
416
            // reflects every overridden line price.
37084 ranu 417
            BigDecimal subtotal = BigDecimal.ZERO;
418
            BigDecimal discounts = BigDecimal.ZERO;
419
            BigDecimal insuranceTotal = BigDecimal.ZERO;
420
            for (HydratedLine line : cart.getLines()) {
421
                if (line.getStatus() != LineStatus.ACTIVE || line.getPrice() == null) continue;
422
                BigDecimal qty = BigDecimal.valueOf(line.getQuantity());
423
                subtotal = subtotal.add(line.getPrice().getMrp().multiply(qty));
424
                BigDecimal cashback = line.getPrice().getSchemeCashback() == null
425
                        ? BigDecimal.ZERO : line.getPrice().getSchemeCashback();
426
                discounts = discounts.add(cashback.multiply(qty));
427
                if (line.getInsurance() != null && line.getInsurance().getPremium() != null) {
428
                    insuranceTotal = insuranceTotal.add(line.getInsurance().getPremium());
429
                }
430
            }
431
            PricingBreakup p = cart.getPricing();
432
            p.setSubtotal(subtotal);
433
            p.setLineDiscounts(discounts);
434
            p.setTaxableBase(subtotal.subtract(discounts));
435
            p.setGrandTotal(p.getTaxableBase().add(insuranceTotal));
436
        }
437
        return result;
438
    }
439
 
37015 ranu 440
    private float resolveCarryBagPrice(int fofoId) {
441
        PartnerType tier = null;
442
        try {
443
            tier = partnerTypeChangeService.getTypeOnDate(fofoId, LocalDate.now());
444
        } catch (Exception e) {
445
            // fall through to premium price
446
        }
37304 ranu 447
        // Only Bronze pays the full TagListing price for the carry bag.
448
        // Every other tier (Silver, Gold, Diamond, Platinum, Rising Star, ALL)
449
        // gets the ₹1 premium price. Silver was previously grouped with Bronze
450
        // but has since been moved into the ₹1 bucket.
451
        if (tier == PartnerType.BRONZE) {
37015 ranu 452
            try {
453
                TagListing tl = tagListingRepository.selectByItemId(ProfitMandiConstants.ITEM_CARRY_BAG);
454
                if (tl != null) return tl.getSellingPrice();
455
            } catch (Exception e) {
456
                // fall through to premium price
457
            }
458
        }
459
        return CARRY_BAG_PREMIUM_PRICE;
460
    }
461
 
36376 aman 462
    private List<CartItem> currentLinesAsItems(int cartId) {
463
        List<CartLine> lines = cartLineRepository.selectAllByCart(cartId);
464
        if (lines == null) return new ArrayList<>();
465
        return lines.stream()
466
                .map(l -> {
467
                    CartItem ci = new CartItem(l.getQuantity(), l.getItemId());
468
                    ci.setSellingPrice(l.getActualPrice());
469
                    return ci;
470
                })
471
                .collect(Collectors.toList());
472
    }
473
 
474
    public static class AddCartItemRequest {
475
        public int productId;
476
        public int quantity;
477
    }
478
 
479
    // -----------------------------------------------------------------
480
    // Auth helpers — login is mandatory per product decision (2026-04-21).
481
    // Guest carts are not supported; unauthenticated requests get 401.
482
    //
483
    // Returns the auth context as an Optional rather than throwing, because
484
    // GlobalExceptionHandler's @ExceptionHandler(Exception.class) would
485
    // otherwise wrap any RuntimeException as HTTP 500 regardless of
486
    // @ResponseStatus annotations on the exception class.
487
    // -----------------------------------------------------------------
488
 
489
    private static final class AuthCtx {
490
        final int userId;
491
        final int storeId;
492
        final int cartId;
493
        AuthCtx(int userId, int storeId, int cartId) {
494
            this.userId = userId; this.storeId = storeId; this.cartId = cartId;
495
        }
496
    }
497
 
498
    /**
499
     * Resolves the authenticated partner's PROCUREMENT cart. v2/cart/* is
500
     * procurement-only. Tertiary billing drafts live under /v2/billing/* and
501
     * are resolved per-cartId there.
502
     */
503
    private java.util.Optional<AuthCtx> resolveAuth(HttpServletRequest request) {
504
        Object userIdAttr = request.getAttribute("userId");
505
        if (userIdAttr == null) {
506
            return java.util.Optional.empty();
507
        }
508
        int userId;
509
        try {
510
            userId = (int) userIdAttr;
511
        } catch (ClassCastException e) {
512
            return java.util.Optional.empty();
513
        }
514
        if (userId <= 0) {
515
            return java.util.Optional.empty();
516
        }
517
        Object userInfoAttr = request.getAttribute("userInfo");
518
        int storeId = userInfoAttr instanceof UserInfo ? ((UserInfo) userInfoAttr).getRetailerId() : 0;
519
 
520
        UserCart uc = userAccountRepository.getUserCart(userId);
521
        if (uc == null || uc.getCartId() <= 0) {
522
            return java.util.Optional.empty();
523
        }
524
        return java.util.Optional.of(new AuthCtx(userId, storeId, uc.getCartId()));
525
    }
526
 
37157 amit 527
    /**
528
     * Distinguishes a genuinely unauthenticated request from a valid login with
529
     * no procurement cart mapped (missing dtr.user_accounts cartId row). The
530
     * latter used to answer "Authentication is required", which mislabels a data
531
     * gap as an auth failure.
532
     */
533
    private ResponseEntity<?> authFailureResponse(HttpServletRequest request) {
534
        Object userIdAttr = request.getAttribute("userId");
535
        if (userIdAttr instanceof Integer && (int) userIdAttr > 0) {
536
            java.util.Map<String, Object> body = new java.util.HashMap<>();
537
            body.put("responseStatus", "FAILURE");
538
            body.put("statusCode", 404);
539
            body.put("statusMessage", "NO_PROCUREMENT_CART");
540
            body.put("message", "No procurement cart is mapped to this user. Contact support to fix the account linkage.");
541
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);
542
        }
543
        return unauthorized();
544
    }
545
 
36376 aman 546
    private ResponseEntity<?> unauthorized() {
547
        java.util.Map<String, Object> body = new java.util.HashMap<>();
548
        body.put("responseStatus", "FAILURE");
549
        body.put("statusCode", 401);
550
        body.put("statusMessage", "LOGIN_REQUIRED");
551
        body.put("message", "Authentication is required. Send a valid Auth-Token header.");
552
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(body);
553
    }
36321 vikas 554
}