Subversion Repositories SmartDukaan

Rev

Rev 37299 | Rev 37303 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
34474 aman.kumar 1
package com.spice.profitmandi.dao.service;
2
 
3
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
4
import com.spice.profitmandi.common.model.CustomPaymentOption;
5
import com.spice.profitmandi.dao.entity.catalog.Item;
6
import com.spice.profitmandi.dao.entity.dtr.ScratchOffer;
7
import com.spice.profitmandi.dao.entity.fofo.FofoOrder;
35221 aman 8
import com.spice.profitmandi.dao.entity.fofo.FofoOrderItem;
34474 aman.kumar 9
import com.spice.profitmandi.dao.entity.scratch.GiftEntity;
10
import com.spice.profitmandi.dao.entity.scratch.OfferEntity;
11
import com.spice.profitmandi.dao.entity.scratch.ScratchRequest;
12
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
13
import com.spice.profitmandi.dao.repository.dtr.ScratchOfferRepository;
14
import com.spice.profitmandi.dao.repository.fofo.FofoOrderItemRepository;
15
import com.spice.profitmandi.dao.repository.fofo.FofoOrderRepository;
16
import com.spice.profitmandi.dao.repository.scratch.GiftRepository;
17
import com.spice.profitmandi.dao.repository.scratch.SeasonalOfferRepository;
18
import org.apache.logging.log4j.LogManager;
19
import org.apache.logging.log4j.Logger;
20
import org.springframework.beans.factory.annotation.Autowired;
21
import org.springframework.stereotype.Service;
22
 
23
import java.time.LocalDate;
24
import java.time.LocalDateTime;
25
import java.time.LocalTime;
26
import java.util.*;
27
import java.util.function.Function;
28
import java.util.stream.Collectors;
29
 
