Subversion Repositories SmartDukaan

Rev

Rev 37303 | Rev 37312 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37303 Rev 37311
Line 49... Line 49...
49
        if (activeOffers.isEmpty()) {
49
        if (activeOffers.isEmpty()) {
50
            return;
50
            return;
51
        }
51
        }
52
 
52
 
53
        FofoOrder fofoOrder = fofoOrderRepository.selectByOrderId(fofoOrderId);
53
        FofoOrder fofoOrder = fofoOrderRepository.selectByOrderId(fofoOrderId);
-
 
54
 
-
 
55
        // --- V2: offer selection and the per-offer availed check now happen inside processOfferV2 ---
54
        OfferEntity eligibleOffer = findEligibleOffer(activeOffers, fofoOrder);
56
        // OfferEntity eligibleOffer = findEligibleOffer(activeOffers, fofoOrder);
55
        if (eligibleOffer == null) {
57
        // if (eligibleOffer == null) {
56
            logger.info("No eligible offer for order {}", fofoOrderId);
58
        //     logger.info("No eligible offer for order {}", fofoOrderId);
-
 
59
        //     return;
-
 
60
        // }
-
 
61
        //
-
 
62
        // ScratchOffer availedOffer = scratchOfferRepository.getScractchOfferByCustomerIdAndOfferId(fofoOrder.getCustomerId(), eligibleOffer.getId());
-
 
63
        // if (availedOffer != null) {
-
 
64
        //     logger.info("Customer {} already availed offer {}", fofoOrder.getCustomerId(), eligibleOffer.getId());
-
 
65
        //     return;
-
 
66
        // }
-
 
67
        //
-
 
68
        // this.processOffer(eligibleOffer, fofoOrder);
-
 
69
 
-
 
70
        this.processOfferV2(activeOffers, fofoOrder);
-
 
71
    }
-
 
72
 
-
 
73
    // ------------------------------------------------------------------------------------------------
-
 
74
    // V2 award flow. Order value picks the slab, a random in-stock gift wins, and the offer's default
-
 
75
    // gift is the floor. A gift carrying a partner list (fofo_store) is a partner-restricted special and
-
 
76
    // is awarded to those partners on bills above SPECIAL_GIFT_MIN_ORDER_VALUE; a gift with no partner
-
 
77
    // list belongs to the random pool.
-
 
78
    //
-
 
79
    // Deliberately bypasses the V1 gating: no per-day user limit, no Apple override, no item-category
-
 
80
    // filter, no payment-method filter, no classification switch. The one-gift-per-customer-per-offer
-
 
81
    // rule is kept, applied per offer across every active offer.
-
 
82
    //
-
 
83
    // Nothing below calls a V1 helper, so this whole block can be deleted to roll back.
-
 
84
    // ------------------------------------------------------------------------------------------------
-
 
85
 
-
 
86
    private static final double SPECIAL_GIFT_MIN_ORDER_VALUE = 25000d;
-
 
87
 
-
 
88
    private void processOfferV2(List<OfferEntity> activeOffers, FofoOrder fofoOrder) {
-
 
89
        if (fofoOrder == null) {
-
 
90
            logger.error("Order not found, skipping scratch award");
-
 
91
            return;
-
 
92
        }
-
 
93
 
-
 
94
        List<OfferEntity> eligibleOffers = filterOffersNotAvailed(activeOffers, fofoOrder.getCustomerId());
-
 
95
        if (eligibleOffers.isEmpty()) {
-
 
96
            logger.info("Customer {} has already availed every active offer", fofoOrder.getCustomerId());
-
 
97
            return;
-
 
98
        }
-
 
99
 
-
 
100
        Map<Integer, OfferEntity> offerById = new HashMap<>();
-
 
101
        List<GiftEntity> pool = new ArrayList<>();
-
 
102
        for (OfferEntity offer : eligibleOffers) {
-
 
103
            offerById.put(offer.getId(), offer);
-
 
104
            List<GiftEntity> gifts = giftRepository.findByOfferId(offer.getId());
-
 
105
            if (gifts != null) {
-
 
106
                pool.addAll(gifts);
-
 
107
            }
-
 
108
        }
-
 
109
        if (pool.isEmpty()) {
-
 
110
            logger.warn("No gifts configured on any active offer, order {} gets no scratch card", fofoOrder.getId());
-
 
111
            return;
-
 
112
        }
-
 
113
 
-
 
114
        double amount = fofoOrder.getTotalAmount();
-
 
115
 
-
 
116
        // 1. Partner-restricted special gift
-
 
117
        GiftEntity special = findSpecialPartnerGift(pool, fofoOrder.getFofoId(), amount);
-
 
118
        if (special != null && awardWithStock(fofoOrder, offerById.get(special.getOfferId()), special)) {
57
            return;
119
            return;
58
        }
120
        }
59
 
121
 
-
 
122
        // 2. Random draw from the order-value slab
-
 
123
        List<GiftEntity> candidates = new ArrayList<>(findSlabGifts(pool, amount));
-
 
