Subversion Repositories SmartDukaan

Rev

Rev 35980 | Rev 37301 | 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
225
        //    (i.e. default fell through), award any in-stock, category-matching Neckband
226
        //    from this offer's config, even if its own slab bounds don't cover this cart.
227
        //    Team keeps Neckband quantity topped up so BLNT never fires under normal ops.
228
        List<GiftEntity> allGifts = giftRepository.findByOfferId(offer.getId());
229
        GiftEntity neckband = allGifts.stream()
230
                .filter(g -> g.getName() != null && "neckband".equalsIgnoreCase(g.getName().trim()))
231
                .filter(g -> g.getMaxRedemptions() != null && g.getMaxRedemptions() > 0)
232
                .filter(g -> itemCategoryIds.stream().anyMatch(catId ->
233
                        isItemCategoryEligible(g.getProductCategory(), catId)))
234
                .findFirst()
235
                .orElse(null);
236
        if (neckband != null) {
237
            logger.info("Fallback fired for order {} ({}) — default not eligible; awarding Neckband row {} as safety net",
238
                    order.getId(), reason, neckband.getId());
239
            createScratchOffer(order, offer, neckband);
240
            return;
241
        }
242
 
243
        // 3) Absolute last resort — no default, no Neckband available anywhere.
244
        logger.warn("Fallback fired for order {} ({}) — no default gift AND no Neckband in stock; awarding Better Luck Next Time",
245
                order.getId(), reason);
246
        createBetterLuckNextTimeScratch(order, offer);
247
    }
248
 
249
 
34474 aman.kumar 250
    private boolean isItemCategoryEligible(String giftCategories, Integer itemCategoryId) {
251
        if (giftCategories == null || giftCategories.trim().isEmpty()) {
252
            return false;
253
        }
254
        return Arrays.stream(giftCategories.split(","))
35980 amit 255
                .map(String::trim)
256
                .filter(s -> !s.isEmpty())
34474 aman.kumar 257
                .map(category -> {
258
                    try {
35980 amit 259
                        return Integer.parseInt(category);
34474 aman.kumar 260
                    } catch (NumberFormatException e) {
35095 aman 261
                        return -1;
34474 aman.kumar 262
                    }
263
                })
264
                .anyMatch(itemCategoryId::equals);
265
    }
266
 
267
    private GiftEntity selectGiftByClassification(OfferEntity offer, List<GiftEntity> eligibleGifts, FofoOrder fofoOrder) {
35980 amit 268
        switch (offer.getClassification()) {
34474 aman.kumar 269
            case "Serial":
35980 amit 270
                return eligibleGifts.get(0);
34474 aman.kumar 271
            case "Random":
35980 amit 272
                List<GiftEntity> shuffled = new ArrayList<>(eligibleGifts);
273
                Collections.shuffle(shuffled);
274
                return shuffled.get(0);
34474 aman.kumar 275
            case "PartnerWise":
35980 amit 276
                int fofoId = fofoOrder.getFofoId();
277
                return eligibleGifts.stream()
278
                        .filter(gift -> gift.getFofoStore() != null
279
                                && Arrays.stream(gift.getFofoStore().split(","))
280
                                .map(String::trim)
281
                                .anyMatch(s -> {
282
                                    try { return Integer.parseInt(s) == fofoId; }
283
                                    catch (NumberFormatException e) { return false; }
284
                                }))
285
                        .findFirst()
286
                        .orElse(null);
34474 aman.kumar 287
            default:
35980 amit 288
                logger.warn("Unknown classification: {}", offer.getClassification());
289
                return null;
34474 aman.kumar 290
        }
291
    }
292
 
