Subversion Repositories SmartDukaan

Rev

Rev 36355 | Go to most recent revision | 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
 
36347 amit 51
    /**
52
     * Fire-and-forget: schedule the batch on a background worker and return immediately.
53
     * Prevents duplicate concurrent runs for the same offerId at the JVM level.
54
     * Used by HTTP controllers; cron CLI keeps using the sync processOfferWithBatch.
55
     */
56
    public String submitBatchAsync(int offerId) {
57
        if (!inFlightOfferIds.add(offerId)) {
58
            LOGGER.info("Offer {} batch submit ignored — already in flight", offerId);
59
            return "Offer " + offerId + " is already being processed. Check the batch summary for progress.";
60
        }
61
        BATCH_EXECUTOR.submit(() -> {
62
            try {
63
                processOfferWithBatch(offerId);
64
            } catch (Exception e) {
65
                LOGGER.error("Offer {} batch processing failed: {}", offerId, e.getMessage(), e);
66
            } finally {
67
                inFlightOfferIds.remove(offerId);
68
            }
69
        });
70
        LOGGER.info("Offer {} batch submitted to background worker", offerId);
71
        return "Offer " + offerId + " submitted for processing. Check the batch summary for progress.";
72
    }
73
 
36342 amit 74
    public void processOfferWithBatch(int offerId) throws Exception {
36357 amit 75
        CronBatch existing = cronBatchService.findRunningForOffer(offerId);
76
        if (existing != null) {
36354 amit 77
            LOGGER.info("Offer {} already running (batch {}, started {}); skipping duplicate run",
78
                    offerId, existing.getId(), existing.getStartedAt());
79
            return;
80
        }
81
 
36342 amit 82
        CreateOfferRequest createOfferRequest = offerProcessingHelper.loadOfferRequest(offerId);
83
        if (createOfferRequest == null) {
84
            return;
85
        }
86
        LOGGER.info("Processing offer {} (type={}) with batch tracking", offerId, createOfferRequest.getSchemeType());
87
 
88
        if (createOfferRequest.getSchemeType().equals(OfferSchemeType.ACTIVATION)) {
89
            processActivationOfferWithBatch(offerId, createOfferRequest);
90
        } else if (createOfferRequest.getSchemeType().equals(OfferSchemeType.SELLIN)) {
91
            processSellinOfferWithBatch(offerId, createOfferRequest);
92
        } else {
93
            LOGGER.warn("Offer {} has unsupported scheme type: {}", offerId, createOfferRequest.getSchemeType());
94
        }
95
    }
96
 
97
    private void processActivationOfferWithBatch(int offerId, CreateOfferRequest createOfferRequest) throws Exception {
98
        List<OfferPartnerPayoutData> partnerPayouts;
99
        try {
100
            partnerPayouts = offerService.calculateOfferPayouts(createOfferRequest);
101
        } catch (Exception e) {
102
            LOGGER.error("Failed to calculate activation payouts for offer {}: {}", offerId, e.getMessage());
103
            return;
104
        }
105
 
106
        if (partnerPayouts.isEmpty()) {
107
            LOGGER.info("No eligible partners for activation offer {}", offerId);
108
            return;
109
        }
110
 
36355 amit 111
        int beforeFilter = partnerPayouts.size();
112
        partnerPayouts = partnerPayouts.stream()
113
                .filter(offerProcessingHelper::hasRemainingOfferPayout)
114
                .collect(Collectors.toList());
115
        if (partnerPayouts.isEmpty()) {
116
            LOGGER.info("Activation offer {}: all {} eligible partners already fully paid, skipping batch", offerId, beforeFilter);
117
            return;
118
        }
119
        if (partnerPayouts.size() < beforeFilter) {
120
            LOGGER.info("Activation offer {}: filtered {} already-paid partners, {} remain", offerId, beforeFilter - partnerPayouts.size(), partnerPayouts.size());
121
        }
122
 
36342 amit 123
        LinkedHashMap<Integer, String> fofoIdPartnerNameMap = new LinkedHashMap<>();
124
        for (OfferPartnerPayoutData data : partnerPayouts) {
125
            fofoIdPartnerNameMap.put(data.getFofoId(), "fofo-" + data.getFofoId());
126
        }
127
 
128
        CronBatch batch = cronBatchService.createBatch("processActivationOffer-" + offerId, fofoIdPartnerNameMap);
129
 
130
        for (OfferPartnerPayoutData data : partnerPayouts) {
131
            try {
132
                offerProcessingHelper.processPartnerOfferPayout(
133
                        batch.getId(), data.getFofoId(), offerId, createOfferRequest.getDescription(),
134
                        data.getLineItemValueMap(), data.getItemCriteriaPayout(), data.getCriteriaId(),
135
                        data.getSerialNumberPaid(), data.getAgeingSummaryModelsMap(),
136
                        data.getEligiblePayoutValue(), data.isDiscount());
137
            } catch (Exception e) {
138
                LOGGER.error("Activation offer {} failed for fofoId={}: {}", offerId, data.getFofoId(), e.getMessage());
139
                cronBatchService.markItemFailed(batch.getId(), data.getFofoId(), e.getMessage());
140
            }
141
        }
142
 
143
        cronBatchService.finalizeBatch(batch.getId());
144
    }
