| /** |
| * 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 java.math.BigDecimal; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.commons.lang.StringUtils; |
| 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.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.PlatformApiDataValidationException; |
| import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; |
| import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; |
| 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.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.collateral.domain.LoanCollateral; |
| import org.apache.fineract.portfolio.collateral.service.CollateralAssembler; |
| import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; |
| import org.apache.fineract.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; |
| import org.apache.fineract.portfolio.common.service.BusinessEventNotifierService; |
| 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.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.Loan; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; |
| 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.LoanStatus; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; |
| 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.exception.LoanNotFoundException; |
| import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.AprCalculator; |
| 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.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.note.domain.Note; |
| import org.apache.fineract.portfolio.note.domain.NoteRepository; |
| import org.apache.fineract.portfolio.savings.domain.SavingsAccount; |
| import org.apache.fineract.portfolio.savings.domain.SavingsAccountAssembler; |
| import org.apache.fineract.useradministration.domain.AppUser; |
| import org.joda.time.LocalDate; |
| 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.JsonElement; |
| |
| @Service |
| public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements LoanApplicationWritePlatformService { |
| |
| private final static Logger logger = 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 LoanRepository loanRepository; |
| 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 CollateralAssembler 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; |
| |
| @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 CollateralAssembler loanCollateralAssembler, final LoanRepository loanRepository, 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) { |
| 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.loanRepository = loanRepository; |
| 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; |
| } |
| |
| 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.findOne(productId); |
| if (loanProduct == null) { throw new LoanProductNotFoundException(productId); } |
| |
| this.fromApiJsonDeserializer.validateForCreate(command.json(), isMeetingMandatoryForJLGLoans, loanProduct); |
| |
| final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); |
| final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); |
| |
| if (loanProduct.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, 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); |
| |
| 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); |
| |
| this.loanRepository.save(newLoanApplication); |
| |
| if (loanProduct.isInterestRecalculationEnabled()) { |
| this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(newLoanApplication); |
| createAndPersistCalendarInstanceForInterestRecalculation(newLoanApplication); |
| } |
| |
| if (newLoanApplication.isAccountNumberRequiresAutoGeneration()) { |
| final AccountNumberFormat accountNumberFormat = this.accountNumberFormatRepository |
| .findByAccountType(EntityAccountType.LOAN); |
| newLoanApplication.updateAccountNo(this.accountNumberGenerator.generate(newLoanApplication, accountNumberFormat)); |
| this.loanRepository.save(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.findOne(calendarId); |
| if (calendar == null) { throw new CalendarNotFoundException(calendarId); } |
| |
| final CalendarInstance calendarInstance = new CalendarInstance(calendar, newLoanApplication.getId(), |
| CalendarEntityType.LOANS.getValue()); |
| this.calendarInstanceRepository.save(calendarInstance); |
| } |
| |
| // Save linked account information |
| final Long savingsAccountId = command.longValueOfParameterNamed("linkAccountId"); |
| if (savingsAccountId != null) { |
| final SavingsAccount savingsAccount = this.savingsAccountAssembler.assembleFrom(savingsAccountId); |
| this.fromApiJsonDeserializer.validatelinkedSavingsAccount(savingsAccount, newLoanApplication); |
| boolean isActive = true; |
| final AccountAssociations accountAssociations = AccountAssociations.associateSavingsAccount(newLoanApplication, |
| savingsAccount, AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue(), isActive); |
| this.accountAssociationsRepository.save(accountAssociations); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(newLoanApplication.getId()) // |
| .withOfficeId(newLoanApplication.getOfficeId()) // |
| .withClientId(newLoanApplication.getClientId()) // |
| .withGroupId(newLoanApplication.getGroupId()) // |
| .withLoanId(newLoanApplication.getId()) // |
| .build(); |
| } catch (final DataIntegrityViolationException dve) { |
| handleDataIntegrityIssues(command, dve); |
| return CommandProcessingResult.empty(); |
| } |
| } |
| |
| 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.loanInterestRecalculationDetails().getRestFrequencyLocalDate(); |
| if (calendarStartDate == null) { |
| calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); |
| } |
| final Integer repeatsOnDay = calendarStartDate.getDayOfWeek(); |
| final RecalculationFrequencyType recalculationFrequencyType = loan.loanInterestRecalculationDetails().getRestFrequencyType(); |
| |
| Integer frequency = loan.loanInterestRecalculationDetails().getRestInterval(); |
| CalendarEntityType calendarEntityType = CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL; |
| final String title = "loan_recalculation_detail_" + loan.loanInterestRecalculationDetails().getId(); |
| |
| createCalendar(loan, calendarStartDate, repeatsOnDay, recalculationFrequencyType, frequency, calendarEntityType, title); |
| |
| if (loan.loanInterestRecalculationDetails().getInterestRecalculationCompoundingMethod().isCompoundingEnabled()) { |
| LocalDate compoundingStartDate = loan.loanInterestRecalculationDetails().getCompoundingFrequencyLocalDate(); |
| if (compoundingStartDate == null) { |
| compoundingStartDate = loan.getExpectedDisbursedOnLocalDate(); |
| } |
| final Integer compoundingRepeatsOnDay = compoundingStartDate.getDayOfWeek(); |
| final RecalculationFrequencyType recalculationCompoundingFrequencyType = loan.loanInterestRecalculationDetails() |
| .getCompoundingFrequencyType(); |
| |
| 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, compoundingRepeatsOnDay, recalculationCompoundingFrequencyType, |
| compoundingFrequency, compoundingCalendarEntityType, compoundingCalendarTitle); |
| } |
| |
| } |
| |
| private void createCalendar(final Loan loan, LocalDate calendarStartDate, final Integer repeatsOnDay, |
| final RecalculationFrequencyType recalculationFrequencyType, Integer frequency, CalendarEntityType calendarEntityType, |
| final String title) { |
| CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.INVALID; |
| 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(); |
| break; |
| case WEEKLY: |
| calendarFrequencyType = CalendarFrequencyType.WEEKLY; |
| break; |
| default: |
| break; |
| } |
| |
| final Calendar calendar = Calendar.createRepeatingCalendar(title, calendarStartDate, CalendarType.COLLECTION.getValue(), |
| calendarFrequencyType, frequency, repeatsOnDay); |
| final CalendarInstance calendarInstance = CalendarInstance.from(calendar, loan.loanInterestRecalculationDetails().getId(), |
| calendarEntityType.getValue()); |
| this.calendarInstanceRepository.save(calendarInstance); |
| } |
| |
| private void updateRestCalendarDetailsForInterestRecalculation(final CalendarInstance calendarInstance, final Loan loan) { |
| |
| Calendar interestRecalculationRecurrings = calendarInstance.getCalendar(); |
| LocalDate calendarStartDate = loan.loanInterestRecalculationDetails().getRestFrequencyLocalDate(); |
| if (calendarStartDate == null) { |
| calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); |
| } |
| final Integer repeatsOnDay = calendarStartDate.getDayOfWeek(); |
| final RecalculationFrequencyType recalculationFrequencyType = loan.loanInterestRecalculationDetails().getRestFrequencyType(); |
| |
| Integer frequency = loan.loanInterestRecalculationDetails().getRestInterval(); |
| |
| updateCalendar(loan, interestRecalculationRecurrings, calendarStartDate, repeatsOnDay, recalculationFrequencyType, frequency); |
| |
| } |
| |
| private void updateCompoundingCalendarDetailsForInterestRecalculation(final CalendarInstance calendarInstance, final Loan loan) { |
| |
| Calendar interestRecalculationRecurrings = calendarInstance.getCalendar(); |
| LocalDate calendarStartDate = loan.loanInterestRecalculationDetails().getCompoundingFrequencyLocalDate(); |
| if (calendarStartDate == null) { |
| calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); |
| } |
| final Integer repeatsOnDay = calendarStartDate.getDayOfWeek(); |
| final RecalculationFrequencyType recalculationFrequencyType = loan.loanInterestRecalculationDetails().getCompoundingFrequencyType(); |
| |
| Integer frequency = loan.loanInterestRecalculationDetails().getCompoundingInterval(); |
| |
| updateCalendar(loan, interestRecalculationRecurrings, calendarStartDate, repeatsOnDay, recalculationFrequencyType, frequency); |
| |
| } |
| |
| private void updateCalendar(final Loan loan, Calendar interestRecalculationRecurrings, LocalDate calendarStartDate, |
| final Integer repeatsOnDay, final RecalculationFrequencyType recalculationFrequencyType, Integer frequency) { |
| CalendarFrequencyType calendarFrequencyType = CalendarFrequencyType.INVALID; |
| switch (recalculationFrequencyType) { |
| case DAILY: |
| calendarFrequencyType = CalendarFrequencyType.DAILY; |
| break; |
| |
| case MONTHLY: |
| calendarFrequencyType = CalendarFrequencyType.MONTHLY; |
| break; |
| case SAME_AS_REPAYMENT_PERIOD: |
| calendarStartDate = loan.getExpectedDisbursedOnLocalDate(); |
| frequency = loan.repaymentScheduleDetail().getRepayEvery(); |
| calendarFrequencyType = CalendarFrequencyType.from(loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType()); |
| break; |
| case WEEKLY: |
| calendarFrequencyType = CalendarFrequencyType.WEEKLY; |
| break; |
| default: |
| break; |
| } |
| |
| interestRecalculationRecurrings.updateRepeatingCalendar(calendarStartDate, calendarFrequencyType, frequency, repeatsOnDay); |
| this.calendarRepository.save(interestRecalculationRecurrings); |
| } |
| |
| @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.findOne(productId); |
| if (newLoanProduct == null) { throw 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); |
| } |
| Set<LoanDisbursementDetails> disbursementDetails = this.loanAssembler.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; |
| } |
| } |
| } |
| |
| final Set<LoanCollateral> possiblyModifedLoanCollateralItems = this.loanCollateralAssembler |
| .fromParsedJson(command.parsedJson()); |
| |
| final Map<String, Object> changes = existingLoanApplication.loanApplicationModification(command, possiblyModifedLoanCharges, |
| possiblyModifedLoanCollateralItems, this.aprCalculator, isChargeModified); |
| |
| 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); |
| } |
| |
| 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); |
| } |
| |
| final String collateralParamName = "collateral"; |
| if (changes.containsKey(collateralParamName)) { |
| final Set<LoanCollateral> loanCollateral = this.loanCollateralAssembler.fromParsedJson(command.parsedJson()); |
| existingLoanApplication.updateLoanCollateral(loanCollateral); |
| } |
| |
| 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(); |
| } |
| |
| 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.findOne(calendarId); |
| if (calendar == null) { throw 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); |
| if (calendarInstance.getCalendar().getId() != 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 calendarInstance = ciList.get(0); |
| this.calendarInstanceRepository.delete(calendarInstance); |
| } |
| |
| // Save linked account information |
| final String linkAccountIdParamName = "linkAccountId"; |
| 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); |
| 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); |
| } |
| } |
| } |
| |
| // 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.loanRepository.save(existingLoanApplication); |
| |
| if (productRelatedDetail.isInterestRecalculationEnabled()) { |
| this.fromApiJsonDeserializer.validateLoanForInterestRecalculation(existingLoanApplication); |
| if (changes.containsKey(LoanProductConstants.isInterestRecalculationEnabledParameterName)) { |
| createAndPersistCalendarInstanceForInterestRecalculation(existingLoanApplication); |
| } else { |
| if (changes.containsKey(LoanProductConstants.recalculationRestFrequencyDateParamName)) { |
| |
| CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( |
| existingLoanApplication.loanInterestRecalculationDetailId(), |
| CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue(), CalendarType.COLLECTION.getValue()); |
| updateRestCalendarDetailsForInterestRecalculation(calendarInstance, existingLoanApplication); |
| } |
| if (changes.containsKey(LoanProductConstants.recalculationCompoundingFrequencyDateParamName)) { |
| CalendarInstance calendarInstance = this.calendarInstanceRepository.findByEntityIdAndEntityTypeIdAndCalendarTypeId( |
| existingLoanApplication.loanInterestRecalculationDetailId(), |
| CalendarEntityType.LOAN_RECALCULATION_COMPOUNDING_DETAIL.getValue(), CalendarType.COLLECTION.getValue()); |
| updateCompoundingCalendarDetailsForInterestRecalculation(calendarInstance, existingLoanApplication); |
| } |
| } |
| |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withEntityId(loanId) // |
| .withOfficeId(existingLoanApplication.getOfficeId()) // |
| .withClientId(existingLoanApplication.getClientId()) // |
| .withGroupId(existingLoanApplication.getGroupId()) // |
| .withLoanId(existingLoanApplication.getId()) // |
| .with(changes).build(); |
| } catch (final DataIntegrityViolationException dve) { |
| handleDataIntegrityIssues(command, dve); |
| return CommandProcessingResult.empty(); |
| } |
| } |
| |
| /* |
| * 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("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")) { |
| |
| 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 DataIntegrityViolationException dve) { |
| logger.error(dve.getMessage(), 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.deleteInBatch(relatedNotes); |
| |
| this.loanRepository.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 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); |
| |
| // 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()); |
| final Calendar calendar = calendarInstance.getCalendar(); |
| this.loanScheduleAssembler.validateDisbursementDateWithMeetingDates(expectedDisbursementDate, calendar); |
| } |
| |
| final Map<String, Object> changes = loan.loanApplicationApproval(currentUser, command, disbursementDataArray, |
| defaultLoanLifecycleStateMachine()); |
| |
| 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); |
| } |
| |
| 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(BUSINESS_EVENTS.LOAN_APPROVED, |
| constructEntityMap(BUSINESS_ENTITY.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 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); |
| } |
| |
| 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(BUSINESS_EVENTS.LOAN_UNDO_APPROVAL, |
| constructEntityMap(BUSINESS_ENTITY.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 rejectApplication(final Long loanId, final JsonCommand command) { |
| |
| final AppUser currentUser = getAppUserIfPresent(); |
| |
| this.loanApplicationTransitionApiJsonValidator.validateRejection(command.json()); |
| |
| final Loan loan = retrieveLoanBy(loanId); |
| checkClientOrGroupActive(loan); |
| |
| final Map<String, Object> changes = loan.loanApplicationRejection(currentUser, command, defaultLoanLifecycleStateMachine()); |
| if (!changes.isEmpty()) { |
| this.loanRepository.save(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(); |
| } |
| |
| @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); |
| |
| final Map<String, Object> changes = loan.loanApplicationWithdrawnByApplicant(currentUser, command, |
| defaultLoanLifecycleStateMachine()); |
| if (!changes.isEmpty()) { |
| this.loanRepository.save(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.loanRepository.findOne(loanId); |
| if (loan == null) { throw new LoanNotFoundException(loanId); } |
| 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 { |
| List<LoanRepaymentScheduleInstallment> installments = loan.fetchRepaymentScheduleInstallments(); |
| for (LoanRepaymentScheduleInstallment installment : installments) { |
| if (installment.getId() == null) { |
| this.repaymentScheduleInstallmentRepository.save(installment); |
| } |
| } |
| this.loanRepository.saveAndFlush(loan); |
| } catch (final 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); } |
| } |
| } |
| |
| private AppUser getAppUserIfPresent() { |
| AppUser user = null; |
| if (this.context != null) { |
| user = this.context.getAuthenticatedUserIfPresent(); |
| } |
| return user; |
| } |
| |
| private Map<BUSINESS_ENTITY, Object> constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { |
| Map<BUSINESS_ENTITY, Object> map = new HashMap<>(1); |
| map.put(entityEvent, entity); |
| return map; |
| } |
| |
| } |