Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
36306 amit 1
package com.smartdukaan.cron.scheduled;
2
 
3
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
4
import com.spice.profitmandi.common.model.CustomRetailer;
5
import com.spice.profitmandi.common.model.ProfitMandiConstants;
6
import com.spice.profitmandi.dao.entity.dtr.CreditAccount;
7
import com.spice.profitmandi.dao.entity.fofo.PartnerDailyInvestment;
8
import com.spice.profitmandi.dao.entity.transaction.SDCreditRequirement;
9
import com.spice.profitmandi.dao.enumuration.fofo.Gateway;
10
import com.spice.profitmandi.dao.enumuration.transaction.CreditRisk;
11
import com.spice.profitmandi.dao.model.BulkCreditSummary;
12
import com.spice.profitmandi.dao.repository.dtr.CreditAccountRepository;
13
import com.spice.profitmandi.dao.repository.transaction.SDCreditRequirementRepository;
14
import com.spice.profitmandi.dao.repository.transaction.TransactionRepository;
15
import com.spice.profitmandi.service.PartnerInvestmentService;
36338 amit 16
import com.spice.profitmandi.service.cron.CronBatchService;
36306 amit 17
import com.spice.profitmandi.service.transaction.PartnerLimitUpdateData;
18
import com.spice.profitmandi.service.transaction.SDCreditService;
19
import com.spice.profitmandi.service.user.RetailerService;
20
import com.spice.profitmandi.dao.service.SidbiService;
21
import org.apache.logging.log4j.LogManager;
22
import org.apache.logging.log4j.Logger;
23
import org.springframework.beans.factory.annotation.Autowired;
24
import org.springframework.stereotype.Service;
25
import org.springframework.transaction.annotation.Propagation;
26
import org.springframework.transaction.annotation.Transactional;
27
 
28
import java.math.BigDecimal;
37589 amit 29
import java.math.RoundingMode;
36306 amit 30
import java.time.LocalDateTime;
31
import java.util.*;
32
import java.util.stream.Collectors;
33
 
