| 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 |
}
|