30
@Service
31
public class ScratchService {
32
    private static final Logger logger = LogManager.getLogger(ScratchService.class);
33
    @Autowired
34
    SeasonalOfferRepository seasonalOfferRepository;
35
    @Autowired
36
    GiftRepository giftRepository;
37
    @Autowired
38
    ScratchOfferRepository scratchOfferRepository;
39
    @Autowired
40
    FofoOrderItemRepository fofoOrderItemRepository;
41
    @Autowired
42
    ItemRepository itemRepository;
43
    @Autowired
44
    FofoOrderRepository fofoOrderRepository;
35095 aman 45
 
46
 
34804 aman.kumar 47
    public void processScratchOffer(int fofoOrderId, Set<CustomPaymentOption> paymentOptions, Set<Integer> fofoOrderItemIds) throws ProfitMandiBusinessException {
35980 amit 48
        List<OfferEntity> activeOffers = seasonalOfferRepository.getOffersByDate(LocalDate.now());
49
        if (activeOffers.isEmpty()) {
50
            return;
51
        }
52
 
34474 aman.kumar 53
        FofoOrder fofoOrder = fofoOrderRepository.selectByOrderId(fofoOrderId);
35980 amit 54
        OfferEntity eligibleOffer = findEligibleOffer(activeOffers, fofoOrder);
55
        if (eligibleOffer == null) {
56
            logger.info("No eligible offer for order {}", fofoOrderId);
57
            return;
58
        }
34474 aman.kumar 59
 
35980 amit 60
        ScratchOffer availedOffer = scratchOfferRepository.getScractchOfferByCustomerIdAndOfferId(fofoOrder.getCustomerId(), eligibleOffer.getId());
61
        if (availedOffer != null) {
62
            logger.info("Customer {} already availed offer {}", fofoOrder.getCustomerId(), eligibleOffer.getId());
35095 aman 63
            return;
34474 aman.kumar 64
        }
65
 
35980 amit 66
        this.processOffer(eligibleOffer, fofoOrder);
34474 aman.kumar 67
    }
68
 
35980 amit 69
    private OfferEntity findEligibleOffer(List<OfferEntity> activeOffers, FofoOrder fofoOrder) {
34804 aman.kumar 70
        int customerFofoId = fofoOrder.getFofoId();
35095 aman 71
 
35980 amit 72
        // Check PartnerWise offers first
34474 aman.kumar 73
        for (OfferEntity activeOffer : activeOffers) {
35980 amit 74
            if (!"PartnerWise".equals(activeOffer.getClassification())) {
75
                continue;
76
            }
77
            List<GiftEntity> gifts = giftRepository.findByOfferId(activeOffer.getId());
78
            for (GiftEntity gift : gifts) {
79
                if (gift.getFofoStore() == null || gift.getFofoStore().trim().isEmpty()) {
35095 aman 80
                    continue;
81
                }
35980 amit 82
                boolean matches = Arrays.stream(gift.getFofoStore().split(","))
83
                        .map(String::trim)
84
                        .filter(s -> !s.isEmpty())
85
                        .anyMatch(store -> {
86
                            try {
87
                                return Integer.parseInt(store) == customerFofoId;
88
                            } catch (NumberFormatException e) {
89
                                return false;
90
                            }
91
                        });
92
                if (matches) {
93
                    return activeOffer;
34474 aman.kumar 94
                }
95
            }
96
        }
35095 aman 97
 
35980 amit 98
        // Fallback to first non-PartnerWise offer
99
        return activeOffers.stream()
34474 aman.kumar 100
                .filter(offer -> !"PartnerWise".equals(offer.getClassification()))
35980 amit 101
                .findFirst()
102
                .orElse(null);
34474 aman.kumar 103
    }
104
 
35095 aman 105
 
35271 aman 106
    private boolean checkGiftAvailability(LocalDate date, int perDayMaxQty, OfferEntity offer) {
35980 amit 107
        GiftEntity defaultGift = giftRepository.getDefaultGift(offer.getId());
37299 ranu 108
        // No default gift configured -> no default-gift daily cap to enforce.
109
        // Return true so the caller skips the fallback and proceeds to the picker.
110
        if (defaultGift == null) return true;
35980 amit 111
        Integer giftCounts = scratchOfferRepository.countTodaysGiftDistribution(date.atStartOfDay(), date.atTime(LocalTime.MAX), defaultGift.getId());
34474 aman.kumar 112
        return giftCounts < perDayMaxQty;
113
    }
114
 
35221 aman 115
    private boolean isAppleItem(FofoOrder fofoOrder) {
116
        List<FofoOrderItem> fofoOrderItems = fofoOrderItemRepository.selectByOrderId(fofoOrder.getId());
117
        if (fofoOrderItems == null || fofoOrderItems.isEmpty()) {
118
            return false;
119
        }
35980 amit 120
        return fofoOrderItems.stream()
121
                .anyMatch(item -> item != null && "apple".equalsIgnoreCase(item.getBrand()));
35221 aman 122
    }
35980 amit 123
 
124
    private boolean processOffer(OfferEntity offer, FofoOrder fofoOrder) {
125
        logger.info("Processing offer {} for order {}", offer.getId(), fofoOrder.getId());
126
 
35253 aman 127
        List<GiftEntity> allGifts = giftRepository.findNonDefaultGIft(offer.getId());
35980 amit 128
        GiftEntity defaultGift = giftRepository.getDefaultGift(offer.getId());
129
 
130
        if (allGifts.isEmpty()) {
131
            logger.warn("Offer {} has no gifts", offer.getId());
132
            return false;
133
        }
134
 
37299 ranu 135
        // Compute cart amount + item categories up-front so both the fast-paths (Apple,
136
        // daily-limit) and the main picker can consult them when deciding fallbacks.
137
        List<FofoOrderItem> orderItems = fofoOrderItemRepository.selectByOrderId(fofoOrder.getId());
138
        Set<Integer> itemCategoryIds = new HashSet<>();
139
        for (FofoOrderItem orderItem : orderItems) {
140
            try {
141
                Item item = itemRepository.selectById(orderItem.getItemId());
142
                if (item != null) {
143
                    itemCategoryIds.add(item.getCategoryId());
144
                }
145
            } catch (ProfitMandiBusinessException e) {
146
                logger.warn("Item {} not found", orderItem.getItemId());
147
            }
148
        }
149
        float totalAmount = fofoOrder.getTotalAmount();
150
 
35980 amit 151
        // Apple items always get the last gift
35221 aman 152
        if (isAppleItem(fofoOrder)) {
153
            GiftEntity lastGift = allGifts.get(allGifts.size() - 1);
154
            if (lastGift.getMaxRedemptions() > 0) {
155
                createScratchOffer(fofoOrder, offer, lastGift);
156
            } else {
157
                createBetterLuckNextTimeScratch(fofoOrder, offer);
158
            }
159
            return true;
160
        }
34474 aman.kumar 161
 
37299 ranu 162
        // Check daily limit — if the default-gift daily cap is hit, we still need to
163
        // honour the default gift's own slab bounds (management rule: default EW must
164
        // not be awarded above its own max_cart_value, e.g. above 20K).
35980 amit 165
        if (!checkGiftAvailability(LocalDate.now(), offer.getUserLimit(), offer)) {
37299 ranu 166
            awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
167
                    "daily-limit-reached");
34474 aman.kumar 168
            return true;
169
        }
35980 amit 170
 
171
        // Find eligible gifts by cart value and category
172
        List<GiftEntity> allOfferGifts = giftRepository.findByOfferId(offer.getId());
173
        List<GiftEntity> eligibleGifts = allOfferGifts.stream()
174
                .filter(gift -> gift.getMaxRedemptions() > 0
175
                        && totalAmount >= gift.getMinCartValue()
176
                        && totalAmount <= gift.getMaxCartValue()
177
                        && itemCategoryIds.stream().anyMatch(catId -> isItemCategoryEligible(gift.getProductCategory(), catId)))
178
                .collect(Collectors.toList());
179
 
34474 aman.kumar 180
        if (eligibleGifts.isEmpty()) {
37299 ranu 181
            awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
182
                    "no-eligible-gifts");
34474 aman.kumar 183
            return true;
184
        }
