blob: 178a3b023f6187001dbce3e0e9395045a27ec29d [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.creditscorecard.service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.PersistenceException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.credit.scorecard.ApiException;
import org.apache.fineract.credit.scorecard.Configuration;
import org.apache.fineract.credit.scorecard.models.PredictionResponse;
import org.apache.fineract.credit.scorecard.services.AlgorithmsApi;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.creditscorecard.annotation.ScorecardService;
import org.apache.fineract.portfolio.creditscorecard.domain.CreditScorecard;
import org.apache.fineract.portfolio.creditscorecard.domain.CreditScorecardFeature;
import org.apache.fineract.portfolio.creditscorecard.domain.CreditScorecardFeatureRepository;
import org.apache.fineract.portfolio.creditscorecard.domain.CreditScorecardRepository;
import org.apache.fineract.portfolio.creditscorecard.domain.FeatureConfiguration;
import org.apache.fineract.portfolio.creditscorecard.domain.FeatureCriteria;
import org.apache.fineract.portfolio.creditscorecard.domain.FeatureCriteriaScore;
import org.apache.fineract.portfolio.creditscorecard.domain.MLScorecard;
import org.apache.fineract.portfolio.creditscorecard.domain.MLScorecardFields;
import org.apache.fineract.portfolio.creditscorecard.domain.RuleBasedScorecard;
import org.apache.fineract.portfolio.creditscorecard.domain.StatScorecard;
import org.apache.fineract.portfolio.creditscorecard.exception.FeatureCannotBeDeletedException;
import org.apache.fineract.portfolio.creditscorecard.exception.FeatureNotFoundException;
import org.apache.fineract.portfolio.creditscorecard.serialization.CreditScorecardApiJsonHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductScorecardFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
@PropertySource(value = "classpath:scorecard-client.properties")
@ScorecardService(name = "CreditScorecardWritePlatformService")
public class CreditScorecardWritePlatformServiceImpl implements CreditScorecardWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(CreditScorecardWritePlatformServiceImpl.class);
@Value("${fineract.credit.scorecard.uid}")
String username;
@Value("${fineract.credit.scorecard.password}")
String password;
@Value("${fineract.credit.scorecard.baseUrl}")
String baseUrl;
private final PlatformSecurityContext context;
private final LoanProductRepository loanProductRepository;
private final CreditScorecardApiJsonHelper fromApiJsonDeserializer;
private final CreditScorecardFeatureRepository featureRepository;
private final CreditScorecardRepository scorecardRepository;
AlgorithmsApi scorecardApiClient;
@Autowired
public CreditScorecardWritePlatformServiceImpl(final PlatformSecurityContext context,
final CreditScorecardApiJsonHelper fromApiJsonDeserializer,
final LoanProductRepository loanProductRepository,
final CreditScorecardFeatureRepository featureRepository,
final CreditScorecardRepository scorecardRepository) {
this.context = context;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanProductRepository = loanProductRepository;
this.featureRepository = featureRepository;
this.scorecardRepository = scorecardRepository;
}
private void initScorecardClient() {
LOG.warn("Base URL at init : {}", baseUrl);
this.scorecardApiClient = new AlgorithmsApi(Configuration.getDefaultApiClient().setBasePath(baseUrl));
}
@Override
public CommandProcessingResult createScoringFeature(JsonCommand command) {
try {
this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateFeatureForCreate(command.json());
final CreditScorecardFeature scorecardFeature = CreditScorecardFeature.fromJson(command);
this.featureRepository.save(scorecardFeature);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(scorecardFeature.getId()).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
@Override
public CommandProcessingResult updateScoringFeature(Long featureId, JsonCommand command) {
try {
this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateFeatureForUpdate(command.json());
final CreditScorecardFeature featureForUpdate = this.featureRepository.findById(featureId)
.orElseThrow(() -> new FeatureNotFoundException(featureId));
final Map<String, Object> changes = featureForUpdate.update(command);
if (!changes.isEmpty()) {
this.featureRepository.save(featureForUpdate);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(featureId).with(changes).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return CommandProcessingResult.empty();
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
@Transactional
@Override
public CreditScorecard assessCreditRisk(final Loan loan) {
final CreditScorecard scorecard = loan.getScorecard();
if (scorecard != null) {
final String scoringMethod = scorecard.getScoringMethod();
final String scoringModel = scorecard.getScoringModel();
if (scoringMethod.equalsIgnoreCase("ruleBased")) {
final RuleBasedScorecard ruleBasedScorecard = scorecard.getRuleBasedScorecard();
final List<FeatureCriteriaScore> criteriaScores = ruleBasedScorecard.getCriteriaScores();
for (final FeatureCriteriaScore criteriaScore : criteriaScores) {
final LoanProductScorecardFeature lpFeature = criteriaScore.getFeature();
final FeatureConfiguration featureConfig = lpFeature.getFeatureConfiguration();
final EnumOptionData dataType = lpFeature.getScorecardFeature().getDataType();
final List<FeatureCriteria> featureCriteriaList = lpFeature.getFeatureCriteria();
for (final FeatureCriteria featureCriteria : featureCriteriaList) {
final String value = criteriaScore.getValue();
if (value != null) {
if (dataType.getValue().equalsIgnoreCase("string")) {
if (value.equalsIgnoreCase(featureCriteria.getCriteria())) {
final BigDecimal score = featureCriteria.getScore().multiply(featureConfig.getWeightage());
final String color = featureConfig.getColorFromScore(score);
criteriaScore.setScore(score, color);
break;
}
} else if (dataType.getValue().equalsIgnoreCase("numeric")) {
final String criteria = featureCriteria.getCriteria().strip();
final float min = Float.parseFloat(criteria.substring(0, criteria.indexOf("-")).strip());
final float max = Float.parseFloat(criteria.substring(criteria.indexOf("-") + 1).strip());
final float floatValue = Float.parseFloat(value);
if (floatValue >= min && floatValue <= max) {
final BigDecimal score = featureCriteria.getScore().multiply(featureConfig.getWeightage());
final String color = featureConfig.getColorFromScore(score);
criteriaScore.setScore(score, color);
break;
}
}
}
}
}
BigDecimal scorecardScore = BigDecimal.ZERO;
int greenCount = 0;
int amberCount = 0;
int redCount = 0;
for (final FeatureCriteriaScore ctScore : criteriaScores) {
scorecardScore = scorecardScore.add(ctScore.getScore());
if (ctScore.getColor().equalsIgnoreCase("green")) {
greenCount += 1;
} else if (ctScore.getColor().equalsIgnoreCase("amber")) {
amberCount += 1;
} else if (ctScore.getColor().equalsIgnoreCase("red")) {
redCount += 1;
}
}
String scorecardColor = "amber";
if (greenCount > amberCount && greenCount > redCount) {
scorecardColor = "green";
} else if (redCount >= greenCount && redCount >= amberCount) {
scorecardColor = "red";
}
ruleBasedScorecard.setScore(scorecardScore, scorecardColor);
return scorecard;
} else {
this.initScorecardClient();
if (scoringMethod.equalsIgnoreCase("ml")) {
final MLScorecard mlScorecard = scorecard.getMlScorecard();
final MLScorecardFields loanScorecardFields = mlScorecard.getScorecardFields();
final PredictionResponse response = this.fetchScorecard(loanScorecardFields, scoringModel);
if (response != null) {
mlScorecard.setPredictionResponse(BigDecimal.valueOf(response.getProbability()),
response.getLabel(), response.getRequestId());
}
} else if (scoringMethod.equalsIgnoreCase("stat")) {
final StatScorecard statScorecard = scorecard.getStatScorecard();
final MLScorecardFields loanScorecardFields = statScorecard.getScorecardFields();
final PredictionResponse response = this.fetchScorecard(loanScorecardFields, scoringModel);
if (response != null) {
final String method = response.getMethod();
final String color = response.getColor();
final BigDecimal probability = BigDecimal.valueOf(response.getProbability());
BigDecimal wilikis = null;
BigDecimal pillaisTrace = null;
BigDecimal hotelling = null;
BigDecimal roys = null;
if (method.equalsIgnoreCase("manova")) {
wilikis = BigDecimal.valueOf(response.getWilkisLambda());
pillaisTrace = BigDecimal.valueOf(response.getPillaisTrace());
hotelling = BigDecimal.valueOf(response.getHotellingTawley());
roys = BigDecimal.valueOf(response.getRoysReatestRoots());
}
statScorecard.setPredictionResponse(method, color, probability, wilikis, pillaisTrace, hotelling, roys);
}
}
}
} else {
return null;
}
this.scorecardRepository.save(scorecard);
return scorecard;
}
private PredictionResponse fetchScorecard(final MLScorecardFields fields, final String scoringModel) {
try {
final Map<String, Object> predictionData = new HashMap<>();
predictionData.put("age", fields.getAge());
predictionData.put("sex", fields.getSex());
predictionData.put("job", fields.getJob());
predictionData.put("housing", fields.getHousing());
predictionData.put("credit_amount", fields.getCreditAmount());
predictionData.put("duration", fields.getDuration());
predictionData.put("purpose", fields.getPurpose());
return this.scorecardApiClient.algorithmsPredict(scoringModel, "0.0.1", null, null,
predictionData);
} catch (ApiException e) {
LOG.debug("An Error Occurred: {}", e.getLocalizedMessage());
}
return null;
}
@Override
public CommandProcessingResult deleteScoringFeature(Long entityId) {
this.context.authenticatedUser();
final CreditScorecardFeature featureForDelete = this.featureRepository.findById(entityId)
.orElseThrow(() -> new FeatureNotFoundException(entityId));
if (featureForDelete.isDeleted()) {
throw new FeatureNotFoundException(entityId);
}
final Collection<LoanProduct> loanProducts = this.loanProductRepository.retrieveLoanProductsByScorecardFeatureId(entityId);
if (!loanProducts.isEmpty()) {
throw new FeatureCannotBeDeletedException("error.msg.scorecard.feature.cannot.be.deleted.it.is.already.used.in.loan",
"This Scoring Feature cannot be deleted, it is already used in loan");
}
featureForDelete.delete();
this.featureRepository.save(featureForDelete);
return new CommandProcessingResultBuilder().withEntityId(featureForDelete.getId()).build();
}
/*
* Guaranteed to throw an exception no matter what the data integrity issue is.
*/
private void handleDataIntegrityIssues(final JsonCommand command, final Throwable realCause, final Exception dve) {
if (realCause.getMessage().contains("name")) {
final String name = command.stringValueOfParameterNamed("name");
throw new PlatformDataIntegrityException("error.msg.scorecard.feature.duplicate.name",
"Scorecard Feature with name `" + name + "` already exists", "name", name);
}
LOG.error("Error occured.", dve);
throw new PlatformDataIntegrityException("error.msg.scorecard.feature.unknown.data.integrity.issue",
"Unknown data integrity issue with resource: " + realCause.getMessage());
}
}