/** * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ package org.mifosplatform.portfolio.loanproduct.service; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.joda.time.LocalDate; import org.mifosplatform.accounting.producttoaccountmapping.service.ProductToGLAccountMappingWritePlatformService; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.infrastructure.core.data.CommandProcessingResult; import org.mifosplatform.infrastructure.core.data.CommandProcessingResultBuilder; import org.mifosplatform.infrastructure.core.exception.PlatformDataIntegrityException; import org.mifosplatform.infrastructure.entityaccess.domain.MifosEntityAccessType; import org.mifosplatform.infrastructure.entityaccess.domain.MifosEntityType; import org.mifosplatform.infrastructure.entityaccess.service.MifosEntityAccessUtil; import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext; import org.mifosplatform.portfolio.charge.domain.Charge; import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; import org.mifosplatform.portfolio.floatingrates.domain.FloatingRate; import org.mifosplatform.portfolio.floatingrates.domain.FloatingRateRepositoryWrapper; import org.mifosplatform.portfolio.fund.domain.Fund; import org.mifosplatform.portfolio.fund.domain.FundRepository; import org.mifosplatform.portfolio.fund.exception.FundNotFoundException; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransactionProcessingStrategyRepository; import org.mifosplatform.portfolio.loanaccount.exception.LoanTransactionProcessingStrategyNotFoundException; import org.mifosplatform.portfolio.loanaccount.loanschedule.domain.AprCalculator; import org.mifosplatform.portfolio.loanproduct.domain.LoanProduct; import org.mifosplatform.portfolio.loanproduct.domain.LoanProductRepository; import org.mifosplatform.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy; import org.mifosplatform.portfolio.loanproduct.exception.InvalidCurrencyException; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductCannotBeModifiedDueToNonClosedLoansException; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductDateException; import org.mifosplatform.portfolio.loanproduct.exception.LoanProductNotFoundException; import org.mifosplatform.portfolio.loanproduct.serialization.LoanProductDataValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.google.gson.JsonArray; import com.google.gson.JsonObject; @Service public class LoanProductWritePlatformServiceJpaRepositoryImpl implements LoanProductWritePlatformService { private final static Logger logger = 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 ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService; private final MifosEntityAccessUtil mifosEntityAccessUtil; private final FloatingRateRepositoryWrapper floatingRateRepository; private final LoanRepository loanRepository; @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 ProductToGLAccountMappingWritePlatformService accountMappingWritePlatformService, final MifosEntityAccessUtil mifosEntityAccessUtil, final FloatingRateRepositoryWrapper floatingRateRepository, final LoanRepository loanRepository) { this.context = context; this.fromApiJsonDeserializer = fromApiJsonDeserializer; this.loanProductRepository = loanProductRepository; this.aprCalculator = aprCalculator; this.fundRepository = fundRepository; this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository; this.chargeRepository = chargeRepository; this.accountMappingWritePlatformService = accountMappingWritePlatformService; this.mifosEntityAccessUtil = mifosEntityAccessUtil; this.floatingRateRepository = floatingRateRepository; this.loanRepository = loanRepository; } @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); 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); loanproduct.updateLoanProductInRelatedClasses(); this.loanProductRepository.save(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. mifosEntityAccessUtil.checkConfigurationAndAddProductResrictionsForUserOffice( MifosEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS, MifosEntityType.LOAN_PRODUCT, loanproduct.getId()); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanproduct.getId()) // .build(); } catch (final DataIntegrityViolationException dve) { handleDataIntegrityIssues(command, dve); return CommandProcessingResult.empty(); } } private LoanTransactionProcessingStrategy findStrategyByIdIfProvided(final Long transactionProcessingStrategyId) { LoanTransactionProcessingStrategy strategy = null; if (transactionProcessingStrategyId != null) { strategy = this.loanTransactionProcessingStrategyRepository.findOne(transactionProcessingStrategyId); if (strategy == null) { throw new LoanTransactionProcessingStrategyNotFoundException(transactionProcessingStrategyId); } } return strategy; } private Fund findFundByIdIfProvided(final Long fundId) { Fund fund = null; if (fundId != null) { fund = this.fundRepository.findOne(fundId); if (fund == null) { throw 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.findOne(loanProductId); if (product == null) { throw new LoanProductNotFoundException(loanProductId); } this.fromApiJsonDeserializer.validateForUpdate(command.json(), product); validateInputDates(command); if(anyChangeInCriticalFloatingRateLinkedParams(command, product) && this.loanRepository.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.isEmpty()) { this.loanProductRepository.saveAndFlush(product); } return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loanProductId) // .with(changes) // .build(); } catch (final DataIntegrityViolationException dve) { handleDataIntegrityIssues(command, dve); return new CommandProcessingResult(Long.valueOf(-1)); } } 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; } /* * Guaranteed to throw an exception no matter what the data integrity issue * is. */ private void handleDataIntegrityIssues(final JsonCommand command, final DataIntegrityViolationException dve) { final Throwable realCause = dve.getMostSpecificCause(); 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); } 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); } 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); } else if (realCause.getMessage().contains("Duplicate entry")) { final Object[] args = null; throw new PlatformDataIntegrityException("error.msg.product.loan.duplicate.charge", "Loan product may only have one charge of each type.`", "charges", args); } logAsErrorUnexpectedDataIntegrityException(dve); throw new PlatformDataIntegrityException("error.msg.product.loan.unknown.data.integrity.issue", "Unknown data integrity issue with resource."); } 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 DataIntegrityViolationException dve) { logger.error(dve.getMessage(), dve); } }