Rev 37465 | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.smartdukaan.cron.scheduled;import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;import com.spice.profitmandi.common.model.CustomRetailer;import com.spice.profitmandi.common.model.ProfitMandiConstants;import com.spice.profitmandi.dao.entity.dtr.CreditAccount;import com.spice.profitmandi.dao.entity.fofo.PartnerDailyInvestment;import com.spice.profitmandi.dao.entity.transaction.SDCreditRequirement;import com.spice.profitmandi.dao.enumuration.fofo.Gateway;import com.spice.profitmandi.dao.enumuration.transaction.CreditRisk;import com.spice.profitmandi.dao.model.BulkCreditSummary;import com.spice.profitmandi.dao.repository.dtr.CreditAccountRepository;import com.spice.profitmandi.dao.repository.transaction.SDCreditRequirementRepository;import com.spice.profitmandi.dao.repository.transaction.TransactionRepository;import com.spice.profitmandi.service.PartnerInvestmentService;import com.spice.profitmandi.service.cron.CronBatchService;import com.spice.profitmandi.service.transaction.PartnerLimitUpdateData;import com.spice.profitmandi.service.transaction.SDCreditService;import com.spice.profitmandi.service.user.RetailerService;import com.spice.profitmandi.dao.service.SidbiService;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import org.springframework.transaction.annotation.Propagation;import org.springframework.transaction.annotation.Transactional;import java.math.BigDecimal;import java.math.RoundingMode;import java.time.LocalDateTime;import java.util.*;import java.util.stream.Collectors;@Servicepublic class PartnerLimitHelper {private static final Logger LOGGER = LogManager.getLogger(PartnerLimitHelper.class);@Autowiredprivate RetailerService retailerService;@Autowiredprivate SidbiService sidbiService;@Autowiredprivate SDCreditRequirementRepository sdCreditRequirementRepository;private static final BigDecimal FIFTEEN_LAC = BigDecimal.valueOf(1500000);@Autowiredprivate CreditAccountRepository creditAccountRepository;@Autowiredprivate SDCreditService sdCreditService;@Autowiredprivate PartnerInvestmentService partnerInvestmentService;@Autowiredprivate TransactionRepository transactionRepository;@Autowiredprivate CronBatchService cronBatchService;private static final NavigableMap<Double, Double> discountMap = new TreeMap<>();private static final List<Integer> hundredPercentLimitPartnerIds = Arrays.asList();static {discountMap.put(4 * ProfitMandiConstants.ONE_LAC - 1, 0.2);discountMap.put(10 * ProfitMandiConstants.ONE_LAC - 1, 0.25);discountMap.put(20 * ProfitMandiConstants.ONE_LAC - 1, 0.3);discountMap.put(Double.MAX_VALUE, 0.4);}/*** Read-only: calculates limits for all partners, returns only those that changed.*/@Transactional(readOnly = true)public List<PartnerLimitUpdateData> calculateChangedPartnerLimits() throws ProfitMandiBusinessException {return calculateChangedPartnerLimits(null);}/*** Read-only: calculates limits and returns only those that changed.** @param restrictTo when non-null, only these partners are considered. The 2-minute investment* sweep passes the partners whose {@code base_value} moved — measured at 3 on average, peak 18 —* so the limit stops rescanning all ~980 partners to find the handful that can possibly differ.*/@Transactional(readOnly = true)public List<PartnerLimitUpdateData> calculateChangedPartnerLimits(Collection<Integer> restrictTo)throws ProfitMandiBusinessException {List<PartnerLimitUpdateData> changedPartners = new ArrayList<>();Map<Integer, CustomRetailer> customRetailerMap = retailerService.getFofoRetailers(true);Map<Integer, BigDecimal> fofoSidbiLimitMap = sidbiService.getSuggestedLimitMap();Map<Integer, SDCreditRequirement> sdCreditRequirementMap = sdCreditRequirementRepository.selectAll().stream().collect(Collectors.toMap(x -> x.getFofoId(), x -> x));Map<Integer, CreditAccount> creditAccountMap = creditAccountRepository.selectAllByGateways(Arrays.asList(Gateway.SIDBI, Gateway.SDDIRECT)).stream().filter(x -> x.isActive()).collect(Collectors.toMap(x -> x.getFofoId(), x -> x));Map<Integer, BulkCreditSummary> bulkSummaryMap = sdCreditService.getCreditSummaryBulk();List<Integer> sortedFofoIds = customRetailerMap.keySet().stream().filter(id -> restrictTo == null || restrictTo.contains(id)).sorted().collect(Collectors.toList());// One grouped query instead of one per partner. getCurrentRisk only consults this when the// partner has no risk_timestamp (418 of 980 have one, so those reads were discarded), and// the per-partner form was ~4.7ms x 980 -- most of this job's runtime.Map<Integer, LocalDateTime> firstBillingDateMap = transactionRepository.getFirstBillingDates(sortedFofoIds);for (int fofoId : sortedFofoIds) {// Per-partner isolation: one partner's bad data (null limit/util in the// compareTo, a getFirstBillingDate/getCurrentRisk failure, etc.) must not// abort the whole read phase and silently update zero partners. Mirrors the// REQUIRES_NEW isolation the write phase (updateSinglePartnerLimit) already has.try {CreditAccount creditAccount = creditAccountMap.get(fofoId);BulkCreditSummary bulkSummary = bulkSummaryMap.get(fofoId);BigDecimal utilizationAmount = bulkSummary != null ? bulkSummary.getUtilization() : BigDecimal.ZERO;PartnerDailyInvestment partnerDailyInvestment = partnerInvestmentService.getInvestment(fofoId, 0);BigDecimal suggestedAmount = getSuggestedAmount(creditAccount, partnerDailyInvestment, utilizationAmount, fofoSidbiLimitMap.get(fofoId));SDCreditRequirement existing = sdCreditRequirementMap.get(fofoId);if (existing == null) {// New partner — needs a recordchangedPartners.add(new PartnerLimitUpdateData(fofoId, suggestedAmount, utilizationAmount,suggestedAmount.subtract(utilizationAmount),CreditRisk.HIGH_RISK, true));continue;}LocalDateTime firstBillingDate = firstBillingDateMap.get(fofoId);CreditRisk newRisk = sdCreditService.getCurrentRisk(existing, firstBillingDate);BigDecimal currentLimit = existing.isHardLimit() ? existing.getLimit() : existing.getSuggestedLimit();BigDecimal newLimit = existing.isHardLimit() ? existing.getLimit() : suggestedAmount;BigDecimal newAvailable = newLimit.subtract(utilizationAmount);// Compare: only include if something actually changed. Both sides are rounded to// paise first — without that, double noise below half a paisa made ~90% of these// "changes" phantom, rewriting sd_credit_requirement and dtr.credit_account (and// churning the SIDBI mirror) ~14,000 times a day for identical values, and leaving// update_timestamp useless as a signal of when a limit really moved.boolean limitChanged = scaleMoney(suggestedAmount).compareTo(scaleMoney(existing.getSuggestedLimit())) != 0;boolean utilizationChanged = scaleMoney(utilizationAmount).compareTo(scaleMoney(existing.getUtilizedAmount())) != 0;boolean riskChanged = !newRisk.equals(existing.getRisk());if (limitChanged || utilizationChanged || riskChanged) {LOGGER.info("fofoId={} changed: limit {}→{}, util {}→{}, risk {}→{}, agedAppleStock={}",fofoId,existing.getSuggestedLimit(), suggestedAmount,existing.getUtilizedAmount(), utilizationAmount,existing.getRisk(), newRisk,partnerDailyInvestment != null ? partnerDailyInvestment.getAgedAppleStockAmount() : 0);changedPartners.add(new PartnerLimitUpdateData(fofoId, suggestedAmount, utilizationAmount, newAvailable, newRisk, false));}} catch (Exception e) {LOGGER.error("Partner limit calc failed for fofoId={}, skipping: {}", fofoId, e.getMessage());}}LOGGER.info("Partner limit check: {} total, {} changed", sortedFofoIds.size(), changedPartners.size());return changedPartners;}/*** Writes updated limit for a single partner in its own transaction.*/@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = {Throwable.class, ProfitMandiBusinessException.class})public void updateSinglePartnerLimit(int batchId, PartnerLimitUpdateData data) throws ProfitMandiBusinessException {int fofoId = data.getFofoId();SDCreditRequirement sdCreditRequirement;if (data.isNewRecord()) {sdCreditRequirement = new SDCreditRequirement();sdCreditRequirement.setFofoId(fofoId);sdCreditRequirement.setCreditDays(15);sdCreditRequirement.setInterestRate(ProfitMandiConstants.NEW_INTEREST_RATE);sdCreditRequirement.setRisk(data.getCreditRisk());sdCreditRequirement.setUtilizedAmount(BigDecimal.ZERO);sdCreditRequirement.setCreateTimestamp(LocalDateTime.now());sdCreditRequirement.setUpdateTimestamp(LocalDateTime.now());sdCreditRequirement.setLimit(data.getSuggestedLimit());sdCreditRequirement.setSuggestedLimit(data.getSuggestedLimit());sdCreditRequirementRepository.persist(sdCreditRequirement);} else {sdCreditRequirement = sdCreditRequirementRepository.selectByFofoId(fofoId);sdCreditRequirement.setRisk(data.getCreditRisk());sdCreditRequirement.setSuggestedLimit(data.getSuggestedLimit());if (!sdCreditRequirement.isHardLimit()) {sdCreditRequirement.setLimit(data.getSuggestedLimit());}sdCreditRequirement.setUtilizedAmount(data.getUtilizationAmount());sdCreditRequirement.setUpdateTimestamp(LocalDateTime.now());}CreditAccount creditAccount = creditAccountRepository.selectByFofoIdAndGateway(fofoId, Gateway.SDDIRECT);if (creditAccount == null) {creditAccount = creditAccountRepository.selectByFofoIdAndGateway(fofoId, Gateway.SIDBI);}if (creditAccount != null) {creditAccount.setInterestRate(sdCreditRequirement.getInterestRate().floatValue());creditAccount.setSanctionedAmount(sdCreditRequirement.getLimit().floatValue());creditAccount.setAvailableAmount(data.getAvailableLimit().floatValue());creditAccount.setFreeDays(sdCreditRequirement.getFreeDays());creditAccount.setUpdatedOn(LocalDateTime.now());}cronBatchService.markItemSuccess(batchId, fofoId);LOGGER.info("fofoId={} updated: limit={}, util={}, risk={}", fofoId,sdCreditRequirement.getLimit(), data.getUtilizationAmount(), data.getCreditRisk());}private BigDecimal getSuggestedAmount(CreditAccount creditAccount, PartnerDailyInvestment partnerDailyInvestment,BigDecimal utilizationAmount, BigDecimal sidbiLimit) {BigDecimal suggestedAmount = BigDecimal.ZERO;double utilization = utilizationAmount != null ? utilizationAmount.doubleValue() : 0;if (creditAccount == null || creditAccount.getGateway().equals(Gateway.SDDIRECT)) {if (partnerDailyInvestment != null) {double creditableInvestment = getCreditableInvestment(partnerDailyInvestment);if (hundredPercentLimitPartnerIds.contains(partnerDailyInvestment.getFofoId())) {suggestedAmount = BigDecimal.valueOf((creditableInvestment - utilization) * 1);suggestedAmount = suggestedAmount.min(FIFTEEN_LAC);} else {suggestedAmount = getSuggestedLimit(creditableInvestment - utilization);}}if (suggestedAmount.doubleValue() < 0) {suggestedAmount = BigDecimal.ZERO;}} else if (creditAccount.getGateway().equals(Gateway.SIDBI) && sidbiLimit != null) {suggestedAmount = getSuggestedLimit(getCreditableInvestment(partnerDailyInvestment) - utilization);suggestedAmount = suggestedAmount.max(sidbiLimit);}return scaleMoney(suggestedAmount);}/*** Rounds to paise before the value is compared or stored.** <p>The limit comes out of a double multiply, so it carries binary noise the stored* DECIMAL(12,4) cannot — 65123.7400 round-trips and comes back as 65123.740000000005, which* {@code BigDecimal.compareTo} reports as a change. Measured on a production run, 208 of 230* "changed" partners differed by less than half a paisa; real changes were never smaller than a* rupee, so rounding here sits in a clean gap and cannot suppress one.*/private static BigDecimal scaleMoney(BigDecimal value) {return (value == null ? BigDecimal.ZERO : value).setScale(2, RoundingMode.HALF_UP);}/*** Investment that counts toward a suggested limit: total investment less the value of Apple* handsets the partner has held beyond {@link ProfitMandiConstants#AGED_STOCK_APPLE_DAYS} days* from GRN.** The haircut is applied here only. PartnerDailyInvestment.getTotalInvestment() is deliberately* left alone, so stock value is unchanged for checkout payment options, the investment-OK gates* and every partner-facing screen — this is a credit-limit policy, not a change to what the* stock is worth.*/private double getCreditableInvestment(PartnerDailyInvestment partnerDailyInvestment) {return partnerDailyInvestment.getTotalInvestment() - partnerDailyInvestment.getAgedAppleStockAmount();}private BigDecimal getSuggestedLimit(double investmentValue) {double percentageValue = discountMap.ceilingEntry(investmentValue).getValue();return BigDecimal.valueOf(investmentValue * percentageValue).min(FIFTEEN_LAC);}}