35221 aman 293
    public void createBetterLuckNextTimeScratch(FofoOrder order, OfferEntity offer) {
35980 amit 294
        ScratchOffer so = new ScratchOffer();
295
        so.setCustomerId(order.getCustomerId());
296
        so.setOfferId(offer.getId());
297
        so.setGiftId(1);
298
        so.setInvoiceNumber(order.getInvoiceNumber());
299
        so.setCreatedTimestamp(LocalDateTime.now());
300
        so.setOfferName(offer.getName());
301
        scratchOfferRepository.persist(so);
35221 aman 302
    }
303
 
35095 aman 304
    public void createScratchOffer(FofoOrder order, OfferEntity offer, GiftEntity gift) {
35980 amit 305
        int updated = giftRepository.decrementMaxRedemptions(gift.getId());
306
        if (updated == 0) {
307
            logger.warn("Gift {} has no remaining redemptions", gift.getId());
308
            return;
309
        }
34474 aman.kumar 310
 
35980 amit 311
        ScratchOffer so = new ScratchOffer();
312
        so.setCustomerId(order.getCustomerId());
313
        so.setOfferId(offer.getId());
314
        so.setGiftId(gift.getId());
315
        so.setInvoiceNumber(order.getInvoiceNumber());
316
        so.setCreatedTimestamp(LocalDateTime.now());
317
        so.setOfferName(gift.getName());
318
        scratchOfferRepository.persist(so);
319
        logger.info("Created scratch offer for order {} gift {}", order.getId(), gift.getId());
34474 aman.kumar 320
    }
321
 
35253 aman 322
    public void createDefaultGiftScratch(FofoOrder order, OfferEntity offer, GiftEntity gift) {
35980 amit 323
        ScratchOffer so = new ScratchOffer();
324
        so.setCustomerId(order.getCustomerId());
325
        so.setOfferId(offer.getId());
326
        so.setGiftId(gift.getId());
327
        so.setInvoiceNumber(order.getInvoiceNumber());
328
        so.setCreatedTimestamp(LocalDateTime.now());
329
        so.setOfferName(gift.getName());
330
        scratchOfferRepository.persist(so);
35253 aman 331
    }
35095 aman 332
 
35253 aman 333
 
34474 aman.kumar 334
    public boolean createOffer(ScratchRequest request) {
335
        OfferEntity offer = mapToOfferEntity(request);
336
        seasonalOfferRepository.save(offer);
337
        List<GiftEntity> gifts = mapToGiftEntities(request.getGifts(), offer.getId());
338
        gifts.forEach(giftRepository::save);
339
        return offer.getId() > 0 && giftRepository.findByOfferId(offer.getId()).size() == request.getGifts().size();
340
    }
341
 
342
    public boolean updateOffer(Integer offerId, ScratchRequest request) {
343
        OfferEntity existingOffer = seasonalOfferRepository.findById(offerId);
344
        boolean res = false;
345
        if (existingOffer != null) {
346
            updateOfferEntity(existingOffer, request);
347
            updateGifts(existingOffer.getId(), request.getGifts());
348
            res = true;
349
        }
350
        return res;
351
    }
352
 
353
    //update offer
354
    private void updateOfferEntity(OfferEntity existingOffer, ScratchRequest request) {
355
        existingOffer.setName(request.getName());
356
        existingOffer.setDescription(request.getDescription());
357
        existingOffer.setStartDate(request.getStartDate());
358
        existingOffer.setEndDate(request.getEndDate());
359
        existingOffer.setTermsCondition(request.getTermsCondition());
360
        existingOffer.setUserLimit(request.getUserLimit());
361
        existingOffer.setScratchValidity(request.getScratchValidity());
362
        existingOffer.setProductCategory(request.getProductCategory());
363
        existingOffer.setPaymentMethod(request.getPaymentMethod());
364
        existingOffer.setClassification(request.getClassification());
365
    }
366
 
367
    //update gift
