blob: 7bf183a95c516a1b6d714de34aefaf505fd5513c [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.loanproduct.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import java.time.LocalDate;
import java.util.ArrayList;
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.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService;
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.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType;
import org.apache.fineract.infrastructure.entityaccess.service.FineractEntityAccessUtil;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEntity;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEvents;
import org.apache.fineract.portfolio.common.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.creditscorecard.provider.ScorecardServiceProvider;
import org.apache.fineract.portfolio.creditscorecard.service.CreditScorecardAssembler;
import org.apache.fineract.portfolio.floatingrates.domain.FloatingRate;
import org.apache.fineract.portfolio.floatingrates.domain.FloatingRateRepositoryWrapper;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.fund.domain.FundRepository;
import org.apache.fineract.portfolio.fund.exception.FundNotFoundException;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository;
import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
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.apache.fineract.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy;
import org.apache.fineract.portfolio.loanproduct.exception.InvalidCurrencyException;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductCannotBeModifiedDueToNonClosedLoansException;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductDateException;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator;
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.rate.domain.RateRepositoryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanProductWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(LoanProductWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final LoanProductDataValidator fromApiJsonDeserializer;
private final LoanProductRepository loanProductRepository;
private final AprCalculator aprCalculator;
private final FundRepository fundRepository;
private final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository;
private final ChargeRepositoryWrapper chargeRepository;
private final RateRepositoryWrapper rateRepository;
private final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService;
private final FineractEntityAccessUtil fineractEntityAccessUtil;
private final FloatingRateRepositoryWrapper floatingRateRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final BusinessEventNotifierService businessEventNotifierService;
private final ScorecardServiceProvider scorecardServiceProvider;
@Autowired
public LoanProductWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final LoanProductDataValidator fromApiJsonDeserializer, final LoanProductRepository loanProductRepository,
final AprCalculator aprCalculator, final FundRepository fundRepository,
final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository,
final ChargeRepositoryWrapper chargeRepository, final RateRepositoryWrapper rateRepository,
final ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService,
final FineractEntityAccessUtil fineractEntityAccessUtil, final FloatingRateRepositoryWrapper floatingRateRepository,
final LoanRepositoryWrapper loanRepositoryWrapper, final BusinessEventNotifierService businessEventNotifierService,
final ScorecardServiceProvider scorecardServiceProvider) {
this.context = context;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanProductRepository = loanProductRepository;
this.aprCalculator = aprCalculator;
this.fundRepository = fundRepository;
this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository;
this.chargeRepository = chargeRepository;
this.rateRepository = rateRepository;
this.accountMappingWritePlatformService = accountMappingWritePlatformService;
this.fineractEntityAccessUtil = fineractEntityAccessUtil;
this.floatingRateRepository = floatingRateRepository;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.businessEventNotifierService = businessEventNotifierService;
this.scorecardServiceProvider = scorecardServiceProvider;
}
@Transactional
@Override
public CommandProcessingResult createLoanProduct(final JsonCommand command) {
try {
this.context.authenticatedUser();
this.fromApiJsonDeserializer.validateForCreate(command.json());
validateInputDates(command);
final Fund fund = findFundByIdIfProvided(command.longValueOfParameterNamed("fundId"));
final Long transactionProcessingStrategyId = command.longValueOfParameterNamed("transactionProcessingStrategyId");
final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided(
transactionProcessingStrategyId);
final String currencyCode = command.stringValueOfParameterNamed("currencyCode");
final List<Charge> charges = assembleListOfProductCharges(command, currencyCode);
final List<Rate> rates = assembleListOfProductRates(command);
List<LoanProductScorecardFeature> scorecardFeatures = null;
if (command.parameterExists("scorecardFeatures")) {
final JsonArray featuresArray = command.arrayOfParameterNamed("scorecardFeatures");
if (!featuresArray.isEmpty()) {
final String serviceName = "CreditScorecardAssembler";
final CreditScorecardAssembler scorecardService = (CreditScorecardAssembler) this.scorecardServiceProvider
.getScorecardService(serviceName);
if (scorecardService == null) {
throw new PlatformServiceUnavailableException("err.msg.credit.scorecard.service.implementation.missing",
ScorecardServiceProvider.SERVICE_MISSING + serviceName, serviceName);
}
scorecardFeatures = scorecardService.assembleListOfProductScoringFeatures(command, null);
}
}
FloatingRate floatingRate = null;
if (command.parameterExists("floatingRatesId")) {
floatingRate = this.floatingRateRepository
.findOneWithNotFoundDetection(command.longValueOfParameterNamed("floatingRatesId"));
}
final LoanProduct loanproduct = LoanProduct.assembleFromJson(fund, loanTransactionProcessingStrategy, charges, command,
this.aprCalculator, floatingRate, rates, scorecardFeatures);
loanproduct.updateLoanProductInRelatedClasses();
this.loanProductRepository.saveAndFlush(loanproduct);
// save accounting mappings
this.accountMappingWritePlatformService.createLoanProductToGLAccountMapping(loanproduct.getId(), command);
// check if the office specific products are enabled. If yes, then
// save this savings product against a specific office
// i.e. this savings product is specific for this office.
fineractEntityAccessUtil.checkConfigurationAndAddProductResrictionsForUserOffice(
FineractEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS, loanproduct.getId());
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_PRODUCT_CREATE,
constructEntityMap(BusinessEntity.LOAN_PRODUCT, loanproduct));
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_PRODUCT_CREATE,
constructEntityMap(BusinessEntity.LOAN_PRODUCT, loanproduct));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanproduct.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();
}
}
private LoanTransactionProcessingStrategy findStrategyByIdIfProvided(final Long transactionProcessingStrategyId) {
LoanTransactionProcessingStrategy strategy = null;
if (transactionProcessingStrategyId != null) {
return this.loanTransactionProcessingStrategyRepository.findById(transactionProcessingStrategyId)
.orElseThrow(() -> new LoanTransactionProcessingStrategyNotFoundException(transactionProcessingStrategyId));
}
return strategy;
}
private Fund findFundByIdIfProvided(final Long fundId) {
Fund fund = null;
if (fundId != null) {
fund = this.fundRepository.findById(fundId).orElseThrow(() -> new FundNotFoundException(fundId));
}
return fund;
}
@Transactional
@Override
public CommandProcessingResult updateLoanProduct(final Long loanProductId, final JsonCommand command) {
try {
this.context.authenticatedUser();
final LoanProduct product = this.loanProductRepository.findById(loanProductId)
.orElseThrow(() -> new LoanProductNotFoundException(loanProductId));
this.fromApiJsonDeserializer.validateForUpdate(command.json(), product);
validateInputDates(command);
if (anyChangeInCriticalFloatingRateLinkedParams(command, product)
&& this.loanRepositoryWrapper.doNonClosedLoanAccountsExistForProduct(product.getId())) {
throw new LoanProductCannotBeModifiedDueToNonClosedLoansException(product.getId());
}
FloatingRate floatingRate = null;
if (command.parameterExists("floatingRatesId")) {
floatingRate = this.floatingRateRepository
.findOneWithNotFoundDetection(command.longValueOfParameterNamed("floatingRatesId"));
}
final Map<String, Object> changes = product.update(command, this.aprCalculator, floatingRate);
if (changes.containsKey("fundId")) {
final Long fundId = (Long) changes.get("fundId");
final Fund fund = findFundByIdIfProvided(fundId);
product.update(fund);
}
if (changes.containsKey("transactionProcessingStrategyId")) {
final Long transactionProcessingStrategyId = (Long) changes.get("transactionProcessingStrategyId");
final LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided(
transactionProcessingStrategyId);
product.update(loanTransactionProcessingStrategy);
}
if (changes.containsKey("charges")) {
final List<Charge> productCharges = assembleListOfProductCharges(command, product.getCurrency().getCode());
final boolean updated = product.update(productCharges);
if (!updated) {
changes.remove("charges");
}
}
// accounting related changes
final boolean accountingTypeChanged = changes.containsKey("accountingRule");
final Map<String, Object> accountingMappingChanges = this.accountMappingWritePlatformService
.updateLoanProductToGLAccountMapping(product.getId(), command, accountingTypeChanged, product.getAccountingType());
changes.putAll(accountingMappingChanges);
if (changes.containsKey(LoanProductConstants.RATES_PARAM_NAME)) {
final List<Rate> productRates = assembleListOfProductRates(command);
final boolean updated = product.updateRates(productRates);
if (!updated) {
changes.remove(LoanProductConstants.RATES_PARAM_NAME);
}
}
if (changes.containsKey("scorecardFeatures")) {
List<LoanProductScorecardFeature> scorecardFeatures = null;
if (command.parameterExists("scorecardFeatures")) {
final JsonArray featuresArray = command.arrayOfParameterNamed("scorecardFeatures");
if (!featuresArray.isEmpty()) {
final String serviceName = "CreditScorecardAssembler";
final CreditScorecardAssembler scorecardService = (CreditScorecardAssembler) this.scorecardServiceProvider
.getScorecardService(serviceName);
if (scorecardService == null) {
throw new PlatformServiceUnavailableException("err.msg.credit.scorecard.service.implementation.missing",
ScorecardServiceProvider.SERVICE_MISSING + serviceName, serviceName);
}
scorecardFeatures = scorecardService.assembleListOfProductScoringFeatures(command, product);
}
}
final boolean updated = product.updateScorecardFeatures(scorecardFeatures);
if (!updated) {
changes.remove("scorecardFeatures");
}
}
if (!changes.isEmpty()) {
product.validateLoanProductPreSave();
this.loanProductRepository.saveAndFlush(product);
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loanProductId) //
.with(changes) //
.build();
} catch (final DataIntegrityViolationException | JpaSystemException dve) {
handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
return new CommandProcessingResult(Long.valueOf(-1));
} catch (final PersistenceException dve) {
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
handleDataIntegrityIssues(command, throwable, dve);
return CommandProcessingResult.empty();
}
}
private boolean anyChangeInCriticalFloatingRateLinkedParams(JsonCommand command, LoanProduct product) {
final boolean isChangeFromFloatingToFlatOrViceVersa = command.isChangeInBooleanParameterNamed("isLinkedToFloatingInterestRates",
product.isLinkedToFloatingInterestRate());
final boolean isChangeInCriticalFloatingRateParams = product.getFloatingRates() != null
&& (command.isChangeInLongParameterNamed("floatingRatesId", product.getFloatingRates().getFloatingRate().getId())
|| command.isChangeInBigDecimalParameterNamed("interestRateDifferential",
product.getFloatingRates().getInterestRateDifferential()));
return isChangeFromFloatingToFlatOrViceVersa || isChangeInCriticalFloatingRateParams;
}
private List<Charge> assembleListOfProductCharges(final JsonCommand command, final String currencyCode) {
final List<Charge> charges = new ArrayList<>();
String loanProductCurrencyCode = command.stringValueOfParameterNamed("currencyCode");
if (loanProductCurrencyCode == null) {
loanProductCurrencyCode = currencyCode;
}
if (command.parameterExists("charges")) {
final JsonArray chargesArray = command.arrayOfParameterNamed("charges");
if (chargesArray != null) {
for (int i = 0; i < chargesArray.size(); i++) {
final JsonObject jsonObject = chargesArray.get(i).getAsJsonObject();
if (jsonObject.has("id")) {
final Long id = jsonObject.get("id").getAsLong();
final Charge charge = this.chargeRepository.findOneWithNotFoundDetection(id);
if (!loanProductCurrencyCode.equals(charge.getCurrencyCode())) {
final String errorMessage = "Charge and Loan Product must have the same currency.";
throw new InvalidCurrencyException("charge", "attach.to.loan.product", errorMessage);
}
charges.add(charge);
}
}
}
}
return charges;
}
private List<Rate> assembleListOfProductRates(final JsonCommand command) {
final List<Rate> rates = new ArrayList<>();
if (command.parameterExists("rates")) {
final JsonArray ratesArray = command.arrayOfParameterNamed("rates");
if (ratesArray != null) {
List<Long> idList = new ArrayList<>();
for (int i = 0; i < ratesArray.size(); i++) {
final JsonObject jsonObject = ratesArray.get(i).getAsJsonObject();
if (jsonObject.has("id")) {
idList.add(jsonObject.get("id").getAsLong());
}
}
rates.addAll(this.rateRepository.findMultipleWithNotFoundDetection(idList));
}
}
return rates;
}
/*
* 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("'external_id'")) {
final String externalId = command.stringValueOfParameterNamed("externalId");
throw new PlatformDataIntegrityException("error.msg.product.loan.duplicate.externalId",
"Loan Product with externalId `" + externalId + "` already exists", "externalId", externalId, realCause);
} else if (realCause.getMessage().contains("'unq_name'")) {
final String name = command.stringValueOfParameterNamed("name");
throw new PlatformDataIntegrityException("error.msg.product.loan.duplicate.name",
"Loan product with name `" + name + "` already exists", "name", name, realCause);
} else if (realCause.getMessage().contains("'unq_short_name'")) {
final String shortName = command.stringValueOfParameterNamed("shortName");
throw new PlatformDataIntegrityException("error.msg.product.loan.duplicate.short.name",
"Loan product with short name `" + shortName + "` already exists", "shortName", shortName, realCause);
} else if (realCause.getMessage().contains("Duplicate entry")) {
throw new PlatformDataIntegrityException("error.msg.product.loan.duplicate.charge",
"Loan product may only have one charge of each type.`", "charges", realCause);
}
logAsErrorUnexpectedDataIntegrityException(dve);
throw new PlatformDataIntegrityException("error.msg.product.loan.unknown.data.integrity.issue",
"Unknown data integrity issue with resource.", realCause);
}
private void validateInputDates(final JsonCommand command) {
final LocalDate startDate = command.localDateValueOfParameterNamed("startDate");
final LocalDate closeDate = command.localDateValueOfParameterNamed("closeDate");
if (startDate != null && closeDate != null) {
if (closeDate.isBefore(startDate)) {
throw new LoanProductDateException(startDate.toString(), closeDate.toString());
}
}
}
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
LOG.error("Error occured.", dve);
}
private Map<BusinessEventNotificationConstants.BusinessEntity, Object> constructEntityMap(
final BusinessEventNotificationConstants.BusinessEntity entityEvent, Object entity) {
Map<BusinessEventNotificationConstants.BusinessEntity, Object> map = new HashMap<>(1);
map.put(entityEvent, entity);
return map;
}
}