124
        Collections.shuffle(candidates);
-
 
125
        for (GiftEntity gift : candidates) {
60
        ScratchOffer availedOffer = scratchOfferRepository.getScractchOfferByCustomerIdAndOfferId(fofoOrder.getCustomerId(), eligibleOffer.getId());
126
            if (awardWithStock(fofoOrder, offerById.get(gift.getOfferId()), gift)) {
-
 
127
                return;
-
 
128
            }
-
 
129
        }
-
 
130
 
-
 
131
        // 3. Floor: the offer's default gift, always issued, never Better Luck Next Time
-
 
132
        GiftEntity defaultGift = resolveDefaultGift(pool, eligibleOffers);
61
        if (availedOffer != null) {
133
        if (defaultGift == null) {
62
            logger.info("Customer {} already availed offer {}", fofoOrder.getCustomerId(), eligibleOffer.getId());
134
            logger.error("No default gift on any active offer, order {} gets no scratch card", fofoOrder.getId());
63
            return;
135
            return;
64
        }
136
        }
-
 
137
        awardUnconditional(fofoOrder, offerById.get(defaultGift.getOfferId()), defaultGift);
-
 
138
    }
-
 
139
 
-
 
140
    /** One gift per customer per offer: drop any offer this customer already holds a card for. */
-
 
141
    private List<OfferEntity> filterOffersNotAvailed(List<OfferEntity> offers, int customerId) {
-
 
142
        List<OfferEntity> eligible = new ArrayList<>();
-
 
143
        for (OfferEntity offer : offers) {
-
 
144
            if (scratchOfferRepository.getScractchOfferByCustomerIdAndOfferId(customerId, offer.getId()) == null) {
-
 
145
                eligible.add(offer);
-
 
146
            } else {
-
 
147
                logger.info("Customer {} already availed offer {}", customerId, offer.getId());
-
 
148
            }
-
 
149
        }
-
 
150
        return eligible;
-
 
151
    }
65
 
152
 
-
 
153
    private GiftEntity findSpecialPartnerGift(List<GiftEntity> pool, int fofoId, double amount) {
-
 
154
        if (amount <= SPECIAL_GIFT_MIN_ORDER_VALUE) {
-
 
155
            return null;
-
 
156
        }
-
 
157
        GiftEntity match = null;
66
        this.processOffer(eligibleOffer, fofoOrder);
158
        for (GiftEntity gift : pool) {
-
 
159
            if (!hasPartnerList(gift) || !hasStock(gift) || !csvContainsId(gift.getFofoStore(), fofoId)) {
-
 
160
                continue;
-
 
161
            }
-
 
162
            // deterministic when a partner is listed on more than one special gift
-
 
163
            if (match == null || gift.getId() < match.getId()) {
-
 
164
                match = gift;
-
 
165
            }
-
 
166
        }
-
 
167
        if (match != null) {
-
 
168
            logger.info("Special partner gift {} matched partner {} on amount {}", match.getId(), fofoId, amount);
-
 
169
        }
-
 
170
        return match;
67
    }
171
    }
68
 
172
 
-
 
173
    /** Random-pool gifts whose cart-value band contains the order amount. Excludes specials and the default. */
-
 
174
    private List<GiftEntity> findSlabGifts(List<GiftEntity> pool, double amount) {
-
 
175
        List<GiftEntity> slab = new ArrayList<>();
-
 
176
        for (GiftEntity gift : pool) {
-
 
177
            if (hasPartnerList(gift) || isDefaultGift(gift) || !hasStock(gift)) {
-
 
178
                continue;
-
 
179
            }
-
 
180
            if (gift.getMinCartValue() == null && gift.getMaxCartValue() == null) {
-
 
181
                logger.warn("Gift {} has no cart-value bounds, excluding it from the slab pool", gift.getId());
-
 
182
                continue;
-
 
183
            }
-
 
184
            double min = gift.getMinCartValue() == null ? 0d : gift.getMinCartValue();
-
 
185
            double max = gift.getMaxCartValue() == null ? Double.MAX_VALUE : gift.getMaxCartValue();
-
 
186
            if (amount >= min && amount <= max) {
-
 
187
                slab.add(gift);
-
 
188
            }
-
 
189
        }
-
 
190
        return slab;
-
 
191
    }
-
 
192
 
-
 
193
    /**
-
 
194
     * The default gift of the first eligible offer that has one. Resolved from the already-fetched pool
-
 
195
     * rather than via getDefaultGift, whose getSingleResult throws when an offer has two defaults.
-
 
196
     */
-
 
197
    private GiftEntity resolveDefaultGift(List<GiftEntity> pool, List<OfferEntity> offers) {
-
 
198
        for (OfferEntity offer : offers) {
-
 
199
            GiftEntity best = null;
-
 
200
            for (GiftEntity gift : pool) {
-
 
201
                if (gift.getOfferId() == null || gift.getOfferId() != offer.getId() || !isDefaultGift(gift)) {
-
 
202
                    continue;
-
 
203
                }
-
 
204
                if (best == null || gift.getId() < best.getId()) {
-
 
205
                    best = gift;
-
 
206
                }
-
 
207
            }
-
 
208
            if (best != null) {
-
 
209
                return best;
-
 
210
            }
-
 
211
        }
-
 
212
        return null;
-
 
213
    }
