Subversion Repositories SmartDukaan

Rev

Rev 36357 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
36342 amit 1
package com.spice.profitmandi.service.offers;
2
 
3
import com.spice.profitmandi.dao.entity.transaction.CronBatch;
4
import com.spice.profitmandi.dao.enumuration.catalog.OfferSchemeType;
5
import com.spice.profitmandi.dao.model.CreateOfferRequest;
6
import com.spice.profitmandi.service.cron.CronBatchService;
7
import org.apache.logging.log4j.LogManager;
8
import org.apache.logging.log4j.Logger;
9
import org.springframework.beans.factory.annotation.Autowired;
10
import org.springframework.stereotype.Service;
11
 
12
import java.util.LinkedHashMap;
13
import java.util.List;
36355 amit 14
import java.util.stream.Collectors;
36347 amit 15
import java.util.Set;
16
import java.util.concurrent.ConcurrentHashMap;
17
import java.util.concurrent.ExecutorService;
18
import java.util.concurrent.Executors;
36342 amit 19
 
20
/**
21
 * Orchestrates offer processing with per-partner transaction isolation and batch tracking.
22
 *
23
 * NO @Transactional on this class or any of its methods — the orchestrator must not
24
 * carry an outer transaction. Each partner's write work runs in REQUIRES_NEW via
25
 * OfferProcessingHelper; batch metadata writes run in REQUIRES_NEW via CronBatchService.
26
 * If the caller is already inside a transaction (e.g., controller class-level @Transactional),
27
 * REQUIRES_NEW will suspend it cleanly.
28
 */