185
 
186
        GiftEntity selectedGift = selectGiftByClassification(offer, eligibleGifts, fofoOrder);
187
        if (selectedGift == null || selectedGift.getMaxRedemptions() <= 0) {
37299 ranu 188
            awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
189
                    "picker-returned-null-or-exhausted");
34804 aman.kumar 190
            return true;
34474 aman.kumar 191
        }
192
 
193
        createScratchOffer(fofoOrder, offer, selectedGift);
194
        return true;
195
    }
196
 
37299 ranu 197
    /**
198
     * Fallback awarder that respects the default gift's own slab bounds and category.
199
     * If the default gift is genuinely eligible for this cart (cart within its
200
     * min/max, category matches, stock left), award it. Otherwise award Better Luck
201
     * Next Time — this prevents the "EW awarded above 20K" management rule violation
202
     * that could happen when a slab gap or exhausted pool caused the picker to fall
203
     * through with no eligible gift.
204
     */
205
    private void awardDefaultOrBlnt(FofoOrder order, OfferEntity offer, GiftEntity defaultGift,
206
                                    float totalAmount, Set<Integer> itemCategoryIds, String reason) {
207
        // 1) Default gift — only if truly eligible for this cart (respects its slab).
208
        boolean defaultEligible = defaultGift != null
209
                && defaultGift.getMaxRedemptions() != null
210
                && defaultGift.getMaxRedemptions() > 0
211
                && defaultGift.getMinCartValue() != null
212
                && defaultGift.getMaxCartValue() != null
213
                && totalAmount >= defaultGift.getMinCartValue()
214
                && totalAmount <= defaultGift.getMaxCartValue()
215
                && itemCategoryIds.stream().anyMatch(catId ->
216
                isItemCategoryEligible(defaultGift.getProductCategory(), catId));
217
        if (defaultEligible) {
218
            logger.info("Fallback fired for order {} ({}) — default gift {} eligible, awarding it",
219
                    order.getId(), reason, defaultGift.getId());
220
            createDefaultGiftScratch(order, offer, defaultGift);
221
            return;
222
        }
34474 aman.kumar 223
 
37299 ranu 224
        // 2) Neckband safety net — management rule: when EW isn't allowed for this cart
37301 ranu 225
        //    (i.e. default fell through), award a Neckband from this offer's config.
226
        //    Prefer a Neckband whose OWN slab covers this cart amount (so stock
227
        //    decrements from the correct slab, matching per-slab quota intent).
228
        //    Only if no slab-matching Neckband exists do we cross-slab pick.
37299 ranu 229
        List<GiftEntity> allGifts = giftRepository.findByOfferId(offer.getId());
37301 ranu 230
        List<GiftEntity> neckbandCandidates = allGifts.stream()
37299 ranu 231
                .filter(g -> g.getName() != null && "neckband".equalsIgnoreCase(g.getName().trim()))
232
                .filter(g -> g.getMaxRedemptions() != null && g.getMaxRedemptions() > 0)
233
                .filter(g -> itemCategoryIds.stream().anyMatch(catId ->
234
                        isItemCategoryEligible(g.getProductCategory(), catId)))
37301 ranu 235
                .collect(Collectors.toList());
236
 
237
        // First pass — slab-matching Neckband (this cart's own slab).
238
        GiftEntity neckband = neckbandCandidates.stream()
239
                .filter(g -> g.getMinCartValue() != null && g.getMaxCartValue() != null
240
                        && totalAmount >= g.getMinCartValue()
241
                        && totalAmount <= g.getMaxCartValue())
37299 ranu 242
                .findFirst()
243
                .orElse(null);
37301 ranu 244
 
245
        // Second pass — cross-slab pick only if the correct slab has no Neckband left.
246
        if (neckband == null && !neckbandCandidates.isEmpty()) {
247
            neckband = neckbandCandidates.get(0);
248
            logger.info("Fallback for order {} ({}) — no slab-matching Neckband for cart={}; cross-slab picking row {}",
249
                    order.getId(), reason, totalAmount, neckband.getId());
250
        }
251
 
37299 ranu 252
        if (neckband != null) {
37301 ranu 253
            logger.info("Fallback fired for order {} ({}) — awarding Neckband row {} (slab {}-{}) for cart={}",
254
                    order.getId(), reason, neckband.getId(),
255
                    neckband.getMinCartValue(), neckband.getMaxCartValue(), totalAmount);
37299 ranu 256
            createScratchOffer(order, offer, neckband);
257
            return;
258
        }
259
 
260
        // 3) Absolute last resort — no default, no Neckband available anywhere.
261
        logger.warn("Fallback fired for order {} ({}) — no default gift AND no Neckband in stock; awarding Better Luck Next Time",
262
                order.getId(), reason);
263
        createBetterLuckNextTimeScratch(order, offer);
264
    }