-
 
214
 
-
 
215
    /** Award only if the atomic decrement wins; false lets the caller try the next candidate. */
-
 
216
    private boolean awardWithStock(FofoOrder order, OfferEntity offer, GiftEntity gift) {
-
 
217
        if (offer == null || gift == null) {
-
 
218
            return false;
-
 
219
        }
-
 
220
        if (giftRepository.decrementMaxRedemptions(gift.getId()) == 0) {
-
 
221
            logger.warn("Gift {} exhausted by a concurrent order, trying the next candidate", gift.getId());
-
 
222
            return false;
-
 
223
        }
-
 
224
        persistScratchV2(order, offer, gift);
-
 
225
        return true;
-
 
226
    }
-
 
227
 
-
 
228
    /** Floor award: attempt the decrement for accounting, but issue the card either way. */
-
 
229
    private void awardUnconditional(FofoOrder order, OfferEntity offer, GiftEntity gift) {
-
 
230
        if (offer == null || gift == null) {
-
 
231
            logger.error("Cannot resolve the floor gift for order {}", order.getId());
-
 
232
            return;
-
 
233
        }
-
 
234
        if (giftRepository.decrementMaxRedemptions(gift.getId()) == 0) {
-
 
235
            logger.warn("Default gift {} is out of stock but is still being issued for order {}", gift.getId(), order.getId());
-
 
236
        }
-
 
237
        persistScratchV2(order, offer, gift);
-
 
238
    }
-
 
239
 
-
 
240
    private void persistScratchV2(FofoOrder order, OfferEntity offer, GiftEntity gift) {
-
 
241
        if (order.getInvoiceNumber() == null) {
-
 
242
            logger.warn("Order {} has no invoice number, persisting the scratch card anyway", order.getId());
-
 
243
        }
-
 
244
        ScratchOffer so = new ScratchOffer();
-
 
245
        so.setCustomerId(order.getCustomerId());
-
 
246
        so.setOfferId(offer.getId());
-
 
247
        so.setGiftId(gift.getId());
-
 
248
        so.setInvoiceNumber(order.getInvoiceNumber());
-
 
249
        so.setCreatedTimestamp(LocalDateTime.now());
-
 
250
        so.setOfferName(gift.getName());
-
 
251
        scratchOfferRepository.persist(so);
-
 
252
        logger.info("Scratch card created for order {} customer {} offer {} gift {} ({})",
-
 
253
                order.getId(), order.getCustomerId(), offer.getId(), gift.getId(), gift.getName());
-
 
254
    }
-
 
255
 
-
 
256
    /** A gift with a partner list is partner-restricted; one without belongs to the random pool. */
-
 
257
    private static boolean hasPartnerList(GiftEntity gift) {
-
 
258
        return gift.getFofoStore() != null && !gift.getFofoStore().trim().isEmpty();
-
 
259
    }
-
 
260
 
-
 
261
    private static boolean hasStock(GiftEntity gift) {
-
 
262
        return gift.getMaxRedemptions() != null && gift.getMaxRedemptions() > 0;
-
 
263
    }
-
 
264
 
-
 
265
    private static boolean isDefaultGift(GiftEntity gift) {
-
 
266
        return Boolean.TRUE.equals(gift.isDefault());
-
 
267
    }
-
 
268
 
-
 
269
    private static boolean csvContainsId(String csv, int id) {
-
 
270
        if (csv == null || csv.trim().isEmpty()) {
-
 
271
            return false;
-
 
272
        }
-
 
273
        for (String token : csv.split(",")) {
-
 
274
            String trimmed = token.trim();
-
 
275
            if (trimmed.isEmpty()) {
-
 
276
                continue;
-
 
277
            }
-
 
278
            try {
-
 
279
                if (Integer.parseInt(trimmed) == id) {
-
 
280
                    return true;
-
 
281
                }
-
 
282
            } catch (NumberFormatException e) {
-
 
283
                // malformed entry, skip it
-
 
284
            }
-
 
285
        }
-
 
286
        return false;
-
 
287
    }
-
 
288
 
-
 
289
    // ------------------------------------------------------------------------------ end of V2 block
-
 
290
 
69
    private OfferEntity findEligibleOffer(List<OfferEntity> activeOffers, FofoOrder fofoOrder) {
291
    private OfferEntity findEligibleOffer(List<OfferEntity> activeOffers, FofoOrder fofoOrder) {
70
        int customerFofoId = fofoOrder.getFofoId();
292
        int customerFofoId = fofoOrder.getFofoId();
71
 
293
 
72
        // Check PartnerWise offers first
294
        // Check PartnerWise offers first
73
        for (OfferEntity activeOffer : activeOffers) {
295
        for (OfferEntity activeOffer : activeOffers) {