29
@Service
30
public class OfferBatchService {
31
 
32
    private static final Logger LOGGER = LogManager.getLogger(OfferBatchService.class);
33
 
36347 amit 34
    private static final ExecutorService BATCH_EXECUTOR = Executors.newFixedThreadPool(3, r -> {
35
        Thread t = new Thread(r, "offer-batch-worker");
36
        t.setDaemon(true);
37
        return t;
38
    });
39
 
40
    private final Set<Integer> inFlightOfferIds = ConcurrentHashMap.newKeySet();
41
 
36342 amit 42
    @Autowired
43
    private OfferService offerService;
44
 
45
    @Autowired
46
    private OfferProcessingHelper offerProcessingHelper;
47
 
48
    @Autowired
49
    private CronBatchService cronBatchService;
50
 
36486 amit 51
    public boolean hasUnfinishedBatch(int offerId) {
52
        return cronBatchService.findRunningForOffer(offerId) != null;
53
    }
54
 
36347 amit 55
    /**
56
     * Fire-and-forget: schedule the batch on a background worker and return immediately.
57
     * Prevents duplicate concurrent runs for the same offerId at the JVM level.
58
     * Used by HTTP controllers; cron CLI keeps using the sync processOfferWithBatch.
59
     */
60
    public String submitBatchAsync(int offerId) {
61
        if (!inFlightOfferIds.add(offerId)) {
62
            LOGGER.info("Offer {} batch submit ignored — already in flight", offerId);
63
            return "Offer " + offerId + " is already being processed. Check the batch summary for progress.";
64
        }
36486 amit 65
        CronBatch existing = cronBatchService.findRunningForOffer(offerId);
66
        if (existing != null) {
67
            inFlightOfferIds.remove(offerId);
68
            LOGGER.info("Offer {} has unfinished batch {} (started {}); blocking reprocess",
69
                    offerId, existing.getId(), existing.getStartedAt());
70
            return "Offer " + offerId + " has an unfinished batch. Reprocessing is not allowed until the existing batch is fully processed.";
71
        }
36347 amit 72
        BATCH_EXECUTOR.submit(() -> {
73
            try {
74
                processOfferWithBatch(offerId);
75
            } catch (Exception e) {
76
                LOGGER.error("Offer {} batch processing failed: {}", offerId, e.getMessage(), e);
77
            } finally {
78
                inFlightOfferIds.remove(offerId);
79
            }
80
        });
81
        LOGGER.info("Offer {} batch submitted to background worker", offerId);
82
        return "Offer " + offerId + " submitted for processing. Check the batch summary for progress.";
83
    }
84
 
36342 amit 85
    public void processOfferWithBatch(int offerId) throws Exception {
36357 amit 86
        CronBatch existing = cronBatchService.findRunningForOffer(offerId);
87
        if (existing != null) {
36354 amit 88
            LOGGER.info("Offer {} already running (batch {}, started {}); skipping duplicate run",
89
                    offerId, existing.getId(), existing.getStartedAt());
90
            return;
91
        }
92
 
36342 amit 93
        CreateOfferRequest createOfferRequest = offerProcessingHelper.loadOfferRequest(offerId);
94
        if (createOfferRequest == null) {
95
            return;
96
        }
97
        LOGGER.info("Processing offer {} (type={}) with batch tracking", offerId, createOfferRequest.getSchemeType());
98
 
99
        if (createOfferRequest.getSchemeType().equals(OfferSchemeType.ACTIVATION)) {
100
            processActivationOfferWithBatch(offerId, createOfferRequest);
101
        } else if (createOfferRequest.getSchemeType().equals(OfferSchemeType.SELLIN)) {
102
            processSellinOfferWithBatch(offerId, createOfferRequest);
103
        } else {
104
            LOGGER.warn("Offer {} has unsupported scheme type: {}", offerId, createOfferRequest.getSchemeType());
105
        }
106
    }
107
 
108
    private void processActivationOfferWithBatch(int offerId, CreateOfferRequest createOfferRequest) throws Exception {
109
        List<OfferPartnerPayoutData> partnerPayouts;
110
        try {
111
            partnerPayouts = offerService.calculateOfferPayouts(createOfferRequest);
112
        } catch (Exception e) {
113
            LOGGER.error("Failed to calculate activation payouts for offer {}: {}", offerId, e.getMessage());
114
            return;
115
        }
116
 
117
        if (partnerPayouts.isEmpty()) {
118
            LOGGER.info("No eligible partners for activation offer {}", offerId);
119
            return;
120
        }
121
 
36355 amit 122
        int beforeFilter = partnerPayouts.size();
123
        partnerPayouts = partnerPayouts.stream()
124
                .filter(offerProcessingHelper::hasRemainingOfferPayout)
125
                .collect(Collectors.toList());
126
        if (partnerPayouts.isEmpty()) {
127
            LOGGER.info("Activation offer {}: all {} eligible partners already fully paid, skipping batch", offerId, beforeFilter);
128
            return;
129
        }
130
        if (partnerPayouts.size() < beforeFilter) {
131
            LOGGER.info("Activation offer {}: filtered {} already-paid partners, {} remain", offerId, beforeFilter - partnerPayouts.size(), partnerPayouts.size());
132
        }
133
 
36342 amit 134
        LinkedHashMap<Integer, String> fofoIdPartnerNameMap = new LinkedHashMap<>();
135
        for (OfferPartnerPayoutData data : partnerPayouts) {
136
            fofoIdPartnerNameMap.put(data.getFofoId(), "fofo-" + data.getFofoId());
137
        }
138
 
139
        CronBatch batch = cronBatchService.createBatch("processActivationOffer-" + offerId, fofoIdPartnerNameMap);
140
 
141
        for (OfferPartnerPayoutData data : partnerPayouts) {
142
            try {
143
                offerProcessingHelper.processPartnerOfferPayout(
144
                        batch.getId(), data.getFofoId(), offerId, createOfferRequest.getDescription(),
145
                        data.getLineItemValueMap(), data.getItemCriteriaPayout(), data.getCriteriaId(),
146
                        data.getSerialNumberPaid(), data.getAgeingSummaryModelsMap(),
147
                        data.getEligiblePayoutValue(), data.isDiscount());
148
            } catch (Exception e) {
149
                LOGGER.error("Activation offer {} failed for fofoId={}: {}", offerId, data.getFofoId(), e.getMessage());
150
                cronBatchService.markItemFailed(batch.getId(), data.getFofoId(), e.getMessage());
151
            }
152
        }
153
 
154
        cronBatchService.finalizeBatch(batch.getId());
155
    }
156
 
157
    private void processSellinOfferWithBatch(int offerId, CreateOfferRequest createOfferRequest) throws Exception {
158
        List<SellinPartnerPayoutData> partnerPayouts;
159
        try {
160
            partnerPayouts = offerService.calculateSellinPayouts(createOfferRequest);
161
        } catch (Exception e) {
162
            LOGGER.error("Failed to calculate sellin payouts for offer {}: {}", offerId, e.getMessage());
163
            return;
164
        }
165
 
166
        if (partnerPayouts.isEmpty()) {
167
            LOGGER.info("No eligible partners for sellin offer {}", offerId);
168
            return;
169
        }
170
 
36355 amit 171
        int beforeFilter = partnerPayouts.size();
172
        partnerPayouts = partnerPayouts.stream()
173
                .filter(offerProcessingHelper::hasRemainingSellinPayout)
174
                .collect(Collectors.toList());
175
        if (partnerPayouts.isEmpty()) {
176
            LOGGER.info("Sellin offer {}: all {} eligible partners already fully paid, skipping batch", offerId, beforeFilter);
177
            return;
178
        }
179
        if (partnerPayouts.size() < beforeFilter) {
180
            LOGGER.info("Sellin offer {}: filtered {} already-paid partners, {} remain", offerId, beforeFilter - partnerPayouts.size(), partnerPayouts.size());
181
        }
182
 
36342 amit 183
        LinkedHashMap<Integer, String> fofoIdPartnerNameMap = new LinkedHashMap<>();
184
        for (SellinPartnerPayoutData data : partnerPayouts) {
185
            fofoIdPartnerNameMap.put(data.getFofoId(), "fofo-" + data.getFofoId());
186
        }
187
 
188
        CronBatch batch = cronBatchService.createBatch("processSellinOffer-" + offerId, fofoIdPartnerNameMap);
189
 
190
        for (SellinPartnerPayoutData data : partnerPayouts) {
191
            try {
192
                offerProcessingHelper.processPartnerSellinPayout(
193
                        batch.getId(), data.getFofoId(), offerId, createOfferRequest.getDescription(),
194
                        data.getSerialNumberInventoryItemMap(), data.getItemCriteriaPayout(), data.getCriteriaId(),
195
                        data.getSerialNumberPaid(), data.getEligiblePayoutValue(), data.isDiscount());
196
            } catch (Exception e) {
197
                LOGGER.error("Sellin offer {} failed for fofoId={}: {}", offerId, data.getFofoId(), e.getMessage());
198
                cronBatchService.markItemFailed(batch.getId(), data.getFofoId(), e.getMessage());
199
            }
200
        }
201
 
202
        cronBatchService.finalizeBatch(batch.getId());
203
    }
204
}