145
 
146
    private void processSellinOfferWithBatch(int offerId, CreateOfferRequest createOfferRequest) throws Exception {
147
        List<SellinPartnerPayoutData> partnerPayouts;
148
        try {
149
            partnerPayouts = offerService.calculateSellinPayouts(createOfferRequest);
150
        } catch (Exception e) {
151
            LOGGER.error("Failed to calculate sellin payouts for offer {}: {}", offerId, e.getMessage());
152
            return;
153
        }
154
 
155
        if (partnerPayouts.isEmpty()) {
156
            LOGGER.info("No eligible partners for sellin offer {}", offerId);
157
            return;
158
        }
159
 
36355 amit 160
        int beforeFilter = partnerPayouts.size();
161
        partnerPayouts = partnerPayouts.stream()
162
                .filter(offerProcessingHelper::hasRemainingSellinPayout)
163
                .collect(Collectors.toList());
164
        if (partnerPayouts.isEmpty()) {
165
            LOGGER.info("Sellin offer {}: all {} eligible partners already fully paid, skipping batch", offerId, beforeFilter);
166
            return;
167
        }
168
        if (partnerPayouts.size() < beforeFilter) {
169
            LOGGER.info("Sellin offer {}: filtered {} already-paid partners, {} remain", offerId, beforeFilter - partnerPayouts.size(), partnerPayouts.size());
170
        }
171
 
36342 amit 172
        LinkedHashMap<Integer, String> fofoIdPartnerNameMap = new LinkedHashMap<>();
173
        for (SellinPartnerPayoutData data : partnerPayouts) {
174
            fofoIdPartnerNameMap.put(data.getFofoId(), "fofo-" + data.getFofoId());
175
        }
176
 
177
        CronBatch batch = cronBatchService.createBatch("processSellinOffer-" + offerId, fofoIdPartnerNameMap);
178
 
179
        for (SellinPartnerPayoutData data : partnerPayouts) {
180
            try {
181
                offerProcessingHelper.processPartnerSellinPayout(
182
                        batch.getId(), data.getFofoId(), offerId, createOfferRequest.getDescription(),
183
                        data.getSerialNumberInventoryItemMap(), data.getItemCriteriaPayout(), data.getCriteriaId(),
184
                        data.getSerialNumberPaid(), data.getEligiblePayoutValue(), data.isDiscount());
185
            } catch (Exception e) {
186
                LOGGER.error("Sellin offer {} failed for fofoId={}: {}", offerId, data.getFofoId(), e.getMessage());
187
                cronBatchService.markItemFailed(batch.getId(), data.getFofoId(), e.getMessage());
188
            }
189
        }
190
 
191
        cronBatchService.finalizeBatch(batch.getId());
192
    }
193
}