368
    private void updateGifts(Integer offerId, List<ScratchRequest.GiftData> giftsData) {
369
        Map<Integer, GiftEntity> existingGifts = giftRepository.findByOfferId(offerId).stream().collect(Collectors.toMap(GiftEntity::getId, Function.identity()));
370
 
371
        for (ScratchRequest.GiftData giftData : giftsData) {
372
            if (giftData.getId() != null && existingGifts.containsKey(giftData.getId())) {
373
                GiftEntity existingGift = existingGifts.get(giftData.getId());
374
                updateGiftEntity(existingGift, giftData);
375
                existingGifts.remove(giftData.getId());
376
            } else {
377
                GiftEntity gift = new GiftEntity();
378
                gift.setName(giftData.getName());
379
                gift.setThumbnailUrl(giftData.getThumbnailUrl());
380
                gift.setMinCartValue(giftData.getMinCartValue());
381
                gift.setMaxRedemptions(giftData.getMaxRedemptions());
382
                gift.setOfferId(offerId);
383
                gift.setMaxCartValue(giftData.getMaxCartValue());
384
                gift.setFofoStore(giftData.getFofoStore());
385
                gift.setProductCategory(giftData.getProductCategory());
35253 aman 386
                gift.setIsDefault(giftData.isDefault());
34474 aman.kumar 387
                giftRepository.save(gift);
388
            }
389
        }
390
    }
391
 
392
    private void updateGiftEntity(GiftEntity gift, ScratchRequest.GiftData data) {
393
        gift.setName(data.getName());
394
        gift.setThumbnailUrl(data.getThumbnailUrl());
395
        gift.setMinCartValue(data.getMinCartValue());
396
        gift.setMaxRedemptions(data.getMaxRedemptions());
397
        gift.setMaxCartValue(data.getMaxCartValue());
398
        gift.setFofoStore(data.getFofoStore());
399
        gift.setProductCategory(data.getProductCategory());
35253 aman 400
        gift.setIsDefault(data.isDefault());
34474 aman.kumar 401
    }
402
 
403
    //offer
404
    private OfferEntity mapToOfferEntity(ScratchRequest request) {
405
        OfferEntity offer = new OfferEntity();
406
        offer.setName(request.getName());
407
        offer.setDescription(request.getDescription());
408
        offer.setOfferImage(request.getOfferImage());
409
        offer.setOfferType(request.getOfferType());
410
        offer.setStartDate(request.getStartDate());
411
        offer.setEndDate(request.getEndDate());
412
        offer.setTermsCondition(request.getTermsCondition());
413
        offer.setUserLimit(request.getUserLimit());
414
        offer.setScratchValidity(request.getScratchValidity());
415
        offer.setProductCategory(request.getProductCategory());
416
        offer.setPaymentMethod(request.getPaymentMethod());
417
        offer.setClassification(request.getClassification());
34804 aman.kumar 418
        logger.info("active flag - " + request.isActive());
34474 aman.kumar 419
        offer.setActive(request.isActive());
420
        return offer;
421
    }
422
 
423
    //gift
424
    private List<GiftEntity> mapToGiftEntities(List<ScratchRequest.GiftData> giftsData, Integer offerId) {
425
        return giftsData.stream().map(giftData -> {
34804 aman.kumar 426
            logger.info("giftData: {}", giftData);
34474 aman.kumar 427
            GiftEntity gift = new GiftEntity();
428
            gift.setName(giftData.getName());
429
            gift.setThumbnailUrl(giftData.getThumbnailUrl());
430
            gift.setMinCartValue(giftData.getMinCartValue());
431
            gift.setMaxRedemptions(giftData.getMaxRedemptions());
432
            gift.setOfferId(offerId);
433
            gift.setMaxCartValue(giftData.getMaxCartValue());
434
            gift.setFofoStore(giftData.getFofoStore());
435
            gift.setProductCategory(giftData.getProductCategory());
35253 aman 436
            gift.setIsDefault(giftData.isDefault());
34474 aman.kumar 437
            return gift;
438
        }).collect(Collectors.toList());
439
    }
440
 
441
}