Subversion Repositories SmartDukaan

Rev

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