Subversion Repositories SmartDukaan

Rev

Rev 36321 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 36321 Rev 36376
Line 1... Line 1...
1
package com.spice.profitmandi.web.v2.controller;
1
package com.spice.profitmandi.web.v2.controller;
2
 
2
 
-
 
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;
3
import com.spice.profitmandi.dao.enumuration.catalog.ByPassRequestStatus;
14
import com.spice.profitmandi.dao.enumuration.catalog.ByPassRequestStatus;
4
import com.spice.profitmandi.dao.model.AddCartRequest;
15
import com.spice.profitmandi.dao.model.AddCartRequest;
-
 
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;
5
import com.spice.profitmandi.web.controller.CartController;
23
import com.spice.profitmandi.web.controller.CartController;
6
import com.spice.profitmandi.web.v2.response.ApiResponse;
24
import com.spice.profitmandi.web.v2.response.ApiResponse;
7
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.beans.factory.annotation.Autowired;
-
 
26
import org.springframework.http.HttpStatus;
8
import org.springframework.http.ResponseEntity;
27
import org.springframework.http.ResponseEntity;
9
import org.springframework.ui.Model;
28
import org.springframework.ui.Model;
10
import org.springframework.web.bind.annotation.*;
29
import org.springframework.web.bind.annotation.*;
11
 
30
 
12
import javax.servlet.http.HttpServletRequest;
31
import javax.servlet.http.HttpServletRequest;
-
 
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;
-
 
39
 
-
 
40
import org.springframework.transaction.annotation.Transactional;
13
 
41
 
14
@RestController
42
@RestController
15
@RequestMapping("/v2")
43
@RequestMapping("/v2")
-
 
44
@Transactional(rollbackFor = Throwable.class)
16
public class V2CartController extends V2BaseController {
45
public class V2CartController extends V2BaseController {
17
 
46
 
18
    @Autowired
47
    @Autowired
19
    private CartController cartController;
48
    private CartController cartController;
20
 
49
 
-
 
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
 
21
    @GetMapping("/cart")
78
    @GetMapping("/cart")
22
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
79
    public ResponseEntity<ApiResponse<?>> validateCart(HttpServletRequest request,
23
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode,
80
                                                      @RequestParam(value = "pincode", defaultValue = "110001") String pincode,
24
                                                      @RequestParam int bucketId) throws Throwable {
81
                                                      @RequestParam int bucketId) throws Throwable {
25
        return wrapResponse(cartController.validateCart(request, pincode, bucketId));
82
        return wrapResponse(cartController.validateCart(request, pincode, bucketId));
Line 61... Line 118...
61
 
118
 
62
    @GetMapping("/partner/hidAllocation")
119
    @GetMapping("/partner/hidAllocation")
63
    public ResponseEntity<ApiResponse<?>> getItemHidAllocation(HttpServletRequest request) throws Throwable {
120
    public ResponseEntity<ApiResponse<?>> getItemHidAllocation(HttpServletRequest request) throws Throwable {
64
        return wrapResponse(cartController.getItemHidAllocation(request));
121
        return wrapResponse(cartController.getItemHidAllocation(request));
65
    }
122
    }
-
 
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
    }
66
}
395
}