| /** |
| * 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; |
| } |
| } |