265
 
266
 
34474 aman.kumar 267
    private boolean isItemCategoryEligible(String giftCategories, Integer itemCategoryId) {
268
        if (giftCategories == null || giftCategories.trim().isEmpty()) {
269
            return false;
270
        }
271
        return Arrays.stream(giftCategories.split(","))
35980 amit 272
                .map(String::trim)
273
                .filter(s -> !s.isEmpty())
34474 aman.kumar 274
                .map(category -> {
275
                    try {
35980 amit 276
                        return Integer.parseInt(category);
34474 aman.kumar 277
                    } catch (NumberFormatException e) {
35095 aman 278
                        return -1;
34474 aman.kumar 279
                    }
280
                })
281
                .anyMatch(itemCategoryId::equals);
282
    }
283
 
284
    private GiftEntity selectGiftByClassification(OfferEntity offer, List<GiftEntity> eligibleGifts, FofoOrder fofoOrder) {
35980 amit 285
        switch (offer.getClassification()) {
34474 aman.kumar 286
            case "Serial":
35980 amit 287
                return eligibleGifts.get(0);
34474 aman.kumar 288
            case "Random":
35980 amit 289
                List<GiftEntity> shuffled = new ArrayList<>(eligibleGifts);
290
                Collections.shuffle(shuffled);
291
                return shuffled.get(0);
34474 aman.kumar 292
            case "PartnerWise":
35980 amit 293
                int fofoId = fofoOrder.getFofoId();
294
                return eligibleGifts.stream()
295
                        .filter(gift -> gift.getFofoStore() != null
296
                                && Arrays.stream(gift.getFofoStore().split(","))
297
                                .map(String::trim)
298
                                .anyMatch(s -> {
299
                                    try { return Integer.parseInt(s) == fofoId; }
300
                                    catch (NumberFormatException e) { return false; }
301
                                }))
302
                        .findFirst()
303
                        .orElse(null);
34474 aman.kumar 304
            default:
35980 amit 305
                logger.warn("Unknown classification: {}", offer.getClassification());
306
                return null;
34474 aman.kumar 307
        }
308
    }
