blob: 5e8bc40ce7811b393f7590497fd61d6823ca58e2 [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.loanaccount.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.temporal.ChronoField;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.persistence.PersistenceException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormat;
import org.apache.fineract.infrastructure.accountnumberformat.domain.AccountNumberFormatRepositoryWrapper;
import org.apache.fineract.infrastructure.accountnumberformat.domain.EntityAccountType;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
import org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
import org.apache.fineract.infrastructure.entityaccess.FineractEntityAccessConstants;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityAccessType;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelation;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityRelationRepository;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMapping;
import org.apache.fineract.infrastructure.entityaccess.domain.FineractEntityToEntityMappingRepository;
import org.apache.fineract.infrastructure.entityaccess.exception.NotOfficeSpecificProductException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.staff.domain.Staff;
import org.apache.fineract.portfolio.account.domain.AccountAssociationType;
import org.apache.fineract.portfolio.account.domain.AccountAssociations;
import org.apache.fineract.portfolio.account.domain.AccountAssociationsRepository;
import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType;
import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarRepository;
import org.apache.fineract.portfolio.calendar.domain.CalendarType;
import org.apache.fineract.portfolio.calendar.exception.CalendarNotFoundException;
import org.apache.fineract.portfolio.calendar.service.CalendarReadPlatformService;
import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.client.domain.AccountNumberGenerator;
import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement;
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepository;
import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEntity;
import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BusinessEvents;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.common.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.creditscorecard.provider.ScorecardServiceProvider;
import org.apache.fineract.portfolio.creditscorecard.service.CreditScorecardWritePlatformService;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper;
import org.apache.fineract.portfolio.group.exception.GroupMemberNotFoundInGSIMException;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository;
import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagementRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTopupDetails;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler;
import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionApiJsonValidator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.data.LoanProductData;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRepository;
import org.apache.fineract.portfolio.loanproduct.domain.LoanTransactionProcessingStrategy;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
import org.apache.fineract.portfolio.loanproduct.exception.LoanProductNotFoundException;
import org.apache.fineract.portfolio.loanproduct.serialization.LoanProductDataValidator;
import org.apache.fineract.portfolio.loanproduct.service.LoanProductReadPlatformService;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.rate.service.RateAssembler;
import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler;
import org.apache.fineract.portfolio.savings.service.GSIMReadPlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
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;
import org.springframework.util.CollectionUtils;
@Service
public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements LoanApplicationWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(LoanApplicationWritePlatformServiceJpaRepositoryImpl.class);
private final PlatformSecurityContext context;
private final FromJsonHelper fromJsonHelper;
private final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator;
private final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer;
private final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final NoteRepository noteRepository;
private final LoanScheduleCalculationPlatformService calculationPlatformService;
private final LoanAssembler loanAssembler;
private final ClientRepositoryWrapper clientRepository;
private final LoanProductRepository loanProductRepository;
private final LoanChargeAssembler loanChargeAssembler;
private final LoanCollateralAssembler loanCollateralAssembler;
private final AprCalculator aprCalculator;
private final AccountNumberGenerator accountNumberGenerator;
private final LoanSummaryWrapper loanSummaryWrapper;
private final GroupRepositoryWrapper groupRepository;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final CalendarRepository calendarRepository;
private final CalendarInstanceRepository calendarInstanceRepository;
private final SavingsAccountAssembler savingsAccountAssembler;
private final AccountAssociationsRepository accountAssociationsRepository;
private final LoanReadPlatformService loanReadPlatformService;
private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository;
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final ConfigurationDomainService configurationDomainService;
private final LoanScheduleAssembler loanScheduleAssembler;
private final LoanUtilService loanUtilService;
private final CalendarReadPlatformService calendarReadPlatformService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final GlobalConfigurationRepositoryWrapper globalConfigurationRepository;
private final FineractEntityToEntityMappingRepository repository;
private final FineractEntityRelationRepository fineractEntityRelationRepository;
private final LoanProductReadPlatformService loanProductReadPlatformService;
private final RateAssembler rateAssembler;
private final GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService;
private final GLIMAccountInfoRepository glimRepository;
private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanCollateralManagementRepository loanCollateralManagementRepository;
private final ClientCollateralManagementRepository clientCollateralManagementRepository;
private final ScorecardServiceProvider scorecardServiceProvider;
@Autowired
public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper,
final LoanApplicationTransitionApiJsonValidator loanApplicationTransitionApiJsonValidator,
final LoanApplicationCommandFromApiJsonHelper fromApiJsonDeserializer,
final LoanProductDataValidator loanProductCommandFromApiJsonDeserializer, final AprCalculator aprCalculator,
final LoanAssembler loanAssembler, final LoanChargeAssembler loanChargeAssembler,
final LoanCollateralAssembler loanCollateralAssembler, final LoanRepositoryWrapper loanRepositoryWrapper,
final NoteRepository noteRepository, final LoanScheduleCalculationPlatformService calculationPlatformService,
final ClientRepositoryWrapper clientRepository, final LoanProductRepository loanProductRepository,
final AccountNumberGenerator accountNumberGenerator, final LoanSummaryWrapper loanSummaryWrapper,
final GroupRepositoryWrapper groupRepository,
final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
final CalendarRepository calendarRepository, final CalendarInstanceRepository calendarInstanceRepository,
final SavingsAccountAssembler savingsAccountAssembler, final AccountAssociationsRepository accountAssociationsRepository,
final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository,
final LoanReadPlatformService loanReadPlatformService, final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository,
final BusinessEventNotifierService businessEventNotifierService, final ConfigurationDomainService configurationDomainService,
final LoanScheduleAssembler loanScheduleAssembler, final LoanUtilService loanUtilService,
final CalendarReadPlatformService calendarReadPlatformService,
final GlobalConfigurationRepositoryWrapper globalConfigurationRepository,
final FineractEntityToEntityMappingRepository repository,
final FineractEntityRelationRepository fineractEntityRelationRepository,
final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService,
final GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, final GLIMAccountInfoRepository glimRepository,
final LoanRepository loanRepository, final GSIMReadPlatformService gsimReadPlatformService, final RateAssembler rateAssembler,
final LoanProductReadPlatformService loanProductReadPlatformService,
final LoanCollateralManagementRepository loanCollateralManagementRepository,
final ClientCollateralManagementRepository clientCollateralManagementRepository,
final ScorecardServiceProvider scorecardServiceProvider) {
this.context = context;
this.fromJsonHelper = fromJsonHelper;
this.loanApplicationTransitionApiJsonValidator = loanApplicationTransitionApiJsonValidator;
this.fromApiJsonDeserializer = fromApiJsonDeserializer;
this.loanProductCommandFromApiJsonDeserializer = loanProductCommandFromApiJsonDeserializer;
this.aprCalculator = aprCalculator;
this.loanAssembler = loanAssembler;
this.loanChargeAssembler = loanChargeAssembler;
this.loanCollateralAssembler = loanCollateralAssembler;
this.loanRepositoryWrapper = loanRepositoryWrapper;
this.noteRepository = noteRepository;
this.calculationPlatformService = calculationPlatformService;
this.clientRepository = clientRepository;
this.loanProductRepository = loanProductRepository;
this.accountNumberGenerator = accountNumberGenerator;
this.loanSummaryWrapper = loanSummaryWrapper;
this.groupRepository = groupRepository;
this.loanRepaymentScheduleTransactionProcessorFactory = loanRepaymentScheduleTransactionProcessorFactory;
this.calendarRepository = calendarRepository;
this.calendarInstanceRepository = calendarInstanceRepository;
this.savingsAccountAssembler = savingsAccountAssembler;
this.accountAssociationsRepository = accountAssociationsRepository;
this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository;
this.loanReadPlatformService = loanReadPlatformService;
this.accountNumberFormatRepository = accountNumberFormatRepository;
this.businessEventNotifierService = businessEventNotifierService;
this.configurationDomainService = configurationDomainService;
this.loanScheduleAssembler = loanScheduleAssembler;
this.loanUtilService = loanUtilService;
this.calendarReadPlatformService = calendarReadPlatformService;
this.entityDatatableChecksWritePlatformService = entityDatatableChecksWritePlatformService;
this.globalConfigurationRepository = globalConfigurationRepository;
this.repository = repository;
this.fineractEntityRelationRepository = fineractEntityRelationRepository;
this.loanProductReadPlatformService = loanProductReadPlatformService;
this.rateAssembler = rateAssembler;
this.glimAccountInfoWritePlatformService = glimAccountInfoWritePlatformService;
this.glimRepository = glimRepository;
this.loanRepository = loanRepository;
this.gsimReadPlatformService = gsimReadPlatformService;
this.loanCollateralManagementRepository = loanCollateralManagementRepository;
this.clientCollateralManagementRepository = clientCollateralManagementRepository;
this.scorecardServiceProvider = scorecardServiceProvider;
}
private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() {
final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values());
return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses);
}
@Transactional
@Override
public CommandProcessingResult submitApplication(final JsonCommand command) {
try {
final AppUser currentUser = getAppUserIfPresent();
boolean isMeetingMandatoryForJLGLoans = configurationDomainService.isMeetingMandatoryForJLGLoans();
final Long productId = this.fromJsonHelper.extractLongNamed("productId", command.parsedJson());
final LoanProduct loanProduct = this.loanProductRepository.findById(productId)
.orElseThrow(() -> new LoanProductNotFoundException(productId));
final Long clientId = this.fromJsonHelper.extractLongNamed("clientId", command.parsedJson());
if (clientId != null) {
Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
officeSpecificLoanProductValidation(productId, client.getOffice().getId());
}
final Long groupId = this.fromJsonHelper.extractLongNamed("groupId", command.parsedJson());
if (groupId != null) {
Group group = this.groupRepository.findOneWithNotFoundDetection(groupId);
officeSpecificLoanProductValidation(productId, group.getOffice().getId());
}
this.fromApiJsonDeserializer.validateForCreate(command.json(), isMeetingMandatoryForJLGLoans, loanProduct);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
if (loanProduct.useBorrowerCycle()) {
Integer cycleNumber = 0;
if (clientId != null) {
cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, loanProduct.getId());
} else if (groupId != null) {
cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(),
loanProduct.getId());
}
this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator,
loanProduct, cycleNumber);
} else {
this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator,
loanProduct);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
final Loan newLoanApplication = this.loanAssembler.assembleFrom(command, currentUser);
checkForProductMixRestrictions(newLoanApplication);
validateSubmittedOnDate(newLoanApplication);
final LoanProductRelatedDetail productRelatedDetail = newLoanApplication.repaymentScheduleDetail();
if (loanProduct.getLoanProductConfigurableAttributes() != null) {
updateProductRelatedDetails(productRelatedDetail, newLoanApplication);
}
this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(newLoanApplication.getTermFrequency(),
newLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(),
productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue(),
newLoanApplication);
if (loanProduct.canUseForTopup() && clientId != null) {
final Boolean isTopup = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isTopup);
if (null == isTopup) {
newLoanApplication.setIsTopup(false);
} else {
newLoanApplication.setIsTopup(isTopup);
}
if (newLoanApplication.isTopup()) {
final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose);
final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, clientId);
if (loanToClose == null) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found",
"loanIdToClose is invalid, No Active Loan associated with the given Client ID found.");
}
if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported",
"Topup on loan with multi-tranche disbursal and without interest recalculation is not supported.");
}
final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate();
if (!newLoanApplication.getSubmittedOnDate().isAfter(disbursalDateOfLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date",
"Submitted date of this loan application " + newLoanApplication.getSubmittedOnDate()
+ " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose);
}
if (!loanToClose.getCurrencyCode().equals(newLoanApplication.getCurrencyCode())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency",
"loanIdToClose is invalid, Currency code is different.");
}
final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate();
if (newLoanApplication.getDisbursementDate().isBefore(lastUserTransactionOnLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed",
"Disbursal date of this loan application " + newLoanApplication.getDisbursementDate()
+ " should be after last transaction date of loan to be closed "
+ lastUserTransactionOnLoanToClose);
}
BigDecimal loanOutstanding = this.loanReadPlatformService
.retrieveLoanPrePaymentTemplate(loanIdToClose, newLoanApplication.getDisbursementDate()).getAmount();
final BigDecimal firstDisbursalAmount = newLoanApplication.getFirstDisbursalAmount();
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
final LoanTopupDetails topupDetails = new LoanTopupDetails(newLoanApplication, loanIdToClose);
newLoanApplication.setTopupLoanDetails(topupDetails);
}
}
this.loanRepositoryWrapper.saveAndFlush(newLoanApplication);
if (loanProduct.isInterestRecalculationEnabled()) {
this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(newLoanApplication);
createAndPersistCalendarInstanceForInterestRecalculation(newLoanApplication);
}
// loan account number generation
String accountNumber = "";
GroupLoanIndividualMonitoringAccount glimAccount;
BigDecimal applicationId = BigDecimal.ZERO;
Boolean isLastChildApplication = false;
if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) {
final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository
.findByAccountType(EntityAccountType.LOAN);
// if application is of GLIM type
if (newLoanApplication.getLoanType() == 4) {
Group group = this.groupRepository.findOneWithNotFoundDetection(groupId);
// GLIM specific parameters
if (command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("applicationId") != null) {
applicationId = command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("applicationId");
}
if (command.booleanObjectValueOfParameterNamed("lastApplication") != null) {
isLastChildApplication = command.booleanPrimitiveValueOfParameterNamed("lastApplication");
}
if (command.booleanObjectValueOfParameterNamed("isParentAccount") != null) {
// empty table check
if (glimRepository.count() != 0) {
// **************Parent-Not an empty
// table********************
accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat);
newLoanApplication.updateAccountNo(accountNumber + "1");
glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group,
command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true,
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId);
newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber));
this.loanRepositoryWrapper.save(newLoanApplication);
} else {
// ************** Parent-empty
// table********************
accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat);
newLoanApplication.updateAccountNo(accountNumber + "1");
glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group,
command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true,
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId);
newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber));
this.loanRepositoryWrapper.save(newLoanApplication);
}
} else {
if (glimRepository.count() != 0) {
// Child-Not an empty table
glimAccount = glimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId);
accountNumber = glimAccount.getAccountNumber() + (glimAccount.getChildAccountsCount() + 1);
newLoanApplication.updateAccountNo(accountNumber);
this.glimAccountInfoWritePlatformService.incrementChildAccountCount(glimAccount);
newLoanApplication.setGlim(glimAccount);
this.loanRepositoryWrapper.save(newLoanApplication);
} else {
// **************Child-empty
// table********************
// if the glim info is empty set the current account
// as parent
accountNumber = this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat);
newLoanApplication.updateAccountNo(accountNumber + "1");
glimAccountInfoWritePlatformService.addGLIMAccountInfo(accountNumber, group,
command.bigDecimalValueOfParameterNamedDefaultToNullIfZero("totalLoan"), Long.valueOf(1), true,
LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), applicationId);
newLoanApplication.setGlim(glimRepository.findOneByAccountNumber(accountNumber));
this.loanRepositoryWrapper.save(newLoanApplication);
}
// reset in cases of last child application of glim
if (isLastChildApplication) {
this.glimAccountInfoWritePlatformService
.resetIsAcceptingChild(glimRepository.findOneByIsAcceptingChildAndApplicationId(true, applicationId));
}
}
} else { // for applications other than GLIM
newLoanApplication.updateAccountNo(this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat));
this.loanRepositoryWrapper.saveAndFlush(newLoanApplication);
}
}
final String submittedOnNote = command.stringValueOfParameterNamed("submittedOnNote");
if (StringUtils.isNotBlank(submittedOnNote)) {
final Note note = Note.loanNote(newLoanApplication, submittedOnNote);
this.noteRepository.save(note);
}
// Save calendar instance
final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId));
final CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(),
CalendarEntityType.LOANS.getValue());
this.calendarInstanceRepository.save(calendarInstance);
} else {
final LoanApplicationTerms loanApplicationTerms = this.loanScheduleAssembler.assembleLoanTerms(command.parsedJson());
final Integer repaymentFrequencyNthDayType = command.integerValueOfParameterNamed("repaymentFrequencyNthDayType");
if (loanApplicationTerms.getRepaymentPeriodFrequencyType() == PeriodFrequencyType.MONTHS
&& repaymentFrequencyNthDayType != null) {
final String title = "loan_schedule_" + newLoanApplication.getId();
LocalDate calendarStartDate = loanApplicationTerms.getRepaymentsStartingFromLocalDate();
if (calendarStartDate == null) {
calendarStartDate = loanApplicationTerms.getExpectedDisbursementDate();
}
final CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.MONTHLY;
final Integer frequency = loanApplicationTerms.getRepaymentEvery();
final Integer repeatsOnDay = loanApplicationTerms.getWeekDayType().getValue();
final Integer repeatsOnNthDayOfMonth = loanApplicationTerms.getNthDay();
final Integer calendarEntityType = CalendarEntityType.LOANS.getValue();
final Calendar loanCalendar = Calendar.createRepeatingCalendar(title, calendarStartDate,
CalendarType.COLLECTION.getValue(), calendarFrequencyType, frequency, repeatsOnDay, repeatsOnNthDayOfMonth);
this.calendarRepository.save(loanCalendar);
final CalendarInstance calendarInstance = CalendarInstance.from(loanCalendar, newLoanApplication.getId(),
calendarEntityType);
this.calendarInstanceRepository.save(calendarInstance);
}
}
if (newLoanApplication.hasScorecard()) {
final String serviceName = "CreditScorecardWritePlatformService";
final CreditScorecardWritePlatformService scorecardService = (CreditScorecardWritePlatformService) this.scorecardServiceProvider
.getScorecardService(serviceName);
if (scorecardService == null) {
throw new PlatformServiceUnavailableException("err.msg.credit.scorecard.service.implementation.missing",
ScorecardServiceProvider.SERVICE_MISSING + serviceName, serviceName);
}
scorecardService.assessCreditRisk(newLoanApplication);
}
// Save linked account information
SavingsAccount savingsAccount;
final boolean backdatedTxnsAllowedTill = false;
AccountAssociations accountAssociations;
final Long savingsAccountId = command.longValueOfParameterNamed("linkAccountId");
if (savingsAccountId != null) {
if (newLoanApplication.getLoanType() == 4) {
List<GroupSavingsIndividualMonitoringAccountData> childSavings = (List<GroupSavingsIndividualMonitoringAccountData>) gsimReadPlatformService
.findGSIMAccountsByGSIMId(savingsAccountId);
// List<SavingsAccountSummaryData>
// childSavings=gsimAccount.getChildGSIMAccounts();
List<BigDecimal> gsimClientMembers = new ArrayList<BigDecimal>();
Map<BigDecimal, BigDecimal> clientAccountMappings = new HashMap<>();
for (GroupSavingsIndividualMonitoringAccountData childSaving : childSavings) {
gsimClientMembers.add(childSaving.getClientId());
clientAccountMappings.put(childSaving.getClientId(), childSaving.getChildAccountId());
}
if (gsimClientMembers.contains(BigDecimal.valueOf(newLoanApplication.getClientId()))) {
savingsAccount = this.savingsAccountAssembler.assembleFrom(
clientAccountMappings.get(BigDecimal.valueOf(newLoanApplication.getClientId())).longValue(),
backdatedTxnsAllowedTill);
this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication);
boolean isActive = true;
accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, savingsAccount,
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive);
this.accountAssociationsRepository.save(accountAssociations);
} else {
throw new GroupMemberNotFoundInGSIMException(newLoanApplication.getClientId());
}
} else {
savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId, backdatedTxnsAllowedTill);
this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication);
boolean isActive = true;
accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, savingsAccount,
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive);
this.accountAssociationsRepository.save(accountAssociations);
}
}
if (command.parameterExists(LoanApiConstants.datatables)) {
this.entityDatatableChecksWritePlatformService.saveDatatables(StatusEnum.CREATE.getCode().longValue(),
EntityTables.LOAN.getName(), newLoanApplication.getId(), newLoanApplication.productId(),
command.arrayOfParameterNamed(LoanApiConstants.datatables));
}
loanRepositoryWrapper.flush();
this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(newLoanApplication.getId(), EntityTables.LOAN.getName(),
StatusEnum.CREATE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(),
newLoanApplication.productId());
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_CREATE,
constructEntityMap(BusinessEntity.LOAN, newLoanApplication));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(newLoanApplication.getId()) //
.withOfficeId(newLoanApplication.getOfficeId()) //
.withClientId(newLoanApplication.getClientId()) //
.withGroupId(newLoanApplication.getGroupId()) //
.withLoanId(newLoanApplication.getId()).withGlimId(newLoanApplication.getGlimId()).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();
}
}
public void checkForProductMixRestrictions(final Loan loan) {
final List<Long> activeLoansLoanProductIds;
final Long productId = loan.loanProduct().getId();
if (loan.isGroupLoan()) {
activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByGroup(loan.getGroupId(),
LoanStatus.ACTIVE.getValue());
} else {
activeLoansLoanProductIds = this.loanRepositoryWrapper.findActiveLoansLoanProductIdsByClient(loan.getClientId(),
LoanStatus.ACTIVE.getValue());
}
checkForProductMixRestrictions(activeLoansLoanProductIds, productId, loan.loanProduct().productName());
}
private void checkForProductMixRestrictions(final List<Long> activeLoansLoanProductIds, final Long productId,
final String productName) {
if (!CollectionUtils.isEmpty(activeLoansLoanProductIds)) {
final Collection<LoanProductData> restrictedPrdouctsList = this.loanProductReadPlatformService
.retrieveRestrictedProductsForMix(productId);
for (final LoanProductData restrictedProduct : restrictedPrdouctsList) {
if (activeLoansLoanProductIds.contains(restrictedProduct.getId())) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.applied.or.to.be.disbursed.can.not.co-exist.with.the.loan.already.active.to.this.client",
"This loan could not be applied/disbursed as the loan and `" + restrictedProduct
+ "` are not allowed to co-exist");
}
}
}
}
private void updateProductRelatedDetails(LoanProductRelatedDetail productRelatedDetail, Loan loan) {
final Boolean amortization = loan.loanProduct().getLoanProductConfigurableAttributes().getAmortizationBoolean();
final Boolean arrearsTolerance = loan.loanProduct().getLoanProductConfigurableAttributes().getArrearsToleranceBoolean();
final Boolean graceOnArrearsAging = loan.loanProduct().getLoanProductConfigurableAttributes().getGraceOnArrearsAgingBoolean();
final Boolean interestCalcPeriod = loan.loanProduct().getLoanProductConfigurableAttributes().getInterestCalcPeriodBoolean();
final Boolean interestMethod = loan.loanProduct().getLoanProductConfigurableAttributes().getInterestMethodBoolean();
final Boolean graceOnPrincipalAndInterestPayment = loan.loanProduct().getLoanProductConfigurableAttributes()
.getGraceOnPrincipalAndInterestPaymentBoolean();
final Boolean repaymentEvery = loan.loanProduct().getLoanProductConfigurableAttributes().getRepaymentEveryBoolean();
final Boolean transactionProcessingStrategy = loan.loanProduct().getLoanProductConfigurableAttributes()
.getTransactionProcessingStrategyBoolean();
if (!amortization) {
productRelatedDetail.setAmortizationMethod(loan.loanProduct().getLoanProductRelatedDetail().getAmortizationMethod());
}
if (!arrearsTolerance) {
productRelatedDetail.setInArrearsTolerance(loan.loanProduct().getLoanProductRelatedDetail().getArrearsTolerance());
}
if (!graceOnArrearsAging) {
productRelatedDetail.setGraceOnArrearsAgeing(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnArrearsAgeing());
}
if (!interestCalcPeriod) {
productRelatedDetail.setInterestCalculationPeriodMethod(
loan.loanProduct().getLoanProductRelatedDetail().getInterestCalculationPeriodMethod());
}
if (!interestMethod) {
productRelatedDetail.setInterestMethod(loan.loanProduct().getLoanProductRelatedDetail().getInterestMethod());
}
if (!graceOnPrincipalAndInterestPayment) {
productRelatedDetail.setGraceOnInterestPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnInterestPayment());
productRelatedDetail.setGraceOnPrincipalPayment(loan.loanProduct().getLoanProductRelatedDetail().getGraceOnPrincipalPayment());
}
if (!repaymentEvery) {
productRelatedDetail.setRepayEvery(loan.loanProduct().getLoanProductRelatedDetail().getRepayEvery());
}
if (!transactionProcessingStrategy) {
loan.updateTransactionProcessingStrategy(loan.loanProduct().getRepaymentStrategy());
}
}
private void createAndPersistCalendarInstanceForInterestRecalculation(final Loan loan) {
LocalDate calendarStartDate = loan.getExpectedDisbursedOnLocalDate();
Integer repeatsOnDay = null;
final RecalculationFrequencyType recalculationFrequencyType = loan.loanInterestRecalculationDetails().getRestFrequencyType();
Integer recalculationFrequencyNthDay = loan.loanInterestRecalculationDetails().getRestFrequencyOnDay();
if (recalculationFrequencyNthDay == null) {
recalculationFrequencyNthDay = loan.loanInterestRecalculationDetails().getRestFrequencyNthDay();
repeatsOnDay = loan.loanInterestRecalculationDetails().getRestFrequencyWeekday();
}
Integer frequency = loan.loanInterestRecalculationDetails().getRestInterval();
CalendarEntityType calendarEntityType = CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL;
final String title = "loan_recalculation_detail_" + loan.loanInterestRecalculationDetails().getId();
createCalendar(loan, calendarStartDate, recalculationFrequencyNthDay, repeatsOnDay, recalculationFrequencyType, frequency,
calendarEntityType, title);
if (loan.loanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod().isCompoundingEnabled()) {
LocalDate compoundingStartDate = loan.getExpectedDisbursedOnLocalDate();
Integer compoundingRepeatsOnDay = null;
final RecalculationFrequencyType recalculationCompoundingFrequencyType = loan.loanInterestRecalculationDetails()
.getCompoundingFrequencyType();
Integer recalculationCompoundingFrequencyNthDay = loan.loanInterestRecalculationDetails().getCompoundingFrequencyOnDay();
if (recalculationCompoundingFrequencyNthDay == null) {
recalculationCompoundingFrequencyNthDay = loan.loanInterestRecalculationDetails().getCompoundingFrequencyNthDay();
compoundingRepeatsOnDay = loan.loanInterestRecalculationDetails().getCompoundingFrequencyWeekday();
}
Integer compoundingFrequency = loan.loanInterestRecalculationDetails().getCompoundingInterval();
CalendarEntityType compoundingCalendarEntityType = CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL;
final String compoundingCalendarTitle = "loan_recalculation_detail_compounding_frequency"
+ loan.loanInterestRecalculationDetails().getId();
createCalendar(loan, compoundingStartDate, recalculationCompoundingFrequencyNthDay, compoundingRepeatsOnDay,
recalculationCompoundingFrequencyType, compoundingFrequency, compoundingCalendarEntityType, compoundingCalendarTitle);
}
}
private void createCalendar(final Loan loan, LocalDate calendarStartDate, Integer recalculationFrequencyNthDay,
final Integer repeatsOnDay, final RecalculationFrequencyType recalculationFrequencyType, Integer frequency,
CalendarEntityType calendarEntityType, final String title) {
CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.INVALID;
Integer updatedRepeatsOnDay = repeatsOnDay;
switch (recalculationFrequencyType) {
case DAILY:
calendarFrequencyType = CalendarFrequencyType.DAILY;
break;
case MONTHLY:
calendarFrequencyType = CalendarFrequencyType.MONTHLY;
break;
case SAME_AS_REPAYMENT_PERIOD:
frequency = loan.repaymentScheduleDetail().getRepayEvery();
calendarFrequencyType = CalendarFrequencyType.from(loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType());
calendarStartDate = loan.getExpectedDisbursedOnLocalDate();
if (updatedRepeatsOnDay == null) {
updatedRepeatsOnDay = calendarStartDate.get(ChronoField.DAY_OF_WEEK);
}
break;
case WEEKLY:
calendarFrequencyType = CalendarFrequencyType.WEEKLY;
break;
default:
break;
}
final Calendar calendar = Calendar.createRepeatingCalendar(title, calendarStartDate, CalendarType.COLLECTION.getValue(),
calendarFrequencyType, frequency, updatedRepeatsOnDay, recalculationFrequencyNthDay);
final CalendarInstance calendarInstance = CalendarInstance.from(calendar, loan.loanInterestRecalculationDetails().getId(),
calendarEntityType.getValue());
this.calendarInstanceRepository.save(calendarInstance);
}
@Transactional
@Override
public CommandProcessingResult modifyApplication(final Long loanId, final JsonCommand command) {
try {
AppUser currentUser = getAppUserIfPresent();
final Loan existingLoanApplication = retrieveLoanBy(loanId);
if (!existingLoanApplication.isSubmittedAndPendingApproval()) {
throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeModified(loanId);
}
final String productIdParamName = "productId";
LoanProduct newLoanProduct = null;
if (command.isChangeInLongParameterNamed(productIdParamName, existingLoanApplication.loanProduct().getId())) {
final Long productId = command.longValueOfParameterNamed(productIdParamName);
newLoanProduct = this.loanProductRepository.findById(productId)
.orElseThrow(() -> new LoanProductNotFoundException(productId));
}
LoanProduct loanProductForValidations = newLoanProduct == null ? existingLoanApplication.loanProduct() : newLoanProduct;
this.fromApiJsonDeserializer.validateForModify(command.json(), loanProductForValidations, existingLoanApplication);
checkClientOrGroupActive(existingLoanApplication);
final Set<LoanCharge> existingCharges = existingLoanApplication.charges();
Map<Long, LoanChargeData> chargesMap = new HashMap<>();
for (LoanCharge charge : existingCharges) {
LoanChargeData chargeData = new LoanChargeData(charge.getId(), charge.getDueLocalDate(), charge.amountOrPercentage());
chargesMap.put(charge.getId(), chargeData);
}
List<LoanDisbursementDetails> disbursementDetails = this.loanUtilService
.fetchDisbursementData(command.parsedJson().getAsJsonObject());
/**
* Stores all charges which are passed in during modify loan application
**/
final Set<LoanCharge> possiblyModifedLoanCharges = this.loanChargeAssembler.fromParsedJson(command.parsedJson(),
disbursementDetails);
/** Boolean determines if any charge has been modified **/
boolean isChargeModified = false;
Set<Charge> newTrancheChages = this.loanChargeAssembler.getNewLoanTrancheCharges(command.parsedJson());
for (Charge charge : newTrancheChages) {
existingLoanApplication.addTrancheLoanCharge(charge);
}
/**
* If there are any charges already present, which are now not passed in as a part of the request, deem the
* charges as modified
**/
if (!possiblyModifedLoanCharges.isEmpty()) {
if (!possiblyModifedLoanCharges.containsAll(existingCharges)) {
isChargeModified = true;
}
}
/**
* If any new charges are added or values of existing charges are modified
**/
for (LoanCharge loanCharge : possiblyModifedLoanCharges) {
if (loanCharge.getId() == null) {
isChargeModified = true;
} else {
LoanChargeData chargeData = chargesMap.get(loanCharge.getId());
if (loanCharge.amountOrPercentage().compareTo(chargeData.amountOrPercentage()) != 0
|| (loanCharge.isSpecifiedDueDate() && !loanCharge.getDueLocalDate().equals(chargeData.getDueDate()))) {
isChargeModified = true;
}
}
}
Set<LoanCollateralManagement> possiblyModifedLoanCollateralItems = null;
if (command.parameterExists("loanType")) {
final String loanTypeStr = command.stringValueOfParameterNamed("loanType");
final AccountType loanType = AccountType.fromName(loanTypeStr);
if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) {
possiblyModifedLoanCollateralItems = this.loanCollateralAssembler.fromParsedJson(command.parsedJson());
}
}
final Map<String, Object> changes = existingLoanApplication.loanApplicationModification(command, possiblyModifedLoanCharges,
possiblyModifedLoanCollateralItems, this.aprCalculator, isChargeModified, loanProductForValidations);
if (changes.containsKey("expectedDisbursementDate")) {
this.loanAssembler.validateExpectedDisbursementForHolidayAndNonWorkingDay(existingLoanApplication);
}
final String clientIdParamName = "clientId";
if (changes.containsKey(clientIdParamName)) {
final Long clientId = command.longValueOfParameterNamed(clientIdParamName);
final Client client = this.clientRepository.findOneWithNotFoundDetection(clientId);
if (client.isNotActive()) {
throw new ClientNotActiveException(clientId);
}
existingLoanApplication.updateClient(client);
}
final String groupIdParamName = "groupId";
if (changes.containsKey(groupIdParamName)) {
final Long groupId = command.longValueOfParameterNamed(groupIdParamName);
final Group group = this.groupRepository.findOneWithNotFoundDetection(groupId);
if (group.isNotActive()) {
throw new GroupNotActiveException(groupId);
}
existingLoanApplication.updateGroup(group);
}
if (newLoanProduct != null) {
existingLoanApplication.updateLoanProduct(newLoanProduct);
if (!changes.containsKey("interestRateFrequencyType")) {
existingLoanApplication.updateInterestRateFrequencyType();
}
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
if (newLoanProduct.useBorrowerCycle()) {
final Long clientId = this.fromJsonHelper.extractLongNamed("clientId", command.parsedJson());
final Long groupId = this.fromJsonHelper.extractLongNamed("groupId", command.parsedJson());
Integer cycleNumber = 0;
if (clientId != null) {
cycleNumber = this.loanReadPlatformService.retriveLoanCounter(clientId, newLoanProduct.getId());
} else if (groupId != null) {
cycleNumber = this.loanReadPlatformService.retriveLoanCounter(groupId, AccountType.GROUP.getValue(),
newLoanProduct.getId());
}
this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator,
newLoanProduct, cycleNumber);
} else {
this.loanProductCommandFromApiJsonDeserializer.validateMinMaxConstraints(command.parsedJson(), baseDataValidator,
newLoanProduct);
}
if (newLoanProduct.isLinkedToFloatingInterestRate()) {
existingLoanApplication.getLoanProductRelatedDetail().updateForFloatingInterestRates();
} else {
existingLoanApplication.setInterestRateDifferential(null);
existingLoanApplication.setIsFloatingInterestRate(null);
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
existingLoanApplication.updateIsInterestRecalculationEnabled();
validateSubmittedOnDate(existingLoanApplication);
final LoanProductRelatedDetail productRelatedDetail = existingLoanApplication.repaymentScheduleDetail();
if (existingLoanApplication.loanProduct().getLoanProductConfigurableAttributes() != null) {
updateProductRelatedDetails(productRelatedDetail, existingLoanApplication);
}
if (existingLoanApplication.getLoanProduct().canUseForTopup() && existingLoanApplication.getClientId() != null) {
final Boolean isTopup = command.booleanObjectValueOfParameterNamed(LoanApiConstants.isTopup);
if (command.isChangeInBooleanParameterNamed(LoanApiConstants.isTopup, existingLoanApplication.isTopup())) {
existingLoanApplication.setIsTopup(isTopup);
changes.put(LoanApiConstants.isTopup, isTopup);
}
if (existingLoanApplication.isTopup()) {
final Long loanIdToClose = command.longValueOfParameterNamed(LoanApiConstants.loanIdToClose);
LoanTopupDetails existingLoanTopupDetails = existingLoanApplication.getTopupLoanDetails();
if (existingLoanTopupDetails == null
|| (existingLoanTopupDetails != null && !existingLoanTopupDetails.getLoanIdToClose().equals(loanIdToClose))
|| changes.containsKey("submittedOnDate") || changes.containsKey("expectedDisbursementDate")
|| changes.containsKey("principal") || changes.containsKey(LoanApiConstants.disbursementDataParameterName)) {
Long existingLoanIdToClose = null;
if (existingLoanTopupDetails != null) {
existingLoanIdToClose = existingLoanTopupDetails.getLoanIdToClose();
}
final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose,
existingLoanApplication.getClientId());
if (loanToClose == null) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found",
"loanIdToClose is invalid, No Active Loan associated with the given Client ID found.");
}
if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported",
"Topup on loan with multi-tranche disbursal and without interest recalculation is not supported.");
}
final LocalDate disbursalDateOfLoanToClose = loanToClose.getDisbursementDate();
if (!existingLoanApplication.getSubmittedOnDate().isAfter(disbursalDateOfLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.submitted.date.should.be.after.topup.loan.disbursal.date",
"Submitted date of this loan application " + existingLoanApplication.getSubmittedOnDate()
+ " should be after the disbursed date of loan to be closed " + disbursalDateOfLoanToClose);
}
if (!loanToClose.getCurrencyCode().equals(existingLoanApplication.getCurrencyCode())) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.has.different.currency",
"loanIdToClose is invalid, Currency code is different.");
}
final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate();
if (existingLoanApplication.getDisbursementDate().isBefore(lastUserTransactionOnLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed",
"Disbursal date of this loan application " + existingLoanApplication.getDisbursementDate()
+ " should be after last transaction date of loan to be closed "
+ lastUserTransactionOnLoanToClose);
}
BigDecimal loanOutstanding = this.loanReadPlatformService
.retrieveLoanPrePaymentTemplate(loanIdToClose, existingLoanApplication.getDisbursementDate()).getAmount();
final BigDecimal firstDisbursalAmount = existingLoanApplication.getFirstDisbursalAmount();
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
if (!existingLoanIdToClose.equals(loanIdToClose)) {
final LoanTopupDetails topupDetails = new LoanTopupDetails(existingLoanApplication, loanIdToClose);
existingLoanApplication.setTopupLoanDetails(topupDetails);
changes.put(LoanApiConstants.loanIdToClose, loanIdToClose);
}
}
} else {
existingLoanApplication.setTopupLoanDetails(null);
}
} else {
if (existingLoanApplication.isTopup()) {
existingLoanApplication.setIsTopup(false);
existingLoanApplication.setTopupLoanDetails(null);
changes.put(LoanApiConstants.isTopup, false);
}
}
final String fundIdParamName = "fundId";
if (changes.containsKey(fundIdParamName)) {
final Long fundId = command.longValueOfParameterNamed(fundIdParamName);
final Fund fund = this.loanAssembler.findFundByIdIfProvided(fundId);
existingLoanApplication.updateFund(fund);
}
final String loanPurposeIdParamName = "loanPurposeId";
if (changes.containsKey(loanPurposeIdParamName)) {
final Long loanPurposeId = command.longValueOfParameterNamed(loanPurposeIdParamName);
final CodeValue loanPurpose = this.loanAssembler.findCodeValueByIdIfProvided(loanPurposeId);
existingLoanApplication.updateLoanPurpose(loanPurpose);
}
final String loanOfficerIdParamName = "loanOfficerId";
if (changes.containsKey(loanOfficerIdParamName)) {
final Long loanOfficerId = command.longValueOfParameterNamed(loanOfficerIdParamName);
final Staff newValue = this.loanAssembler.findLoanOfficerByIdIfProvided(loanOfficerId);
existingLoanApplication.updateLoanOfficerOnLoanApplication(newValue);
}
final String strategyIdParamName = "transactionProcessingStrategyId";
if (changes.containsKey(strategyIdParamName)) {
final Long strategyId = command.longValueOfParameterNamed(strategyIdParamName);
final LoanTransactionProcessingStrategy strategy = this.loanAssembler.findStrategyByIdIfProvided(strategyId);
existingLoanApplication.updateTransactionProcessingStrategy(strategy);
}
/**
* TODO: Allow other loan types if needed.
*/
if (command.parameterExists("loanType")) {
final String loanTypeStr = command.stringValueOfParameterNamed("loanType");
final AccountType loanType = AccountType.fromName(loanTypeStr);
if (!StringUtils.isBlank(loanTypeStr) && loanType.isIndividualAccount()) {
final String collateralParamName = "collateral";
if (changes.containsKey(collateralParamName)) {
existingLoanApplication.updateLoanCollateral(possiblyModifedLoanCollateralItems);
}
}
}
final String chargesParamName = "charges";
if (changes.containsKey(chargesParamName)) {
existingLoanApplication.updateLoanCharges(possiblyModifedLoanCharges);
}
if (changes.containsKey("recalculateLoanSchedule")) {
changes.remove("recalculateLoanSchedule");
final JsonElement parsedQuery = this.fromJsonHelper.parse(command.json());
final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromJsonHelper);
final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false);
existingLoanApplication.updateLoanSchedule(loanSchedule, currentUser);
existingLoanApplication.recalculateAllCharges();
}
// Changes to modify loan rates.
if (command.hasParameter(LoanProductConstants.RATES_PARAM_NAME)) {
existingLoanApplication.updateLoanRates(rateAssembler.fromParsedJson(command.parsedJson()));
}
this.fromApiJsonDeserializer.validateLoanTermAndRepaidEveryValues(existingLoanApplication.getTermFrequency(),
existingLoanApplication.getTermPeriodFrequencyType(), productRelatedDetail.getNumberOfRepayments(),
productRelatedDetail.getRepayEvery(), productRelatedDetail.getRepaymentPeriodFrequencyType().getValue(),
existingLoanApplication);
saveAndFlushLoanWithDataIntegrityViolationChecks(existingLoanApplication);
final String submittedOnNote = command.stringValueOfParameterNamed("submittedOnNote");
if (StringUtils.isNotBlank(submittedOnNote)) {
final Note note = Note.loanNote(existingLoanApplication, submittedOnNote);
this.noteRepository.save(note);
}
final Long calendarId = command.longValueOfParameterNamed("calendarId");
Calendar calendar = null;
if (calendarId != null && calendarId != 0) {
calendar = this.calendarRepository.findById(calendarId).orElseThrow(() -> new CalendarNotFoundException(calendarId));
}
final List<CalendarInstance> ciList = (List<CalendarInstance>) this.calendarInstanceRepository
.findByEntityIdAndEntityTypeId(loanId, CalendarEntityType.LOANS.getValue());
if (calendar != null) {
// For loans, allow to attach only one calendar instance per
// loan
if (ciList != null && !ciList.isEmpty()) {
final CalendarInstance calendarInstance = ciList.get(0);
final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity(
calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(),
CalendarEntityType.LOANS.getValue().longValue());
if (isCalendarAssociatedWithEntity && calendarId == null) {
this.calendarRepository.delete(calendarInstance.getCalendar());
}
if (!calendarInstance.getCalendar().getId().equals(calendar.getId())) {
calendarInstance.updateCalendar(calendar);
this.calendarInstanceRepository.saveAndFlush(calendarInstance);
}
} else {
// attaching new calendar
final CalendarInstance calendarInstance = new CalendarInstance(calendar, existingLoanApplication.getId(),
CalendarEntityType.LOANS.getValue());
this.calendarInstanceRepository.save(calendarInstance);
}
} else {
if (ciList != null && !ciList.isEmpty()) {
final CalendarInstance existingCalendarInstance = ciList.get(0);
final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity(
existingCalendarInstance.getEntityId(), existingCalendarInstance.getCalendar().getId(),
CalendarEntityType.GROUPS.getValue().longValue());
if (isCalendarAssociatedWithEntity) {
this.calendarInstanceRepository.delete(existingCalendarInstance);
}
}
if (changes.containsKey("repaymentFrequencyNthDayType") || changes.containsKey("repaymentFrequencyDayOfWeekType")) {
if (changes.get("repaymentFrequencyNthDayType") == null) {
if (ciList != null && !ciList.isEmpty()) {
final CalendarInstance calendarInstance = ciList.get(0);
final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService.isCalendarAssociatedWithEntity(
calendarInstance.getEntityId(), calendarInstance.getCalendar().getId(),
CalendarEntityType.LOANS.getValue().longValue());
if (isCalendarAssociatedWithEntity) {
this.calendarInstanceRepository.delete(calendarInstance);
this.calendarRepository.delete(calendarInstance.getCalendar());
}
}
} else {
Integer repaymentFrequencyTypeInt = command.integerValueOfParameterNamed("repaymentFrequencyType");
if (repaymentFrequencyTypeInt != null) {
if (PeriodFrequencyType.fromInt(repaymentFrequencyTypeInt) == PeriodFrequencyType.MONTHS) {
final String title = "loan_schedule_" + existingLoanApplication.getId();
final Integer typeId = CalendarType.COLLECTION.getValue();
final CalendarFrequencyType repaymentFrequencyType = CalendarFrequencyType.MONTHLY;
final Integer interval = command.integerValueOfParameterNamed("repaymentEvery");
LocalDate startDate = command.localDateValueOfParameterNamed("repaymentsStartingFromDate");
if (startDate == null) {
startDate = command.localDateValueOfParameterNamed("expectedDisbursementDate");
}
final Calendar newCalendar = Calendar.createRepeatingCalendar(title, startDate, typeId,
repaymentFrequencyType, interval, (Integer) changes.get("repaymentFrequencyDayOfWeekType"),
(Integer) changes.get("repaymentFrequencyNthDayType"));
if (ciList != null && !ciList.isEmpty()) {
final CalendarInstance calendarInstance = ciList.get(0);
final boolean isCalendarAssociatedWithEntity = this.calendarReadPlatformService
.isCalendarAssociatedWithEntity(calendarInstance.getEntityId(),
calendarInstance.getCalendar().getId(),
CalendarEntityType.LOANS.getValue().longValue());
if (isCalendarAssociatedWithEntity) {
final Calendar existingCalendar = calendarInstance.getCalendar();
if (existingCalendar != null) {
String existingRecurrence = existingCalendar.getRecurrence();
if (!existingRecurrence.equals(newCalendar.getRecurrence())) {
existingCalendar.setRecurrence(newCalendar.getRecurrence());
this.calendarRepository.save(existingCalendar);
}
}
}
} else {
this.calendarRepository.save(newCalendar);
final Integer calendarEntityType = CalendarEntityType.LOANS.getValue();
final CalendarInstance calendarInstance = new CalendarInstance(newCalendar,
existingLoanApplication.getId(), calendarEntityType);
this.calendarInstanceRepository.save(calendarInstance);
}
}
}
}
}
}
// Save linked account information
final String linkAccountIdParamName = "linkAccountId";
final boolean backdatedTxnsAllowedTill = false;
final Long savingsAccountId = command.longValueOfParameterNamed(linkAccountIdParamName);
AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loanId,
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue());
boolean isLinkedAccPresent = false;
if (savingsAccountId == null) {
if (accountAssociations != null) {
if (this.fromJsonHelper.parameterExists(linkAccountIdParamName, command.parsedJson())) {
this.accountAssociationsRepository.delete(accountAssociations);
changes.put(linkAccountIdParamName, null);
} else {
isLinkedAccPresent = true;
}
}
} else {
isLinkedAccPresent = true;
boolean isModified = false;
if (accountAssociations == null) {
isModified = true;
} else {
final SavingsAccount savingsAccount = accountAssociations.linkedSavingsAccount();
if (savingsAccount == null || !savingsAccount.getId().equals(savingsAccountId)) {
isModified = true;
}
}
if (isModified) {
final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId,
backdatedTxnsAllowedTill);
this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, existingLoanApplication);
if (accountAssociations == null) {
boolean isActive = true;
accountAssociations = AccountAssociations.associateSavingsAccount(existingLoanApplication, savingsAccount,
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive);
} else {
accountAssociations.updateLinkedSavingsAccount(savingsAccount);
}
changes.put(linkAccountIdParamName, savingsAccountId);
this.accountAssociationsRepository.save(accountAssociations);
}
}
if (!isLinkedAccPresent) {
final Set<LoanCharge> charges = existingLoanApplication.charges();
for (final LoanCharge loanCharge : charges) {
if (loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
final String errorMessage = "one of the charges requires linked savings account for payment";
throw new LinkedAccountRequiredException("loanCharge", errorMessage);
}
}
}
if ((command.longValueOfParameterNamed(productIdParamName) != null)
|| (command.longValueOfParameterNamed(clientIdParamName) != null)
|| (command.longValueOfParameterNamed(groupIdParamName) != null)) {
Long OfficeId = null;
if (existingLoanApplication.getClient() != null) {
OfficeId = existingLoanApplication.getClient().getOffice().getId();
} else if (existingLoanApplication.getGroup() != null) {
OfficeId = existingLoanApplication.getGroup().getOffice().getId();
}
officeSpecificLoanProductValidation(existingLoanApplication.getLoanProduct().getId(), OfficeId);
}
// updating loan interest recalculation details throwing null
// pointer exception after saveAndFlush
// http://stackoverflow.com/questions/17151757/hibernate-cascade-update-gives-null-pointer/17334374#17334374
this.loanRepositoryWrapper.saveAndFlush(existingLoanApplication);
if (productRelatedDetail.isInterestRecalculationEnabled()) {
this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(existingLoanApplication);
if (changes.containsKey(LoanProductConstants.IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME)) {
createAndPersistCalendarInstanceForInterestRecalculation(existingLoanApplication);
}
}
return new CommandProcessingResultBuilder() //
.withEntityId(loanId) //
.withOfficeId(existingLoanApplication.getOfficeId()) //
.withClientId(existingLoanApplication.getClientId()) //
.withGroupId(existingLoanApplication.getGroupId()) //
.withLoanId(existingLoanApplication.getId()) //
.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();
}
}
/*
* 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("loan_account_no_UNIQUE")
|| (realCause.getCause() != null && realCause.getCause().getMessage().contains("loan_account_no_UNIQUE"))) {
final String accountNo = command.stringValueOfParameterNamed("accountNo");
throw new PlatformDataIntegrityException("error.msg.loan.duplicate.accountNo",
"Loan with accountNo `" + accountNo + "` already exists", "accountNo", accountNo);
} else if (realCause.getMessage().contains("loan_externalid_UNIQUE")
|| (realCause.getCause() != null && realCause.getCause().getMessage().contains("loan_externalid_UNIQUE"))) {
final String externalId = command.stringValueOfParameterNamed("externalId");
throw new PlatformDataIntegrityException("error.msg.loan.duplicate.externalId",
"Loan with externalId `" + externalId + "` already exists", "externalId", externalId);
}
logAsErrorUnexpectedDataIntegrityException(dve);
throw new PlatformDataIntegrityException("error.msg.unknown.data.integrity.issue", "Unknown data integrity issue with resource.");
}
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
LOG.error("Error occured.", dve);
}
@Transactional
@Override
public CommandProcessingResult deleteApplication(final Long loanId) {
final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
if (loan.isNotSubmittedAndPendingApproval()) {
throw new LoanApplicationNotInSubmittedAndPendingApprovalStateCannotBeDeleted(loanId);
}
final List<Note> relatedNotes = this.noteRepository.findByLoanId(loan.getId());
this.noteRepository.deleteAllInBatch(relatedNotes);
final AccountAssociations accountAssociations = this.accountAssociationsRepository.findByLoanIdAndType(loanId,
AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue());
if (accountAssociations != null) {
this.accountAssociationsRepository.delete(accountAssociations);
}
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
BigDecimal quantity = loanCollateralManagement.getQuantity();
ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement();
clientCollateralManagement.updateQuantityAfterLoanClosed(quantity);
loanCollateralManagement.setIsReleased(true);
loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement);
}
this.loanRepositoryWrapper.delete(loanId);
return new CommandProcessingResultBuilder() //
.withEntityId(loanId) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loan.getId()) //
.build();
}
public void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate) {
final String json = command.json();
final JsonElement element = this.fromJsonHelper.parse(json);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan");
final BigDecimal principal = this.fromJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element);
fromApiJsonDeserializer.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, principal);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
@Transactional
@Override
public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, final JsonCommand command) {
final Long parentLoanId = loanId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).get();
JsonArray approvalFormData = command.arrayOfParameterNamed("approvalFormData");
JsonObject jsonObject = null;
JsonCommand childCommand = null;
Long[] childLoanId = new Long[approvalFormData.size()];
BigDecimal parentPrincipalAmount = command.bigDecimalValueOfParameterNamed("glimPrincipal");
for (int i = 0; i < approvalFormData.size(); i++) {
jsonObject = approvalFormData.get(i).getAsJsonObject();
childLoanId[i] = jsonObject.get("loanId").getAsLong();
}
CommandProcessingResult result = null;
int count = 0;
int j = 0;
for (JsonElement approvals : approvalFormData) {
childCommand = JsonCommand.fromExistingCommand(command, approvals);
result = approveApplication(childLoanId[j++], childCommand);
if (result.getLoanId() != null) {
count++;
// if all the child loans are approved, mark the parent loan as
// approved
if (count == parentLoan.getChildAccountsCount()) {
parentLoan.setPrincipalAmount(parentPrincipalAmount);
parentLoan.setLoanStatus(LoanStatus.APPROVED.getValue());
glimRepository.save(parentLoan);
}
}
}
return result;
}
@Transactional
@Override
public CommandProcessingResult approveApplication(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
LocalDate expectedDisbursementDate = null;
this.loanApplicationTransitionApiJsonValidator.validateApproval(command.json());
final Loan loan = retrieveLoanBy(loanId);
final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName);
expectedDisbursementDate = command.localDateValueOfParameterNamed(LoanApiConstants.disbursementDateParameterName);
if (expectedDisbursementDate == null) {
expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate();
}
if (loan.loanProduct().isMultiDisburseLoan()) {
this.validateMultiDisbursementData(command, expectedDisbursementDate);
}
checkClientOrGroupActive(loan);
Boolean isSkipRepaymentOnFirstMonth = false;
Integer numberOfDays = 0;
// validate expected disbursement date against meeting date
if (loan.isSyncDisbursementWithMeeting() && (loan.isGroupLoan() || loan.isJLGLoan())) {
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
Calendar calendar = null;
if (calendarInstance != null) {
calendar = calendarInstance.getCalendar();
}
// final Calendar calendar = calendarInstance.getCalendar();
boolean isSkipRepaymentOnFirstMonthEnabled = this.configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled();
if (isSkipRepaymentOnFirstMonthEnabled) {
isSkipRepaymentOnFirstMonth = this.loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar);
if (isSkipRepaymentOnFirstMonth) {
numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue();
}
}
this.loanScheduleAssembler.validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar,
isSkipRepaymentOnFirstMonth, numberOfDays);
}
final Map<String, Object> changes = loan.loanApplicationApproval(currentUser, command, disbursementDataArray,
defaultLoanLifecycleStateMachine());
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.APPROVE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
if (!changes.isEmpty()) {
// If loan approved amount less than loan demanded amount, then need
// to recompute the schedule
if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || changes.containsKey("recalculateLoanSchedule")
|| changes.containsKey("expectedDisbursementDate")) {
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
}
if (loan.isTopup() && loan.getClientId() != null) {
final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose();
final Loan loanToClose = this.loanRepositoryWrapper.findNonClosedLoanThatBelongsToClient(loanIdToClose, loan.getClientId());
if (loanToClose == null) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.to.be.closed.with.topup.is.not.active",
"Loan to be closed with this topup is not active.");
}
final LocalDate lastUserTransactionOnLoanToClose = loanToClose.getLastUserTransactionDate();
if (loan.getDisbursementDate().isBefore(lastUserTransactionOnLoanToClose)) {
throw new GeneralPlatformDomainRuleException(
"error.msg.loan.disbursal.date.should.be.after.last.transaction.date.of.loan.to.be.closed",
"Disbursal date of this loan application " + loan.getDisbursementDate()
+ " should be after last transaction date of loan to be closed " + lastUserTransactionOnLoanToClose);
}
BigDecimal loanOutstanding = this.loanReadPlatformService
.retrieveLoanPrePaymentTemplate(loanIdToClose, expectedDisbursementDate).getAmount();
final BigDecimal firstDisbursalAmount = loan.getFirstDisbursalAmount();
if (loanOutstanding.compareTo(firstDisbursalAmount) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.loan.amount.less.than.outstanding.of.loan.to.be.closed",
"Topup loan amount should be greater than outstanding amount of loan to be closed.");
}
BigDecimal netDisbursalAmount = loan.getApprovedPrincipal().subtract(loanOutstanding);
loan.adjustNetDisbursalAmount(netDisbursalAmount);
}
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
changes.put("note", noteText);
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_APPROVED,
constructEntityMap(BusinessEntity.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult undoGLIMLoanApplicationApproval(final Long loanId, final JsonCommand command) {
// GroupLoanIndividualMonitoringAccount
// glimAccount=glimRepository.findOne(loanId);
final Long parentLoanId = loanId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).get();
List<Loan> childLoans = this.loanRepository.findByGlimId(loanId);
CommandProcessingResult result = null;
int count = 0;
for (Loan loan : childLoans) {
result = undoApplicationApproval(loan.getId(), command);
if (result.getLoanId() != null) {
count++;
// if all the child loans are approved, mark the parent loan as
// approved
if (count == parentLoan.getChildAccountsCount()) {
parentLoan.setLoanStatus(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue());
glimRepository.save(parentLoan);
}
}
}
return result;
}
@Transactional
@Override
public CommandProcessingResult undoApplicationApproval(final Long loanId, final JsonCommand command) {
AppUser currentUser = getAppUserIfPresent();
this.fromApiJsonDeserializer.validateForUndo(command.json());
final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
final Map<String, Object> changes = loan.undoApproval(defaultLoanLifecycleStateMachine());
if (!changes.isEmpty()) {
// If loan approved amount is not same as loan amount demanded, then
// during undo, restore the demand amount to principal amount.
if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName)
|| changes.containsKey(LoanApiConstants.disbursementPrincipalParameterName)) {
LocalDate recalculateFrom = null;
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO, currentUser);
}
loan.adjustNetDisbursalAmount(loan.getProposedPrincipal());
saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_UNDO_APPROVAL,
constructEntityMap(BusinessEntity.LOAN, loan));
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult rejectGLIMApplicationApproval(final Long glimId, final JsonCommand command) {
// GroupLoanIndividualMonitoringAccount
// glimAccount=glimRepository.findOne(loanId);
final Long parentLoanId = glimId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).get();
List<Loan> childLoans = this.loanRepository.findByGlimId(glimId);
CommandProcessingResult result = null;
int count = 0;
for (Loan loan : childLoans) {
result = rejectApplication(loan.getId(), command);
if (result.getLoanId() != null) {
count++;
// if all the child loans are Rejected, mark the parent loan as
// rejected
if (count == parentLoan.getChildAccountsCount()) {
parentLoan.setLoanStatus(LoanStatus.REJECTED.getValue());
glimRepository.save(parentLoan);
}
}
}
return result;
}
@Transactional
@Override
public CommandProcessingResult rejectApplication(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
this.loanApplicationTransitionApiJsonValidator.validateRejection(command.json());
final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.REJECTED.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
final Map<String, Object> changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine());
if (!changes.isEmpty()) {
this.loanRepositoryWrapper.saveAndFlush(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
}
this.businessEventNotifierService.notifyBusinessEventWasExecuted(BusinessEvents.LOAN_REJECTED,
constructEntityMap(BusinessEntity.LOAN, loan));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
@Transactional
@Override
public CommandProcessingResult applicantWithdrawsFromApplication(final Long loanId, final JsonCommand command) {
final AppUser currentUser = getAppUserIfPresent();
this.loanApplicationTransitionApiJsonValidator.validateApplicantWithdrawal(command.json());
final Loan loan = retrieveLoanBy(loanId);
checkClientOrGroupActive(loan);
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(),
StatusEnum.WITHDRAWN.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
final Map<String, Object> changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command,
defaultLoanLifecycleStateMachine());
// Release attached collaterals
if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) {
Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement();
clientCollateralManagement
.updateQuantity(clientCollateralManagement.getQuantity().add(loanCollateralManagement.getQuantity()));
loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement);
loanCollateralManagement.setIsReleased(true);
}
loan.updateLoanCollateral(loanCollateralManagements);
}
if (!changes.isEmpty()) {
this.loanRepositoryWrapper.saveAndFlush(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
}
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) //
.withEntityId(loan.getId()) //
.withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) //
.withLoanId(loanId) //
.with(changes) //
.build();
}
private Loan retrieveLoanBy(final Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
loan.setHelpers(defaultLoanLifecycleStateMachine(), this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory);
return loan;
}
private void validateSubmittedOnDate(final Loan loan) {
final LocalDate startDate = loan.loanProduct().getStartDate();
final LocalDate closeDate = loan.loanProduct().getCloseDate();
final LocalDate expectedFirstRepaymentOnDate = loan.getExpectedFirstRepaymentOnDate();
final LocalDate submittedOnDate = loan.getSubmittedOnDate();
String defaultUserMessage = "";
if (startDate != null && submittedOnDate.isBefore(startDate)) {
defaultUserMessage = "submittedOnDate cannot be before the loan product startDate.";
throw new LoanApplicationDateException("submitted.on.date.cannot.be.before.the.loan.product.start.date", defaultUserMessage,
submittedOnDate.toString(), startDate.toString());
}
if (closeDate != null && submittedOnDate.isAfter(closeDate)) {
defaultUserMessage = "submittedOnDate cannot be after the loan product closeDate.";
throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.product.close.date", defaultUserMessage,
submittedOnDate.toString(), closeDate.toString());
}
if (expectedFirstRepaymentOnDate != null && submittedOnDate.isAfter(expectedFirstRepaymentOnDate)) {
defaultUserMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate.";
throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.expected.first.repayment.date",
defaultUserMessage, submittedOnDate.toString(), expectedFirstRepaymentOnDate.toString());
}
}
private void checkClientOrGroupActive(final Loan loan) {
final Client client = loan.client();
if (client != null) {
if (client.isNotActive()) {
throw new ClientNotActiveException(client.getId());
}
}
final Group group = loan.group();
if (group != null) {
if (group.isNotActive()) {
throw new GroupNotActiveException(group.getId());
}
}
}
private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) {
try {
this.loanRepositoryWrapper.saveAndFlush(loan);
} catch (final JpaSystemException | DataIntegrityViolationException e) {
final Throwable realCause = e.getCause();
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.application");
if (realCause.getMessage().toLowerCase().contains("external_id_unique")) {
baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique");
}
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors, e);
}
}
}
private AppUser getAppUserIfPresent() {
AppUser user = null;
if (this.context != null) {
user = this.context.getAuthenticatedUserIfPresent();
}
return user;
}
private Map<BusinessEntity, Object> constructEntityMap(final BusinessEntity entityEvent, Object entity) {
Map<BusinessEntity, Object> map = new HashMap<>(1);
map.put(entityEvent, entity);
return map;
}
private void officeSpecificLoanProductValidation(final Long productId, final Long officeId) {
final GlobalConfigurationProperty restrictToUserOfficeProperty = this.globalConfigurationRepository
.findOneByNameWithNotFoundDetection(FineractEntityAccessConstants.GLOBAL_CONFIG_FOR_OFFICE_SPECIFIC_PRODUCTS);
if (restrictToUserOfficeProperty.isEnabled()) {
FineractEntityRelation fineractEntityRelation = fineractEntityRelationRepository
.findOneByCodeName(FineractEntityAccessType.OFFICE_ACCESS_TO_LOAN_PRODUCTS.toStr());
FineractEntityToEntityMapping officeToLoanProductMappingList = this.repository.findListByProductId(fineractEntityRelation,
productId, officeId);
if (officeToLoanProductMappingList == null) {
throw new NotOfficeSpecificProductException(productId, officeId);
}
}
}
}