34
@Service
35
public class PartnerLimitHelper {
36
 
37
    private static final Logger LOGGER = LogManager.getLogger(PartnerLimitHelper.class);
38
 
39
    @Autowired
40
    private RetailerService retailerService;
41
 
42
    @Autowired
43
    private SidbiService sidbiService;
44
 
45
    @Autowired
46
    private SDCreditRequirementRepository sdCreditRequirementRepository;
47
 
37339 amit 48
    private static final BigDecimal FIFTEEN_LAC = BigDecimal.valueOf(1500000);
49
 
50
    @Autowired
36306 amit 51
    private CreditAccountRepository creditAccountRepository;
52
 
53
    @Autowired
54
    private SDCreditService sdCreditService;
55
 
56
    @Autowired
57
    private PartnerInvestmentService partnerInvestmentService;
58
 
59
    @Autowired
60
    private TransactionRepository transactionRepository;
61
 
62
    @Autowired
63
    private CronBatchService cronBatchService;
64
 
65
    private static final NavigableMap<Double, Double> discountMap = new TreeMap<>();
66
    private static final List<Integer> hundredPercentLimitPartnerIds = Arrays.asList();
67
 
68
    static {
69
        discountMap.put(4 * ProfitMandiConstants.ONE_LAC - 1, 0.2);
70
        discountMap.put(10 * ProfitMandiConstants.ONE_LAC - 1, 0.25);
71
        discountMap.put(20 * ProfitMandiConstants.ONE_LAC - 1, 0.3);
72
        discountMap.put(Double.MAX_VALUE, 0.4);
73
    }
74
 
75
    /**
76
     * Read-only: calculates limits for all partners, returns only those that changed.
77
     */
78
    @Transactional(readOnly = true)
79
    public List<PartnerLimitUpdateData> calculateChangedPartnerLimits() throws ProfitMandiBusinessException {
37589 amit 80
        return calculateChangedPartnerLimits(null);
81
    }
82
 
83
    /**
84
     * Read-only: calculates limits and returns only those that changed.
85
     *
86
     * @param restrictTo when non-null, only these partners are considered. The 2-minute investment
87
     * sweep passes the partners whose {@code base_value} moved — measured at 3 on average, peak 18 —
88
     * so the limit stops rescanning all ~980 partners to find the handful that can possibly differ.
89
     */
90
    @Transactional(readOnly = true)
91
    public List<PartnerLimitUpdateData> calculateChangedPartnerLimits(Collection<Integer> restrictTo)
92
            throws ProfitMandiBusinessException {
36306 amit 93
        List<PartnerLimitUpdateData> changedPartners = new ArrayList<>();
94
 
95
        Map<Integer, CustomRetailer> customRetailerMap = retailerService.getFofoRetailers(true);
96
        Map<Integer, BigDecimal> fofoSidbiLimitMap = sidbiService.getSuggestedLimitMap();
97
        Map<Integer, SDCreditRequirement> sdCreditRequirementMap = sdCreditRequirementRepository.selectAll()
98
                .stream().collect(Collectors.toMap(x -> x.getFofoId(), x -> x));
99
        Map<Integer, CreditAccount> creditAccountMap = creditAccountRepository
100
                .selectAllByGateways(Arrays.asList(Gateway.SIDBI, Gateway.SDDIRECT))
101
                .stream().filter(x -> x.isActive()).collect(Collectors.toMap(x -> x.getFofoId(), x -> x));
102
        Map<Integer, BulkCreditSummary> bulkSummaryMap = sdCreditService.getCreditSummaryBulk();
103
 
37589 amit 104
        List<Integer> sortedFofoIds = customRetailerMap.keySet().stream()
105
                .filter(id -> restrictTo == null || restrictTo.contains(id))
106
                .sorted().collect(Collectors.toList());
36306 amit 107
 
37589 amit 108
        // One grouped query instead of one per partner. getCurrentRisk only consults this when the
109
        // partner has no risk_timestamp (418 of 980 have one, so those reads were discarded), and
110
        // the per-partner form was ~4.7ms x 980 -- most of this job's runtime.
111
        Map<Integer, LocalDateTime> firstBillingDateMap = transactionRepository.getFirstBillingDates(sortedFofoIds);
112
 
36306 amit 113
        for (int fofoId : sortedFofoIds) {
37087 amit 114
            // Per-partner isolation: one partner's bad data (null limit/util in the
115
            // compareTo, a getFirstBillingDate/getCurrentRisk failure, etc.) must not
116
            // abort the whole read phase and silently update zero partners. Mirrors the
117
            // REQUIRES_NEW isolation the write phase (updateSinglePartnerLimit) already has.
36306 amit 118
            try {
37087 amit 119
                CreditAccount creditAccount = creditAccountMap.get(fofoId);
120
                BulkCreditSummary bulkSummary = bulkSummaryMap.get(fofoId);
121
                BigDecimal utilizationAmount = bulkSummary != null ? bulkSummary.getUtilization() : BigDecimal.ZERO;
36306 amit 122
 
37087 amit 123
                PartnerDailyInvestment partnerDailyInvestment = partnerInvestmentService.getInvestment(fofoId, 0);
36306 amit 124
 
37087 amit 125
                BigDecimal suggestedAmount = getSuggestedAmount(creditAccount, partnerDailyInvestment, utilizationAmount, fofoSidbiLimitMap.get(fofoId));
126
                SDCreditRequirement existing = sdCreditRequirementMap.get(fofoId);
36306 amit 127
 
37087 amit 128
                if (existing == null) {
129
                    // New partner — needs a record
130
                    changedPartners.add(new PartnerLimitUpdateData(
131
                            fofoId, suggestedAmount, utilizationAmount,
132
                            suggestedAmount.subtract(utilizationAmount),
133
                            CreditRisk.HIGH_RISK, true));
134
                    continue;
135
                }
36306 amit 136
 
37589 amit 137
                LocalDateTime firstBillingDate = firstBillingDateMap.get(fofoId);
37087 amit 138
                CreditRisk newRisk = sdCreditService.getCurrentRisk(existing, firstBillingDate);
36306 amit 139
 
37087 amit 140
                BigDecimal currentLimit = existing.isHardLimit() ? existing.getLimit() : existing.getSuggestedLimit();
141
                BigDecimal newLimit = existing.isHardLimit() ? existing.getLimit() : suggestedAmount;
142
                BigDecimal newAvailable = newLimit.subtract(utilizationAmount);
36306 amit 143
 
37589 amit 144
                // Compare: only include if something actually changed. Both sides are rounded to
145
                // paise first — without that, double noise below half a paisa made ~90% of these
146
                // "changes" phantom, rewriting sd_credit_requirement and dtr.credit_account (and
147
                // churning the SIDBI mirror) ~14,000 times a day for identical values, and leaving
148
                // update_timestamp useless as a signal of when a limit really moved.
149
                boolean limitChanged = scaleMoney(suggestedAmount).compareTo(scaleMoney(existing.getSuggestedLimit())) != 0;
150
                boolean utilizationChanged = scaleMoney(utilizationAmount).compareTo(scaleMoney(existing.getUtilizedAmount())) != 0;
37087 amit 151
                boolean riskChanged = !newRisk.equals(existing.getRisk());
152
 
153
                if (limitChanged || utilizationChanged || riskChanged) {
37465 amit 154
                    LOGGER.info("fofoId={} changed: limit {}→{}, util {}→{}, risk {}→{}, agedAppleStock={}",
37087 amit 155
                            fofoId,
156
                            existing.getSuggestedLimit(), suggestedAmount,
157
                            existing.getUtilizedAmount(), utilizationAmount,
37465 amit 158
                            existing.getRisk(), newRisk,
159
                            partnerDailyInvestment != null ? partnerDailyInvestment.getAgedAppleStockAmount() : 0);
37087 amit 160
                    changedPartners.add(new PartnerLimitUpdateData(
161
                            fofoId, suggestedAmount, utilizationAmount, newAvailable, newRisk, false));
162
                }
163
            } catch (Exception e) {
164
                LOGGER.error("Partner limit calc failed for fofoId={}, skipping: {}", fofoId, e.getMessage());
36306 amit 165
            }
166
        }
167
 
168
        LOGGER.info("Partner limit check: {} total, {} changed", sortedFofoIds.size(), changedPartners.size());
169
        return changedPartners;
170
    }
171
 
172
    /**
173
     * Writes updated limit for a single partner in its own transaction.
174
     */
175
    @Transactional(propagation = Propagation.REQUIRES_NEW,
176
            rollbackFor = {Throwable.class, ProfitMandiBusinessException.class})
177
    public void updateSinglePartnerLimit(int batchId, PartnerLimitUpdateData data) throws ProfitMandiBusinessException {
178
        int fofoId = data.getFofoId();
179
 
180
        SDCreditRequirement sdCreditRequirement;
181
        if (data.isNewRecord()) {
182
            sdCreditRequirement = new SDCreditRequirement();
183
            sdCreditRequirement.setFofoId(fofoId);
184
            sdCreditRequirement.setCreditDays(15);
185
            sdCreditRequirement.setInterestRate(ProfitMandiConstants.NEW_INTEREST_RATE);
186
            sdCreditRequirement.setRisk(data.getCreditRisk());
187
            sdCreditRequirement.setUtilizedAmount(BigDecimal.ZERO);
188
            sdCreditRequirement.setCreateTimestamp(LocalDateTime.now());
189
            sdCreditRequirement.setUpdateTimestamp(LocalDateTime.now());
190
            sdCreditRequirement.setLimit(data.getSuggestedLimit());
191
            sdCreditRequirement.setSuggestedLimit(data.getSuggestedLimit());
192
            sdCreditRequirementRepository.persist(sdCreditRequirement);
193
        } else {
194
            sdCreditRequirement = sdCreditRequirementRepository.selectByFofoId(fofoId);
195
            sdCreditRequirement.setRisk(data.getCreditRisk());
196
            sdCreditRequirement.setSuggestedLimit(data.getSuggestedLimit());
197
            if (!sdCreditRequirement.isHardLimit()) {
198
                sdCreditRequirement.setLimit(data.getSuggestedLimit());
199
            }
200
            sdCreditRequirement.setUtilizedAmount(data.getUtilizationAmount());
201
            sdCreditRequirement.setUpdateTimestamp(LocalDateTime.now());
202
        }
203
 
204
        CreditAccount creditAccount = creditAccountRepository.selectByFofoIdAndGateway(fofoId, Gateway.SDDIRECT);
205
        if (creditAccount == null) {
206
            creditAccount = creditAccountRepository.selectByFofoIdAndGateway(fofoId, Gateway.SIDBI);
207
        }
208
        if (creditAccount != null) {
209
            creditAccount.setInterestRate(sdCreditRequirement.getInterestRate().floatValue());
210
            creditAccount.setSanctionedAmount(sdCreditRequirement.getLimit().floatValue());
211
            creditAccount.setAvailableAmount(data.getAvailableLimit().floatValue());
212
            creditAccount.setFreeDays(sdCreditRequirement.getFreeDays());
213
            creditAccount.setUpdatedOn(LocalDateTime.now());
214
        }
215
 
216
        cronBatchService.markItemSuccess(batchId, fofoId);
217
        LOGGER.info("fofoId={} updated: limit={}, util={}, risk={}", fofoId,
218
                sdCreditRequirement.getLimit(), data.getUtilizationAmount(), data.getCreditRisk());
219
    }
220
 
221
    private BigDecimal getSuggestedAmount(CreditAccount creditAccount, PartnerDailyInvestment partnerDailyInvestment,
222
                                          BigDecimal utilizationAmount, BigDecimal sidbiLimit) {
223
        BigDecimal suggestedAmount = BigDecimal.ZERO;
224
        double utilization = utilizationAmount != null ? utilizationAmount.doubleValue() : 0;
225
        if (creditAccount == null || creditAccount.getGateway().equals(Gateway.SDDIRECT)) {
226
            if (partnerDailyInvestment != null) {
37465 amit 227
                double creditableInvestment = getCreditableInvestment(partnerDailyInvestment);
36306 amit 228
                if (hundredPercentLimitPartnerIds.contains(partnerDailyInvestment.getFofoId())) {
37465 amit 229
                    suggestedAmount = BigDecimal.valueOf((creditableInvestment - utilization) * 1);
37339 amit 230
                    suggestedAmount = suggestedAmount.min(FIFTEEN_LAC);
36306 amit 231
                } else {
37465 amit 232
                    suggestedAmount = getSuggestedLimit(creditableInvestment - utilization);
36306 amit 233
                }
234
            }
235
            if (suggestedAmount.doubleValue() < 0) {
236
                suggestedAmount = BigDecimal.ZERO;
237
            }
238
        } else if (creditAccount.getGateway().equals(Gateway.SIDBI) && sidbiLimit != null) {
37465 amit 239
            suggestedAmount = getSuggestedLimit(getCreditableInvestment(partnerDailyInvestment) - utilization);
36306 amit 240
            suggestedAmount = suggestedAmount.max(sidbiLimit);
241
        }
37589 amit 242
        return scaleMoney(suggestedAmount);
36306 amit 243
    }
244
 
37465 amit 245
    /**
37589 amit 246
     * Rounds to paise before the value is compared or stored.
247
     *
248
     * <p>The limit comes out of a double multiply, so it carries binary noise the stored
249
     * DECIMAL(12,4) cannot — 65123.7400 round-trips and comes back as 65123.740000000005, which
250
     * {@code BigDecimal.compareTo} reports as a change. Measured on a production run, 208 of 230
251
     * "changed" partners differed by less than half a paisa; real changes were never smaller than a
252
     * rupee, so rounding here sits in a clean gap and cannot suppress one.
253
     */
254
    private static BigDecimal scaleMoney(BigDecimal value) {
255
        return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP);
256
    }
257
 
258
    /**
37465 amit 259
     * Investment that counts toward a suggested limit: total investment less the value of Apple
260
     * handsets the partner has held beyond {@link ProfitMandiConstants#AGED_STOCK_APPLE_DAYS} days
261
     * from GRN.
262
     *
263
     * The haircut is applied here only. PartnerDailyInvestment.getTotalInvestment() is deliberately
264
     * left alone, so stock value is unchanged for checkout payment options, the investment-OK gates
265
     * and every partner-facing screen — this is a credit-limit policy, not a change to what the
266
     * stock is worth.
267
     */
268
    private double getCreditableInvestment(PartnerDailyInvestment partnerDailyInvestment) {
269
        return partnerDailyInvestment.getTotalInvestment() - partnerDailyInvestment.getAgedAppleStockAmount();
270
    }
271
 
36306 amit 272
    private BigDecimal getSuggestedLimit(double investmentValue) {
273
        double percentageValue = discountMap.ceilingEntry(investmentValue).getValue();
37339 amit 274
        return BigDecimal.valueOf(investmentValue * percentageValue).min(FIFTEEN_LAC);
36306 amit 275
    }
276
}