309
 
35221 aman 310
    public void createBetterLuckNextTimeScratch(FofoOrder order, OfferEntity offer) {
35980 amit 311
        ScratchOffer so = new ScratchOffer();
312
        so.setCustomerId(order.getCustomerId());
313
        so.setOfferId(offer.getId());
314
        so.setGiftId(1);
315
        so.setInvoiceNumber(order.getInvoiceNumber());
316
        so.setCreatedTimestamp(LocalDateTime.now());
317
        so.setOfferName(offer.getName());
318
        scratchOfferRepository.persist(so);
35221 aman 319
    }
320
 
35095 aman 321
    public void createScratchOffer(FofoOrder order, OfferEntity offer, GiftEntity gift) {
35980 amit 322
        int updated = giftRepository.decrementMaxRedemptions(gift.getId());
323
        if (updated == 0) {
324
            logger.warn("Gift {} has no remaining redemptions", gift.getId());
325
            return;
326
        }
34474 aman.kumar 327
 
35980 amit 328
        ScratchOffer so = new ScratchOffer();
329
        so.setCustomerId(order.getCustomerId());
330
        so.setOfferId(offer.getId());
331
        so.setGiftId(gift.getId());
332
        so.setInvoiceNumber(order.getInvoiceNumber());
333
        so.setCreatedTimestamp(LocalDateTime.now());
334
        so.setOfferName(gift.getName());
335
        scratchOfferRepository.persist(so);
336
        logger.info("Created scratch offer for order {} gift {}", order.getId(), gift.getId());
34474 aman.kumar 337
    }
338
 
35253 aman 339
    public void createDefaultGiftScratch(FofoOrder order, OfferEntity offer, GiftEntity gift) {
35980 amit 340
        ScratchOffer so = new ScratchOffer();
341
        so.setCustomerId(order.getCustomerId());
342
        so.setOfferId(offer.getId());
343
        so.setGiftId(gift.getId());
344
        so.setInvoiceNumber(order.getInvoiceNumber());
345
        so.setCreatedTimestamp(LocalDateTime.now());
346
        so.setOfferName(gift.getName());
347
        scratchOfferRepository.persist(so);
35253 aman 348
    }
35095 aman 349
 
35253 aman 350
 
34474 aman.kumar 351
    public boolean createOffer(ScratchRequest request) {
352
        OfferEntity offer = mapToOfferEntity(request);
353
        seasonalOfferRepository.save(offer);
354
        List<GiftEntity> gifts = mapToGiftEntities(request.getGifts(), offer.getId());
355
        gifts.forEach(giftRepository::save);
356
        return offer.getId() > 0 && giftRepository.findByOfferId(offer.getId()).size() == request.getGifts().size();
357
    }
358
 
359
    public boolean updateOffer(Integer offerId, ScratchRequest request) {
360
        OfferEntity existingOffer = seasonalOfferRepository.findById(offerId);
361
        boolean res = false;
362
        if (existingOffer != null) {
363
            updateOfferEntity(existingOffer, request);
364
            updateGifts(existingOffer.getId(), request.getGifts());
365
            res = true;
366
        }
367
        return res;
368
    }
369
 
370
    //update offer
371
    private void updateOfferEntity(OfferEntity existingOffer, ScratchRequest request) {
372
        existingOffer.setName(request.getName());
373
        existingOffer.setDescription(request.getDescription());
374
        existingOffer.setStartDate(request.getStartDate());
375
        existingOffer.setEndDate(request.getEndDate());
376
        existingOffer.setTermsCondition(request.getTermsCondition());
377
        existingOffer.setUserLimit(request.getUserLimit());
378
        existingOffer.setScratchValidity(request.getScratchValidity());
379
        existingOffer.setProductCategory(request.getProductCategory());
380
        existingOffer.setPaymentMethod(request.getPaymentMethod());
381
        existingOffer.setClassification(request.getClassification());
382
    }
