| Line 103... |
Line 103... |
| 103 |
}
|
103 |
}
|
| 104 |
|
104 |
|
| 105 |
|
105 |
|
| 106 |
private boolean checkGiftAvailability(LocalDate date, int perDayMaxQty, OfferEntity offer) {
|
106 |
private boolean checkGiftAvailability(LocalDate date, int perDayMaxQty, OfferEntity offer) {
|
| 107 |
GiftEntity defaultGift = giftRepository.getDefaultGift(offer.getId());
|
107 |
GiftEntity defaultGift = giftRepository.getDefaultGift(offer.getId());
|
| - |
|
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.
|
| 108 |
if (defaultGift == null) return false;
|
110 |
if (defaultGift == null) return true;
|
| 109 |
Integer giftCounts = scratchOfferRepository.countTodaysGiftDistribution(date.atStartOfDay(), date.atTime(LocalTime.MAX), defaultGift.getId());
|
111 |
Integer giftCounts = scratchOfferRepository.countTodaysGiftDistribution(date.atStartOfDay(), date.atTime(LocalTime.MAX), defaultGift.getId());
|
| 110 |
return giftCounts < perDayMaxQty;
|
112 |
return giftCounts < perDayMaxQty;
|
| 111 |
}
|
113 |
}
|
| 112 |
|
114 |
|
| 113 |
private boolean isAppleItem(FofoOrder fofoOrder) {
|
115 |
private boolean isAppleItem(FofoOrder fofoOrder) {
|
| Line 128... |
Line 130... |
| 128 |
if (allGifts.isEmpty()) {
|
130 |
if (allGifts.isEmpty()) {
|
| 129 |
logger.warn("Offer {} has no gifts", offer.getId());
|
131 |
logger.warn("Offer {} has no gifts", offer.getId());
|
| 130 |
return false;
|
132 |
return false;
|
| 131 |
}
|
133 |
}
|
| 132 |
|
134 |
|
| - |
|
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 |
|
| 133 |
// Apple items always get the last gift
|
151 |
// Apple items always get the last gift
|
| 134 |
if (isAppleItem(fofoOrder)) {
|
152 |
if (isAppleItem(fofoOrder)) {
|
| 135 |
GiftEntity lastGift = allGifts.get(allGifts.size() - 1);
|
153 |
GiftEntity lastGift = allGifts.get(allGifts.size() - 1);
|
| 136 |
if (lastGift.getMaxRedemptions() > 0) {
|
154 |
if (lastGift.getMaxRedemptions() > 0) {
|
| 137 |
createScratchOffer(fofoOrder, offer, lastGift);
|
155 |
createScratchOffer(fofoOrder, offer, lastGift);
|
| Line 139... |
Line 157... |
| 139 |
createBetterLuckNextTimeScratch(fofoOrder, offer);
|
157 |
createBetterLuckNextTimeScratch(fofoOrder, offer);
|
| 140 |
}
|
158 |
}
|
| 141 |
return true;
|
159 |
return true;
|
| 142 |
}
|
160 |
}
|
| 143 |
|
161 |
|
| 144 |
// Check daily limit
|
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).
|
| 145 |
if (!checkGiftAvailability(LocalDate.now(), offer.getUserLimit(), offer)) {
|
165 |
if (!checkGiftAvailability(LocalDate.now(), offer.getUserLimit(), offer)) {
|
| 146 |
createDefaultGiftScratch(fofoOrder, offer, defaultGift);
|
166 |
awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
|
| - |
|
167 |
"daily-limit-reached");
|
| 147 |
return true;
|
168 |
return true;
|
| 148 |
}
|
169 |
}
|
| 149 |
|
170 |
|
| 150 |
// Find eligible gifts by cart value and category
|
171 |
// Find eligible gifts by cart value and category
|
| 151 |
List<FofoOrderItem> orderItems = fofoOrderItemRepository.selectByOrderId(fofoOrder.getId());
|
- |
|
| 152 |
Set<Integer> itemCategoryIds = new HashSet<>();
|
- |
|
| 153 |
for (FofoOrderItem orderItem : orderItems) {
|
- |
|
| 154 |
try {
|
- |
|
| 155 |
Item item = itemRepository.selectById(orderItem.getItemId());
|
- |
|
| 156 |
if (item != null) {
|
- |
|
| 157 |
itemCategoryIds.add(item.getCategoryId());
|
- |
|
| 158 |
}
|
- |
|
| 159 |
} catch (ProfitMandiBusinessException e) {
|
- |
|
| 160 |
logger.warn("Item {} not found", orderItem.getItemId());
|
- |
|
| 161 |
}
|
- |
|
| 162 |
}
|
- |
|
| 163 |
|
- |
|
| 164 |
float totalAmount = fofoOrder.getTotalAmount();
|
- |
|
| 165 |
List<GiftEntity> allOfferGifts = giftRepository.findByOfferId(offer.getId());
|
172 |
List<GiftEntity> allOfferGifts = giftRepository.findByOfferId(offer.getId());
|
| 166 |
List<GiftEntity> eligibleGifts = allOfferGifts.stream()
|
173 |
List<GiftEntity> eligibleGifts = allOfferGifts.stream()
|
| 167 |
.filter(gift -> gift.getMaxRedemptions() > 0
|
174 |
.filter(gift -> gift.getMaxRedemptions() > 0
|
| 168 |
&& totalAmount >= gift.getMinCartValue()
|
175 |
&& totalAmount >= gift.getMinCartValue()
|
| 169 |
&& totalAmount <= gift.getMaxCartValue()
|
176 |
&& totalAmount <= gift.getMaxCartValue()
|
| 170 |
&& itemCategoryIds.stream().anyMatch(catId -> isItemCategoryEligible(gift.getProductCategory(), catId)))
|
177 |
&& itemCategoryIds.stream().anyMatch(catId -> isItemCategoryEligible(gift.getProductCategory(), catId)))
|
| 171 |
.collect(Collectors.toList());
|
178 |
.collect(Collectors.toList());
|
| 172 |
|
179 |
|
| 173 |
if (eligibleGifts.isEmpty()) {
|
180 |
if (eligibleGifts.isEmpty()) {
|
| 174 |
createDefaultGiftScratch(fofoOrder, offer, defaultGift);
|
181 |
awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
|
| - |
|
182 |
"no-eligible-gifts");
|
| 175 |
return true;
|
183 |
return true;
|
| 176 |
}
|
184 |
}
|
| 177 |
|
185 |
|
| 178 |
GiftEntity selectedGift = selectGiftByClassification(offer, eligibleGifts, fofoOrder);
|
186 |
GiftEntity selectedGift = selectGiftByClassification(offer, eligibleGifts, fofoOrder);
|
| 179 |
if (selectedGift == null || selectedGift.getMaxRedemptions() <= 0) {
|
187 |
if (selectedGift == null || selectedGift.getMaxRedemptions() <= 0) {
|
| 180 |
createDefaultGiftScratch(fofoOrder, offer, defaultGift);
|
188 |
awardDefaultOrBlnt(fofoOrder, offer, defaultGift, totalAmount, itemCategoryIds,
|
| - |
|
189 |
"picker-returned-null-or-exhausted");
|
| 181 |
return true;
|
190 |
return true;
|
| 182 |
}
|
191 |
}
|
| 183 |
|
192 |
|
| 184 |
createScratchOffer(fofoOrder, offer, selectedGift);
|
193 |
createScratchOffer(fofoOrder, offer, selectedGift);
|
| 185 |
return true;
|
194 |
return true;
|
| 186 |
}
|
195 |
}
|
| 187 |
|
196 |
|
| - |
|
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 |
}
|
| - |
|
223 |
|
| - |
|
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 |
|
| 188 |
|
249 |
|
| 189 |
private boolean isItemCategoryEligible(String giftCategories, Integer itemCategoryId) {
|
250 |
private boolean isItemCategoryEligible(String giftCategories, Integer itemCategoryId) {
|
| 190 |
if (giftCategories == null || giftCategories.trim().isEmpty()) {
|
251 |
if (giftCategories == null || giftCategories.trim().isEmpty()) {
|
| 191 |
return false;
|
252 |
return false;
|
| 192 |
}
|
253 |
}
|