383
 
384
    //update gift
385
    private void updateGifts(Integer offerId, List<ScratchRequest.GiftData> giftsData) {
386
        Map<Integer, GiftEntity> existingGifts = giftRepository.findByOfferId(offerId).stream().collect(Collectors.toMap(GiftEntity::getId, Function.identity()));
387
 
388
        for (ScratchRequest.GiftData giftData : giftsData) {
389
            if (giftData.getId() != null && existingGifts.containsKey(giftData.getId())) {
390
                GiftEntity existingGift = existingGifts.get(giftData.getId());
391
                updateGiftEntity(existingGift, giftData);
392
                existingGifts.remove(giftData.getId());
393
            } else {
394
                GiftEntity gift = new GiftEntity();
395
                gift.setName(giftData.getName());
396
                gift.setThumbnailUrl(giftData.getThumbnailUrl());
397
                gift.setMinCartValue(giftData.getMinCartValue());
398
                gift.setMaxRedemptions(giftData.getMaxRedemptions());
399
                gift.setOfferId(offerId);
400
                gift.setMaxCartValue(giftData.getMaxCartValue());
401
                gift.setFofoStore(giftData.getFofoStore());
402
                gift.setProductCategory(giftData.getProductCategory());
35253 aman 403
                gift.setIsDefault(giftData.isDefault());
34474 aman.kumar 404
                giftRepository.save(gift);
405
            }
406
        }
407
    }
408
 
409
    private void updateGiftEntity(GiftEntity gift, ScratchRequest.GiftData data) {
410
        gift.setName(data.getName());
411
        gift.setThumbnailUrl(data.getThumbnailUrl());
412
        gift.setMinCartValue(data.getMinCartValue());
413
        gift.setMaxRedemptions(data.getMaxRedemptions());
414
        gift.setMaxCartValue(data.getMaxCartValue());
415
        gift.setFofoStore(data.getFofoStore());
416
        gift.setProductCategory(data.getProductCategory());
35253 aman 417
        gift.setIsDefault(data.isDefault());
34474 aman.kumar 418
    }
419
 
420
    //offer
421
    private OfferEntity mapToOfferEntity(ScratchRequest request) {
422
        OfferEntity offer = new OfferEntity();
423
        offer.setName(request.getName());
424
        offer.setDescription(request.getDescription());
425
        offer.setOfferImage(request.getOfferImage());
426
        offer.setOfferType(request.getOfferType());
427
        offer.setStartDate(request.getStartDate());
428
        offer.setEndDate(request.getEndDate());
429
        offer.setTermsCondition(request.getTermsCondition());
430
        offer.setUserLimit(request.getUserLimit());
431
        offer.setScratchValidity(request.getScratchValidity());
432
        offer.setProductCategory(request.getProductCategory());
433
        offer.setPaymentMethod(request.getPaymentMethod());
434
        offer.setClassification(request.getClassification());
34804 aman.kumar 435
        logger.info("active flag - " + request.isActive());
34474 aman.kumar 436
        offer.setActive(request.isActive());
437
        return offer;
438
    }
439
 
440
    //gift
441
    private List<GiftEntity> mapToGiftEntities(List<ScratchRequest.GiftData> giftsData, Integer offerId) {
442
        return giftsData.stream().map(giftData -> {
34804 aman.kumar 443
            logger.info("giftData: {}", giftData);
34474 aman.kumar 444
            GiftEntity gift = new GiftEntity();
445
            gift.setName(giftData.getName());
446
            gift.setThumbnailUrl(giftData.getThumbnailUrl());
447
            gift.setMinCartValue(giftData.getMinCartValue());
448
            gift.setMaxRedemptions(giftData.getMaxRedemptions());
449
            gift.setOfferId(offerId);
450
            gift.setMaxCartValue(giftData.getMaxCartValue());
451
            gift.setFofoStore(giftData.getFofoStore());
452
            gift.setProductCategory(giftData.getProductCategory());
35253 aman 453
            gift.setIsDefault(giftData.isDefault());
34474 aman.kumar 454
            return gift;
455
        }).collect(Collectors.toList());
456
    }
457
 
458
}