| /** |
| * 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.common.collect.Lists; |
| import com.google.gson.JsonArray; |
| import com.google.gson.JsonElement; |
| import com.google.gson.JsonObject; |
| import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
| import io.github.resilience4j.retry.annotation.Retry; |
| import java.math.BigDecimal; |
| import java.time.LocalDate; |
| import java.time.format.DateTimeFormatter; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import lombok.RequiredArgsConstructor; |
| import lombok.extern.slf4j.Slf4j; |
| import org.apache.commons.lang3.StringUtils; |
| import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; |
| import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException; |
| import org.apache.fineract.cob.service.LoanAccountLockService; |
| import org.apache.fineract.infrastructure.codes.domain.CodeValue; |
| import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; |
| import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; |
| import org.apache.fineract.infrastructure.core.api.JsonCommand; |
| 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.domain.ExternalId; |
| import org.apache.fineract.infrastructure.core.exception.ErrorHandler; |
| import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; |
| import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; |
| import org.apache.fineract.infrastructure.core.exception.PlatformServiceUnavailableException; |
| import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; |
| import org.apache.fineract.infrastructure.core.service.MathUtil; |
| 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.event.business.domain.loan.LoanAcceptTransferBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanChargebackTransactionBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseAsRescheduleBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanDisbursalBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanInitiateTransferBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanInterestRecalculationBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanReassignOfficerBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRejectTransferBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRemoveOfficerBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanRescheduledDueCalendarChangeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoDisbursalBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUndoLastDisbursalBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanUpdateDisbursementDataBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdrawTransferBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoWrittenOffBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWaiveInterestBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWrittenOffPostBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanWrittenOffPreBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; |
| import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; |
| import org.apache.fineract.organisation.holiday.domain.Holiday; |
| import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; |
| import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; |
| import org.apache.fineract.organisation.monetary.domain.Money; |
| import org.apache.fineract.organisation.office.domain.Office; |
| import org.apache.fineract.organisation.staff.domain.Staff; |
| import org.apache.fineract.organisation.teller.data.CashierTransactionDataValidator; |
| import org.apache.fineract.organisation.workingdays.domain.WorkingDays; |
| import org.apache.fineract.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; |
| import org.apache.fineract.portfolio.account.PortfolioAccountType; |
| import org.apache.fineract.portfolio.account.data.AccountTransferDTO; |
| import org.apache.fineract.portfolio.account.data.PortfolioAccountData; |
| 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.account.domain.AccountTransferDetailRepository; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferDetails; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferStandingInstruction; |
| import org.apache.fineract.portfolio.account.domain.AccountTransferType; |
| import org.apache.fineract.portfolio.account.domain.StandingInstructionPriority; |
| import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; |
| import org.apache.fineract.portfolio.account.domain.StandingInstructionType; |
| import org.apache.fineract.portfolio.account.service.AccountAssociationsReadPlatformService; |
| import org.apache.fineract.portfolio.account.service.AccountTransfersReadPlatformService; |
| import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; |
| 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.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.CalendarParameterUpdateNotSupportedException; |
| import org.apache.fineract.portfolio.client.domain.Client; |
| import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; |
| import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; |
| import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException; |
| import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; |
| import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; |
| import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand; |
| import org.apache.fineract.portfolio.collectionsheet.command.SingleRepaymentCommand; |
| import org.apache.fineract.portfolio.group.domain.Group; |
| import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; |
| import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; |
| import org.apache.fineract.portfolio.loanaccount.command.LoanUpdateCommand; |
| import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; |
| import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; |
| import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; |
| 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.LoanAccountDomainService; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetailsRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; |
| 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.LoanRepositoryWrapper; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; |
| import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; |
| import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; |
| import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; |
| import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanMultiDisbursementException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentException; |
| import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; |
| import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; |
| import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; |
| import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; |
| import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; |
| import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; |
| import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; |
| import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; |
| import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationCommandFromApiJsonHelper; |
| import org.apache.fineract.portfolio.loanaccount.serialization.LoanEventApiJsonValidator; |
| import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; |
| import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; |
| import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; |
| import org.apache.fineract.portfolio.note.domain.Note; |
| import org.apache.fineract.portfolio.note.domain.NoteRepository; |
| import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; |
| import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; |
| import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; |
| import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; |
| import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler; |
| import org.apache.fineract.portfolio.savings.domain.SavingsAccount; |
| import org.apache.fineract.portfolio.transfer.api.TransferApiConstants; |
| import org.apache.fineract.useradministration.domain.AppUser; |
| import org.springframework.dao.DataIntegrityViolationException; |
| import org.springframework.orm.jpa.JpaSystemException; |
| import org.springframework.transaction.annotation.Transactional; |
| |
| @Slf4j |
| @RequiredArgsConstructor |
| public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { |
| |
| private final PlatformSecurityContext context; |
| private final LoanEventApiJsonValidator loanEventApiJsonValidator; |
| private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer; |
| private final LoanRepositoryWrapper loanRepositoryWrapper; |
| private final LoanAccountDomainService loanAccountDomainService; |
| private final NoteRepository noteRepository; |
| private final LoanTransactionRepository loanTransactionRepository; |
| private final LoanTransactionRelationRepository loanTransactionRelationRepository; |
| private final LoanAssembler loanAssembler; |
| private final JournalEntryWritePlatformService journalEntryWritePlatformService; |
| private final CalendarInstanceRepository calendarInstanceRepository; |
| private final PaymentDetailWritePlatformService paymentDetailWritePlatformService; |
| private final HolidayRepositoryWrapper holidayRepository; |
| private final ConfigurationDomainService configurationDomainService; |
| private final WorkingDaysRepositoryWrapper workingDaysRepository; |
| private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; |
| private final AccountTransfersReadPlatformService accountTransfersReadPlatformService; |
| private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; |
| private final LoanReadPlatformService loanReadPlatformService; |
| private final FromJsonHelper fromApiJsonHelper; |
| private final CalendarRepository calendarRepository; |
| private final LoanScheduleHistoryWritePlatformService loanScheduleHistoryWritePlatformService; |
| private final LoanApplicationCommandFromApiJsonHelper loanApplicationCommandFromApiJsonHelper; |
| private final AccountAssociationsRepository accountAssociationRepository; |
| private final AccountTransferDetailRepository accountTransferDetailRepository; |
| private final BusinessEventNotifierService businessEventNotifierService; |
| private final GuarantorDomainService guarantorDomainService; |
| private final LoanUtilService loanUtilService; |
| private final LoanSummaryWrapper loanSummaryWrapper; |
| private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; |
| private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessingStrategy; |
| private final CodeValueRepositoryWrapper codeValueRepository; |
| private final CashierTransactionDataValidator cashierTransactionDataValidator; |
| private final GLIMAccountInfoRepository glimRepository; |
| private final LoanRepository loanRepository; |
| private final RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler; |
| private final PostDatedChecksRepository postDatedChecksRepository; |
| private final LoanDisbursementDetailsRepository loanDisbursementDetailsRepository; |
| private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; |
| private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; |
| private final LoanAccountLockService loanAccountLockService; |
| private final ExternalIdFactory externalIdFactory; |
| private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; |
| private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; |
| private final ErrorHandler errorHandler; |
| private final LoanDownPaymentHandlerService loanDownPaymentHandlerService; |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult disburseGLIMLoan(final Long loanId, final JsonCommand command) { |
| final Long parentLoanId = loanId; |
| GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow(); |
| List<Loan> childLoans = this.loanRepository.findByGlimId(loanId); |
| CommandProcessingResult result = null; |
| int count = 0; |
| for (Loan loan : childLoans) { |
| result = disburseLoan(loan.getId(), command, false); |
| 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.ACTIVE.getValue()); |
| glimRepository.save(parentLoan); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer) { |
| |
| final AppUser currentUser = getAppUserIfPresent(); |
| |
| this.loanEventApiJsonValidator.validateDisbursement(command.json(), isAccountTransfer); |
| |
| if (command.parameterExists("postDatedChecks")) { |
| // validate with post dated checks for the disbursement |
| this.loanEventApiJsonValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId); |
| } |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| // Fail fast if client/group is not active or actual loan status disallows disbursal |
| checkClientOrGroupActive(loan); |
| |
| final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); |
| |
| if (loan.isChargedOff() && DateUtils.isBefore(actualDisbursementDate, loan.getChargedOffOnDate())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " |
| + loanId |
| + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", |
| loanId); |
| } |
| |
| if (loan.loanProduct().isDisallowExpectedDisbursements()) { |
| List<LoanDisbursementDetails> filteredList = loan.getDisbursementDetails().stream() |
| .filter(disbursementDetails -> disbursementDetails.actualDisbursementDate() == null).toList(); |
| // Check whether a new LoanDisbursementDetails is required |
| if (filteredList.isEmpty()) { |
| // create artificial 'tranche/expected disbursal' as current disburse code expects it for |
| // multi-disbursal |
| // products |
| final LocalDate artificialExpectedDate = loan.getExpectedDisbursedOnLocalDate(); |
| LoanDisbursementDetails disbursementDetail = new LoanDisbursementDetails(artificialExpectedDate, null, |
| loan.getDisbursedAmount(), null, false); |
| disbursementDetail.updateLoan(loan); |
| loan.getAllDisbursementDetails().add(disbursementDetail); |
| } |
| } |
| loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); |
| |
| // Get disbursedAmount |
| final BigDecimal disbursedAmount = loan.getDisbursedAmount(); |
| final Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements(); |
| |
| // Get relevant loan collateral modules |
| if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) |
| && AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { |
| |
| BigDecimal totalCollateral = BigDecimal.valueOf(0); |
| |
| for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { |
| BigDecimal quantity = loanCollateralManagement.getQuantity(); |
| BigDecimal pctToBase = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getPctToBase(); |
| BigDecimal basePrice = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getBasePrice(); |
| totalCollateral = totalCollateral.add(quantity.multiply(basePrice).multiply(pctToBase).divide(BigDecimal.valueOf(100))); |
| } |
| |
| // Validate the loan collateral value against the disbursedAmount |
| if (disbursedAmount.compareTo(totalCollateral) > 0) { |
| throw new LoanCollateralAmountNotSufficientException(disbursedAmount); |
| } |
| } |
| |
| // validate ActualDisbursement Date Against Expected Disbursement Date |
| LoanProduct loanProduct = loan.loanProduct(); |
| if (loanProduct.syncExpectedWithDisbursementDate()) { |
| syncExpectedDateWithActualDisbursementDate(loan, actualDisbursementDate); |
| } |
| |
| final LocalDate nextPossibleRepaymentDate = loan.getNextPossibleRepaymentDateForRescheduling(); |
| final LocalDate rescheduledRepaymentDate = command.localDateValueOfParameterNamed("adjustRepaymentDate"); |
| |
| entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), |
| StatusEnum.DISBURSE.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); |
| |
| LocalDate recalculateFrom = null; |
| if (!loan.isMultiDisburmentLoan()) { |
| loan.setActualDisbursementDate(actualDisbursementDate); |
| } |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| // validate actual disbursement date against meeting date |
| final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), |
| CalendarEntityType.LOANS.getValue()); |
| if (loan.isSyncDisbursementWithMeeting()) { |
| this.loanEventApiJsonValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance, |
| scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays()); |
| } |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanDisbursalBusinessEvent(loan)); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| |
| final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); |
| if (paymentDetail != null && paymentDetail.getPaymentType() != null && paymentDetail.getPaymentType().getIsCashPayment()) { |
| BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| this.cashierTransactionDataValidator.validateOnLoanDisbursal(currentUser, loan.getCurrencyCode(), transactionAmount); |
| } |
| final boolean isPaymentTypeApplicableForDisbursementCharge = configurationDomainService |
| .isPaymentTypeApplicableForDisbursementCharge(); |
| |
| // Recalculate first repayment date based in actual disbursement date. |
| updateLoanCounters(loan, actualDisbursementDate); |
| Money amountBeforeAdjust = loan.getPrincipal(); |
| boolean canDisburse = loan.canDisburse(actualDisbursementDate); |
| ChangedTransactionDetail changedTransactionDetail = null; |
| final Locale locale = command.extractLocale(); |
| final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); |
| if (canDisburse) { |
| |
| // Get netDisbursalAmount from disbursal screen field. |
| final BigDecimal netDisbursalAmount = command |
| .bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName); |
| if (netDisbursalAmount != null) { |
| loan.setNetDisbursalAmount(netDisbursalAmount); |
| } |
| Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); |
| Money amountToDisburse = disburseAmount.copy(); |
| boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| 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 (DateUtils.isBefore(loan.getDisbursementDate(), 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(LoanTransactionType.REPAYMENT, loanIdToClose, actualDisbursementDate).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."); |
| } |
| |
| amountToDisburse = disburseAmount.minus(loanOutstanding); |
| |
| disburseLoanToLoan(loan, command, loanOutstanding); |
| } |
| LoanTransaction disbursementTransaction = null; |
| if (isAccountTransfer) { |
| disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail); |
| existingTransactionIds.addAll(loan.findExistingTransactionIds()); |
| existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); |
| } else { |
| existingTransactionIds.addAll(loan.findExistingTransactionIds()); |
| existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); |
| disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, |
| actualDisbursementDate, txnExternalId); |
| disbursementTransaction.updateLoan(loan); |
| loan.addLoanTransaction(disbursementTransaction); |
| } |
| if (loan.getRepaymentScheduleInstallments().isEmpty()) { |
| /* |
| * If no schedule, generate one (applicable to non-tranche multi-disbursal loans) |
| */ |
| recalculateSchedule = true; |
| } |
| regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, |
| rescheduledRepaymentDate); |
| boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment(); |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { |
| createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| } |
| if (isPaymentTypeApplicableForDisbursementCharge) { |
| changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); |
| } else { |
| changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); |
| } |
| loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); |
| if (disbursementTransaction != null) { |
| loanTransactionRepository.saveAndFlush(disbursementTransaction); |
| } |
| if (loan.isAutoRepaymentForDownPaymentEnabled()) { |
| // updating linked savings account for auto down payment transaction for disbursement to savings account |
| if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { |
| final PortfolioAccountData linkedSavingsAccountData = this.accountAssociationsReadPlatformService |
| .retriveLoanLinkedAssociation(loanId); |
| final SavingsAccount fromSavingsAccount = null; |
| final boolean isRegularTransaction = true; |
| final boolean isExceptionForBalanceCheck = false; |
| |
| BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail() |
| .getDisbursedAmountPercentageForDownPayment(); |
| Money downPaymentMoney = Money.of(loan.getCurrency(), |
| MathUtil.percentageOf(amountToDisburse.getAmount(), disbursedAmountPercentageForDownPayment, 19)); |
| |
| final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, |
| downPaymentMoney.getAmount(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, |
| linkedSavingsAccountData.getId(), loan.getId(), |
| "To loan " + loan.getAccountNumber() + " from savings " + linkedSavingsAccountData.getAccountNo() |
| + " Standing instruction transfer ", |
| locale, fmt, null, null, LoanTransactionType.DOWN_PAYMENT.getValue(), null, null, |
| AccountTransferType.LOAN_DOWN_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, |
| fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); |
| this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); |
| } else { |
| loanDownPaymentHandlerService.handleDownPayment(scheduleGeneratorDTO, command, amountToDisburse, loan); |
| } |
| } |
| } |
| if (!changes.isEmpty()) { |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| // auto create standing instruction |
| createStandingInstruction(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| } |
| |
| final Set<LoanCharge> loanCharges = loan.getActiveCharges(); |
| final Map<Long, BigDecimal> disBuLoanCharges = new HashMap<>(); |
| for (final LoanCharge loanCharge : loanCharges) { |
| if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer() |
| && loanCharge.isChargePending()) { |
| disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding()); |
| } |
| if (loanCharge.isDisbursementCharge()) { |
| LoanTransaction loanTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), actualDisbursementDate, |
| loanCharge.amount(), null, loanCharge.amount(), null, externalIdFactory.create()); |
| LoanTransaction savedLoanTransaction = loanTransactionRepository.saveAndFlush(loanTransaction); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(savedLoanTransaction)); |
| } |
| } |
| for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) { |
| final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanLinkedAssociation(loanId); |
| final SavingsAccount fromSavingsAccount = null; |
| final boolean isRegularTransaction = true; |
| final boolean isExceptionForBalanceCheck = false; |
| final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), |
| PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.getId(), loanId, "Loan Charge Payment", |
| locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null, |
| AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount, |
| isRegularTransaction, isExceptionForBalanceCheck); |
| this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); |
| } |
| updateRecurringCalendarDatesForInterestRecalculation(loan); |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| |
| // Post Dated Checks |
| if (command.parameterExists("postDatedChecks")) { |
| // get repayment with post dates checks to update |
| Set<PostDatedChecks> postDatedChecks = this.repaymentWithPostDatedChecksAssembler.fromParsedJson(command.json(), loan); |
| updatePostDatedChecks(postDatedChecks); |
| } |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); |
| |
| Long disbursalTransactionId = null; |
| ExternalId disbursalTransactionExternalId = null; |
| |
| if (!isAccountTransfer) { |
| // If accounting is not periodic accrual, the last transaction might be the accrual not the disbursement |
| LoanTransaction disbursalTransaction = Lists.reverse(loan.getLoanTransactions()).stream() |
| .filter(e -> LoanTransactionType.DISBURSEMENT.equals(e.getTypeOf())).findFirst().orElseThrow(); |
| disbursalTransactionId = disbursalTransaction.getId(); |
| disbursalTransactionExternalId = disbursalTransaction.getExternalId(); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction)); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loan.getId()) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withSubEntityId(disbursalTransactionId) // |
| .withSubEntityExternalId(disbursalTransactionExternalId) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes) // |
| .build(); |
| } |
| |
| private void updatePostDatedChecks(Set<PostDatedChecks> postDatedChecks) { |
| this.postDatedChecksRepository.saveAll(postDatedChecks); |
| } |
| |
| private void createAndSaveLoanScheduleArchive(final Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO) { |
| LoanRescheduleRequest loanRescheduleRequest = null; |
| LoanScheduleModel loanScheduleModel = loan.regenerateScheduleModel(scheduleGeneratorDTO); |
| List<LoanRepaymentScheduleInstallment> installments = retrieveRepaymentScheduleFromModel(loanScheduleModel); |
| this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(installments, loan, loanRescheduleRequest); |
| } |
| |
| /** |
| * create standing instruction for disbursed loan |
| * |
| * @param loan |
| * the disbursed loan |
| **/ |
| private void createStandingInstruction(Loan loan) { |
| |
| if (loan.shouldCreateStandingInstructionAtDisbursement()) { |
| AccountAssociations accountAssociations = this.accountAssociationRepository.findByLoanIdAndType(loan.getId(), |
| AccountAssociationType.LINKED_ACCOUNT_ASSOCIATION.getValue()); |
| |
| if (accountAssociations != null) { |
| |
| SavingsAccount linkedSavingsAccount = accountAssociations.linkedSavingsAccount(); |
| |
| // name is auto-generated |
| final String name = "To loan " + loan.getAccountNumber() + " from savings " + linkedSavingsAccount.getAccountNumber(); |
| final Office fromOffice = loan.getOffice(); |
| final Client fromClient = loan.getClient(); |
| final Office toOffice = loan.getOffice(); |
| final Client toClient = loan.getClient(); |
| final Integer priority = StandingInstructionPriority.MEDIUM.getValue(); |
| final Integer transferType = AccountTransferType.LOAN_REPAYMENT.getValue(); |
| final Integer instructionType = StandingInstructionType.DUES.getValue(); |
| final Integer status = StandingInstructionStatus.ACTIVE.getValue(); |
| final Integer recurrenceType = AccountTransferRecurrenceType.AS_PER_DUES.getValue(); |
| final LocalDate validFrom = DateUtils.getBusinessLocalDate(); |
| |
| AccountTransferDetails accountTransferDetails = AccountTransferDetails.savingsToLoanTransfer(fromOffice, fromClient, |
| linkedSavingsAccount, toOffice, toClient, loan, transferType); |
| |
| AccountTransferStandingInstruction accountTransferStandingInstruction = AccountTransferStandingInstruction.create( |
| accountTransferDetails, name, priority, instructionType, status, null, validFrom, null, recurrenceType, null, null, |
| null); |
| accountTransferDetails.updateAccountTransferStandingInstruction(accountTransferStandingInstruction); |
| |
| this.accountTransferDetailRepository.save(accountTransferDetails); |
| } |
| } |
| } |
| |
| private void updateRecurringCalendarDatesForInterestRecalculation(final Loan loan) { |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() |
| && loan.loanInterestRecalculationDetails().getRestFrequencyType().isSameAsRepayment()) { |
| final CalendarInstance calendarInstanceForInterestRecalculation = this.calendarInstanceRepository |
| .findByEntityIdAndEntityTypeIdAndCalendarTypeId(loan.loanInterestRecalculationDetailId(), |
| CalendarEntityType.LOAN_RECALCULATION_REST_DETAIL.getValue(), CalendarType.COLLECTION.getValue()); |
| |
| Calendar calendarForInterestRecalculation = calendarInstanceForInterestRecalculation.getCalendar(); |
| calendarForInterestRecalculation.updateStartAndEndDate(loan.getDisbursementDate(), loan.getMaturityDate()); |
| this.calendarRepository.save(calendarForInterestRecalculation); |
| } |
| |
| } |
| |
| private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { |
| /* |
| * Due to the "saveAndFlushLoanWithDataIntegrityViolationChecks" method the loan is saved and flushed in the |
| * middle of the transaction. EclipseLink is in some situations are saving inconsistently the newly created |
| * associations, like the newly created repayment schedule installments. The save and flush cannot be removed |
| * safely till any native queries are used as part of this transaction either. See: |
| * this.loanAccountDomainService.recalculateAccruals(loan); |
| */ |
| try { |
| loanRepaymentScheduleInstallmentRepository.saveAll(loan.getRepaymentScheduleInstallments()); |
| return 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.transaction"); |
| if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { |
| baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique"); |
| } |
| if (!dataValidationErrors.isEmpty()) { |
| throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", |
| dataValidationErrors, e); |
| } |
| throw e; |
| } |
| } |
| |
| private void saveLoanWithDataIntegrityViolationChecks(final Loan loan) { |
| try { |
| this.loanRepositoryWrapper.save(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.transaction"); |
| if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { |
| baseDataValidator.reset().parameter(LoanApiConstants.externalIdParameterName).failWithCode("value.must.be.unique"); |
| } |
| if (!dataValidationErrors.isEmpty()) { |
| throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", |
| dataValidationErrors, e); |
| } |
| } |
| } |
| |
| /**** |
| * TODO Vishwas: Pair with Ashok and re-factor collection sheet code-base |
| * |
| * May of the changes made to disburseLoan aren't being made here, should refactor to reuse disburseLoan ASAP |
| *****/ |
| @Transactional |
| @Override |
| public Map<String, Object> bulkLoanDisbursal(final JsonCommand command, final CollectionSheetBulkDisbursalCommand bulkDisbursalCommand, |
| Boolean isAccountTransfer) { |
| final AppUser currentUser = getAppUserIfPresent(); |
| |
| final SingleDisbursalCommand[] disbursalCommand = bulkDisbursalCommand.getDisburseTransactions(); |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| if (disbursalCommand == null) { |
| return changes; |
| } |
| |
| final LocalDate nextPossibleRepaymentDate = null; |
| final LocalDate rescheduledRepaymentDate = null; |
| |
| for (final SingleDisbursalCommand singleLoanDisbursalCommand : disbursalCommand) { |
| Loan loan = this.loanAssembler.assembleFrom(singleLoanDisbursalCommand.getLoanId()); |
| final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); |
| |
| // validate ActualDisbursement Date Against Expected Disbursement |
| // Date |
| LoanProduct loanProduct = loan.loanProduct(); |
| if (loanProduct.syncExpectedWithDisbursementDate()) { |
| syncExpectedDateWithActualDisbursementDate(loan, actualDisbursementDate); |
| } |
| checkClientOrGroupActive(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanDisbursalBusinessEvent(loan)); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); |
| |
| // Bulk disbursement should happen on meeting date (mostly from |
| // collection sheet). |
| // FIXME: AA - this should be first meeting date based on |
| // disbursement date and next available meeting dates |
| // assuming repayment schedule won't regenerate because expected |
| // disbursement and actual disbursement happens on same date |
| loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); |
| updateLoanCounters(loan, actualDisbursementDate); |
| boolean canDisburse = loan.canDisburse(actualDisbursementDate); |
| ChangedTransactionDetail changedTransactionDetail = null; |
| if (canDisburse) { |
| Money amountBeforeAdjust = loan.getPrincipal(); |
| Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); |
| boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| if (isAccountTransfer) { |
| disburseLoanToSavings(loan, command, disburseAmount, paymentDetail); |
| existingTransactionIds.addAll(loan.findExistingTransactionIds()); |
| existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); |
| |
| } else { |
| existingTransactionIds.addAll(loan.findExistingTransactionIds()); |
| existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); |
| LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail, |
| actualDisbursementDate, txnExternalId); |
| disbursementTransaction.updateLoan(loan); |
| loan.addLoanTransaction(disbursementTransaction); |
| businessEventNotifierService |
| .notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursementTransaction)); |
| } |
| LocalDate recalculateFrom = null; |
| final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, |
| rescheduledRepaymentDate); |
| boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment(); |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { |
| createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| } |
| if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) { |
| changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); |
| } else { |
| changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null); |
| } |
| } |
| if (!changes.isEmpty()) { |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings() |
| .entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| } |
| final Set<LoanCharge> loanCharges = loan.getActiveCharges(); |
| final Map<Long, BigDecimal> disBuLoanCharges = new HashMap<>(); |
| for (final LoanCharge loanCharge : loanCharges) { |
| if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer() |
| && loanCharge.isChargePending()) { |
| disBuLoanCharges.put(loanCharge.getId(), loanCharge.amountOutstanding()); |
| } |
| } |
| final Locale locale = command.extractLocale(); |
| final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); |
| for (final Map.Entry<Long, BigDecimal> entrySet : disBuLoanCharges.entrySet()) { |
| final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService |
| .retriveLoanLinkedAssociation(loan.getId()); |
| final SavingsAccount fromSavingsAccount = null; |
| final boolean isRegularTransaction = true; |
| final boolean isExceptionForBalanceCheck = false; |
| final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), |
| PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.getId(), loan.getId(), |
| "Loan Charge Payment", locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), |
| entrySet.getKey(), null, AccountTransferType.CHARGE_PAYMENT.getValue(), null, null, ExternalId.empty(), null, null, |
| fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); |
| this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); |
| } |
| updateRecurringCalendarDatesForInterestRecalculation(loan); |
| loanAccountDomainService.recalculateAccruals(loan); |
| loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); |
| } |
| |
| return changes; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult undoGLIMLoanDisbursal(final Long loanId, final JsonCommand command) { |
| final Long parentLoanId = loanId; |
| GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow(); |
| List<Loan> childLoans = this.loanRepository.findByGlimId(loanId); |
| CommandProcessingResult result = null; |
| int count = 0; |
| for (Loan loan : childLoans) { |
| result = undoLoanDisbursal(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.APPROVED.getValue()); |
| glimRepository.save(parentLoan); |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult undoLoanDisbursal(final Long loanId, final JsonCommand command) { |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Undo Loan: " + loanId + " disbursement is not allowed. Loan Account is Charged-off", loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan)); |
| removeLoanCycle(loan); |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| // |
| final MonetaryCurrency currency = loan.getCurrency(); |
| |
| final LocalDate recalculateFrom = null; |
| loan.setActualDisbursementDate(null); |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| // Remove post dated checks if added. |
| loan.removePostDatedChecks(); |
| |
| final Map<String, Object> changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds, |
| existingReversedTransactionIds); |
| |
| if (!changes.isEmpty()) { |
| if (loan.isTopup() && loan.getClientId() != null) { |
| final Long loanIdToClose = loan.getTopupLoanDetails().getLoanIdToClose(); |
| final LocalDate expectedDisbursementDate = command |
| .localDateValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName); |
| BigDecimal loanOutstanding = this.loanReadPlatformService |
| .retrieveLoanPrePaymentTemplate(LoanTransactionType.REPAYMENT, loanIdToClose, expectedDisbursementDate).getAmount(); |
| BigDecimal netDisbursalAmount = loan.getApprovedPrincipal().subtract(loanOutstanding); |
| loan.adjustNetDisbursalAmount(netDisbursalAmount); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| this.accountTransfersWritePlatformService.reverseAllTransactions(loanId, PortfolioAccountType.LOAN); |
| String noteText; |
| if (command.hasParameter("note")) { |
| noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| } |
| boolean isAccountTransfer = false; |
| final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, |
| existingReversedTransactionIds, isAccountTransfer); |
| journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoDisbursalBusinessEvent(loan)); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loan.getId()) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT") |
| public CommandProcessingResult makeGLIMLoanRepayment(final Long loanId, final JsonCommand command) { |
| |
| final Long parentLoanId = loanId; |
| |
| glimRepository.findById(parentLoanId).orElseThrow(); |
| |
| JsonArray repayments = command.arrayOfParameterNamed("formDataArray"); |
| JsonCommand childCommand; |
| CommandProcessingResult result = null; |
| JsonObject jsonObject; |
| |
| Long[] childLoanId = new Long[repayments.size()]; |
| for (int i = 0; i < repayments.size(); i++) { |
| jsonObject = repayments.get(i).getAsJsonObject(); |
| log.debug("{}", jsonObject.toString()); |
| childLoanId[i] = jsonObject.get("loanId").getAsLong(); |
| } |
| int j = 0; |
| for (JsonElement element : repayments) { |
| childCommand = JsonCommand.fromExistingCommand(command, element); |
| result = makeLoanRepayment(LoanTransactionType.REPAYMENT, childLoanId[j++], childCommand, false); |
| } |
| return result; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult makeLoanRepayment(final LoanTransactionType repaymentTransactionType, final Long loanId, |
| final JsonCommand command, final boolean isRecoveryRepayment) { |
| final String chargeRefundChargeType = null; |
| return makeLoanRepaymentWithChargeRefundChargeType(repaymentTransactionType, loanId, command, isRecoveryRepayment, |
| chargeRefundChargeType); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType, |
| final Long loanId, final JsonCommand command, final boolean isRecoveryRepayment, final String chargeRefundChargeType) { |
| |
| this.loanUtilService.validateRepaymentTransactionType(repaymentTransactionType); |
| this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json()); |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId")); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| } |
| if (!txnExternalId.isEmpty()) { |
| changes.put(LoanApiConstants.externalIdParameterName, txnExternalId); |
| } |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); |
| final Boolean isHolidayValidationDone = false; |
| final HolidayDetailDTO holidayDetailDto = null; |
| boolean isAccountTransfer = false; |
| |
| LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(repaymentTransactionType, loan, transactionDate, |
| transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, |
| holidayDetailDto, isHolidayValidationDone); |
| loan = loanTransaction.getLoan(); |
| // Update loan transaction on repayment. |
| if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { |
| Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements(); |
| for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { |
| loanCollateralManagement.setLoanTransactionData(loanTransaction); |
| ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); |
| |
| if (loan.getStatus().isClosed()) { |
| loanCollateralManagement.setIsReleased(true); |
| BigDecimal quantity = loanCollateralManagement.getQuantity(); |
| clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); |
| loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); |
| } |
| } |
| this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); |
| } |
| |
| return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // |
| .withLoanId(loan.getId()) // |
| .withEntityId(loanTransaction.getId()) // |
| .withEntityExternalId(loanTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public Map<String, Object> makeLoanBulkRepayment(final CollectionSheetBulkRepaymentCommand bulkRepaymentCommand) { |
| |
| final SingleRepaymentCommand[] repaymentCommand = bulkRepaymentCommand.getLoanTransactions(); |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| final boolean isRecoveryRepayment = false; |
| |
| if (repaymentCommand == null) { |
| return changes; |
| } |
| List<Long> transactionIds = new ArrayList<>(); |
| boolean isAccountTransfer = false; |
| HolidayDetailDTO holidayDetailDTO = null; |
| boolean isHolidayValidationDone = false; |
| final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); |
| for (final SingleRepaymentCommand singleLoanRepaymentCommand : repaymentCommand) { |
| if (singleLoanRepaymentCommand != null) { |
| Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(singleLoanRepaymentCommand.getLoanId()); |
| final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), |
| singleLoanRepaymentCommand.getTransactionDate()); |
| final WorkingDays workingDays = this.workingDaysRepository.findOne(); |
| final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); |
| boolean isHolidayEnabled; |
| isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); |
| holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, |
| allowTransactionsOnNonWorkingDay); |
| loan.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(), |
| holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); |
| loan.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(), |
| holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); |
| isHolidayValidationDone = true; |
| break; |
| } |
| |
| } |
| for (final SingleRepaymentCommand singleLoanRepaymentCommand : repaymentCommand) { |
| if (singleLoanRepaymentCommand != null) { |
| final Loan loan = this.loanAssembler.assembleFrom(singleLoanRepaymentCommand.getLoanId()); |
| final PaymentDetail paymentDetail = singleLoanRepaymentCommand.getPaymentDetail(); |
| ExternalId externalId = singleLoanRepaymentCommand.getExternalId(); |
| if (externalId.isEmpty() && configurationDomainService.isExternalIdAutoGenerationEnabled()) { |
| externalId = ExternalId.generate(); |
| } |
| if (paymentDetail != null && paymentDetail.getId() == null) { |
| this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); |
| } |
| final String chargeRefundChargeType = null; |
| LoanTransaction loanTransaction = this.loanAccountDomainService.makeRepayment(LoanTransactionType.REPAYMENT, loan, |
| bulkRepaymentCommand.getTransactionDate(), singleLoanRepaymentCommand.getTransactionAmount(), paymentDetail, |
| bulkRepaymentCommand.getNote(), externalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, |
| holidayDetailDTO, isHolidayValidationDone); |
| transactionIds.add(loanTransaction.getId()); |
| } |
| } |
| changes.put("loanTransactions", transactionIds); |
| return changes; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult adjustLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateTransaction(command.json()); |
| LoanTransaction transactionToAdjust = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) |
| .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| if (loan.getStatus().isClosed() && loan.getLoanSubStatus() != null |
| && loan.getLoanSubStatus().equals(LoanSubStatus.FORECLOSED.getValue())) { |
| final String defaultUserMessage = "The loan cannot reopened as it is foreclosed."; |
| throw new LoanForeclosureException("loan.cannot.be.reopened.as.it.is.foreclosured", defaultUserMessage, loanId); |
| } |
| checkClientOrGroupActive(loan); |
| |
| businessEventNotifierService.notifyPreBusinessEvent( |
| new LoanAdjustTransactionBusinessEvent(new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust))); |
| if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed", |
| "Loan transaction:" + transactionId + " update not allowed as it involves in account transfer", transactionId); |
| } |
| if (loan.isClosedWrittenOff()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.written.off.update.not.allowed", |
| "Loan transaction:" + transactionId + " update not allowed as loan status is written off", transactionId); |
| } |
| |
| if (transactionToAdjust.hasChargebackLoanTransactionRelations()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.transaction.update.not.allowed", |
| "Loan transaction:" + transactionId + " update not allowed as loan transaction is linked to other transactions", |
| transactionId); |
| } |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final boolean isAdjustCommand = (transactionAmount.compareTo(BigDecimal.ZERO) > 0); |
| if (isAdjustCommand && !transactionToAdjust.isEditable()) { |
| final String errorMessage = "Loan transaction: " + transactionId + " update not allowed as loan transaction is a " |
| + transactionToAdjust.getTypeOf().getCode(); |
| throw new InvalidLoanTransactionTypeException("transaction", "error.msg.loan.transaction.update.not.allowed", errorMessage); |
| } |
| |
| // We dont need auto generation for reversal external id... if it is not provided, it remains null (empty) |
| final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); |
| final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| changes.put("paymentTypeId", command.longValueOfParameterNamed("paymentTypeId")); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount); |
| final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes); |
| LoanTransaction newTransactionDetail = LoanTransaction.repayment(loan.getOffice(), transactionAmountAsMoney, paymentDetail, |
| transactionDate, txnExternalId); |
| if (transactionToAdjust.isInterestWaiver()) { |
| Money unrecognizedIncome = transactionAmountAsMoney.zero(); |
| Money interestComponent = transactionAmountAsMoney; |
| if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { |
| Money receivableInterest = loan.getReceivableInterest(transactionDate); |
| if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { |
| interestComponent = receivableInterest; |
| unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest); |
| } |
| } |
| newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, transactionDate, |
| interestComponent, unrecognizedIncome, txnExternalId); |
| } |
| |
| LocalDate recalculateFrom = null; |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| recalculateFrom = DateUtils.isAfter(transactionToAdjust.getTransactionDate(), transactionDate) ? transactionDate |
| : transactionToAdjust.getTransactionDate(); |
| } |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, |
| defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, |
| scheduleGeneratorDTO, reversalTxnExternalId); |
| |
| boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency()); |
| if (thereIsNewTransaction) { |
| if (paymentDetail != null) { |
| this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); |
| } |
| this.loanTransactionRepository.saveAndFlush(newTransactionDetail); |
| } |
| |
| /* |
| * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the |
| * time being, not a major issue for now as this loop is entered only in edge cases (when a adjustment is made |
| * before the latest payment recorded against the loan) |
| */ |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| Note note; |
| /** |
| * If a new transaction is not created, associate note with the transaction to be adjusted |
| **/ |
| if (thereIsNewTransaction) { |
| note = Note.loanTransactionNote(loan, newTransactionDetail, noteText); |
| } else { |
| note = Note.loanTransactionNote(loan, transactionToAdjust, noteText); |
| } |
| this.noteRepository.save(note); |
| } |
| |
| Collection<Long> transactionIds = new ArrayList<>(); |
| List<LoanTransaction> transactions = loan.getLoanTransactions(); |
| for (LoanTransaction transaction : transactions) { |
| if (transaction.isRefund() && transaction.isNotReversed()) { |
| transactionIds.add(transaction.getId()); |
| } |
| } |
| |
| if (!transactionIds.isEmpty()) { |
| this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds, |
| PortfolioAccountType.LOAN); |
| loan.updateLoanSummaryAndStatus(); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| |
| LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(transactionToAdjust); |
| if (newTransactionDetail.isRepaymentLikeType() && thereIsNewTransaction) { |
| eventData.setNewTransactionDetail(newTransactionDetail); |
| } |
| Long entityId = transactionToAdjust.getId(); |
| ExternalId entityExternalId = transactionToAdjust.getExternalId(); |
| |
| if (thereIsNewTransaction) { |
| entityId = newTransactionDetail.getId(); |
| entityExternalId = newTransactionDetail.getExternalId(); |
| } |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData)); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(entityId) // |
| .withEntityExternalId(entityExternalId) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult chargebackLoanTransaction(final Long loanId, final Long transactionId, final JsonCommand command) { |
| this.loanEventApiJsonValidator.validateChargebackTransaction(command.json()); |
| |
| LoanTransaction loanTransaction = this.loanTransactionRepository.findByIdAndLoanId(command.entityId(), command.getLoanId()) |
| .orElseThrow(() -> new LoanTransactionNotFoundException(command.entityId(), command.getLoanId())); |
| |
| if (loanTransaction.isReversed()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", |
| "Loan transaction:" + transactionId + " chargeback not allowed as loan transaction repayment is reversed", |
| transactionId); |
| } |
| |
| if (!loanTransaction.isTypeAllowedForChargeback()) { |
| throw new PlatformServiceUnavailableException( |
| "error.msg.loan.chargeback.operation.not.allowed", "Loan transaction:" + transactionId |
| + " chargeback not allowed for loan transaction type, its type is " + loanTransaction.getTypeOf().getCode(), |
| transactionId); |
| } |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| if (this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.LOAN)) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.transfer.transaction.update.not.allowed", |
| "Loan transaction:" + transactionId + " chargeback not allowed as it involves in account transfer", transactionId); |
| } |
| if (loan.isClosedWrittenOff()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", |
| "Loan transaction:" + transactionId + " chargeback not allowed as loan status is written off", transactionId); |
| } |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", |
| "Loan transaction:" + transactionId + " chargeback not allowed as loan product is interest recalculation enabled", |
| transactionId); |
| } |
| checkClientOrGroupActive(loan); |
| |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanChargebackTransactionBusinessEvent(loanTransaction)); |
| |
| final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed(LoanApiConstants.TRANSACTION_AMOUNT_PARAMNAME)); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| changes.put("paymentTypeId", command.longValueOfParameterNamed(LoanApiConstants.PAYMENT_TYPE_PARAMNAME)); |
| |
| final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount); |
| PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes); |
| if (paymentDetail != null) { |
| paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); |
| } |
| LoanTransaction newTransaction = LoanTransaction.chargeback(loan, transactionAmountAsMoney, paymentDetail, transactionDate, |
| txnExternalId); |
| |
| validateLoanTransactionAmountChargeBack(loanTransaction, newTransaction); |
| |
| // Store the Loan Transaction Relation |
| LoanTransactionRelation loanTransactionRelation = LoanTransactionRelation.linkToTransaction(loanTransaction, newTransaction, |
| LoanTransactionRelationTypeEnum.CHARGEBACK); |
| this.loanTransactionRelationRepository.save(loanTransactionRelation); |
| |
| newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction); |
| |
| loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine); |
| |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParamName); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| Note note = Note.loanTransactionNote(loan, newTransaction, noteText); |
| this.noteRepository.save(note); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanChargebackTransactionBusinessEvent(newTransaction)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(newTransaction.getId()) // |
| .withEntityExternalId(newTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| private void validateLoanTransactionAmountChargeBack(LoanTransaction loanTransaction, LoanTransaction chargebackTransaction) { |
| BigDecimal actualAmount = BigDecimal.ZERO; |
| for (LoanTransactionRelation loanTransactionRelation : loanTransaction.getLoanTransactionRelations()) { |
| if (loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK) |
| && loanTransactionRelation.getToTransaction().isNotReversed()) { |
| actualAmount = actualAmount.add(loanTransactionRelation.getToTransaction().getAmount()); |
| } |
| } |
| actualAmount = actualAmount.add(chargebackTransaction.getAmount()); |
| if (loanTransaction.getAmount() != null && actualAmount.compareTo(loanTransaction.getAmount()) > 0) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.chargeback.operation.not.allowed", |
| "Loan transaction:" + loanTransaction.getId() + " chargeback not allowed as loan transaction amount is not enough", |
| loanTransaction.getId()); |
| } |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateTransaction(command.json()); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| final Money transactionAmountAsMoney = Money.of(loan.getCurrency(), transactionAmount); |
| Money unrecognizedIncome = transactionAmountAsMoney.zero(); |
| Money interestComponent = transactionAmountAsMoney; |
| if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { |
| Money receivableInterest = loan.getReceivableInterest(transactionDate); |
| if (transactionAmountAsMoney.isGreaterThan(receivableInterest)) { |
| interestComponent = receivableInterest; |
| unrecognizedIncome = transactionAmountAsMoney.minus(receivableInterest); |
| } |
| } |
| final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(loan.getOffice(), loan, transactionAmountAsMoney, |
| transactionDate, interestComponent, unrecognizedIncome, externalId); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); |
| LocalDate recalculateFrom = null; |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| recalculateFrom = transactionDate; |
| } |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, |
| defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); |
| |
| this.loanTransactionRepository.saveAndFlush(waiveInterestTransaction); |
| |
| /*** |
| * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the |
| * time being, not a major issue for now as this loop is entered only in edge cases (when a waiver is made |
| * before the latest payment recorded against the loan) |
| ***/ |
| |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| final Note note = Note.loanTransactionNote(loan, waiveInterestTransaction, noteText); |
| this.noteRepository.save(note); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| loanAccountDomainService.recalculateAccruals(loan); |
| loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(waiveInterestTransaction.getId()) // |
| .withEntityExternalId(waiveInterestTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult writeOff(final Long loanId, final JsonCommand command) { |
| final AppUser currentUser = getAppUserIfPresent(); |
| |
| this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json()); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| if (command.hasParameter("writeoffReasonId")) { |
| Long writeoffReasonId = command.longValueOfParameterNamed("writeoffReasonId"); |
| CodeValue writeoffReason = this.codeValueRepository |
| .findOneByCodeNameAndIdWithNotFoundDetection(LoanApiConstants.WRITEOFFREASONS, writeoffReasonId); |
| changes.put("writeoffReasonId", writeoffReasonId); |
| loan.updateWriteOffReason(writeoffReason); |
| } |
| |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff() && DateUtils.isBefore(transactionDate, loan.getChargedOffOnDate())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " |
| + loanId |
| + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", |
| loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanWrittenOffPreBusinessEvent(loan)); |
| entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), |
| StatusEnum.WRITE_OFF.getCode().longValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId()); |
| |
| removeLoanCycle(loan); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| updateLoanCounters(loan, loan.getDisbursementDate()); |
| |
| LocalDate recalculateFrom = null; |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| recalculateFrom = command.localDateValueOfParameterNamed("transactionDate"); |
| } |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes, |
| existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); |
| LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L); |
| this.loanTransactionRepository.saveAndFlush(writeOff); |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| this.loanTransactionRepository.save(mapEntry.getValue()); |
| this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| final Note note = Note.loanTransactionNote(loan, writeOff, noteText); |
| this.noteRepository.save(note); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| loanAccountDomainService.recalculateAccruals(loan); |
| loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(writeOff.getId()) // |
| .withEntityExternalId(writeOff.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json()); |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| if (loan.isChargedOff() && DateUtils.isBefore(transactionDate, loan.getChargedOffOnDate())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: " |
| + loanId |
| + " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan", |
| loanId); |
| } |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseBusinessEvent(loan)); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| updateLoanCounters(loan, loan.getDisbursementDate()); |
| |
| LocalDate recalculateFrom = null; |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| recalculateFrom = command.localDateValueOfParameterNamed("transactionDate"); |
| } |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes, |
| existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); |
| final LoanTransaction possibleClosingTransaction = changedTransactionDetail.getNewTransactionMappings().remove(0L); |
| if (possibleClosingTransaction != null) { |
| this.loanTransactionRepository.saveAndFlush(possibleClosingTransaction); |
| } |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| this.loanTransactionRepository.save(mapEntry.getValue()); |
| this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| |
| if (possibleClosingTransaction != null) { |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| } |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| loanAccountDomainService.recalculateAccruals(loan); |
| |
| loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan)); |
| |
| // Update loan transaction on repayment. |
| if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { |
| Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements(); |
| for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { |
| ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); |
| |
| if (loan.getStatus().isClosed()) { |
| loanCollateralManagement.setIsReleased(true); |
| BigDecimal quantity = loanCollateralManagement.getQuantity(); |
| clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); |
| loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); |
| } |
| } |
| this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); |
| } |
| |
| // disable all active standing instructions linked to the loan |
| this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); |
| |
| CommandProcessingResult result; |
| if (possibleClosingTransaction != null) { |
| |
| result = new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(possibleClosingTransaction.getId()) // |
| .withEntityExternalId(possibleClosingTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } else { |
| result = new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanId) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| return result; |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateTransactionWithNoAmount(command.json()); |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Loan: " + loanId + " Close as rescheduled is not allowed. Loan Account is Charged-off", loanId); |
| } |
| removeLoanCycle(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanCloseAsRescheduleBusinessEvent(loan)); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| |
| loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine, changes); |
| |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseAsRescheduleBusinessEvent(loan)); |
| |
| // disable all active standing instructions linked to the loan |
| this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); |
| |
| // Update loan transaction on repayment. |
| if (AccountType.fromInt(loan.getLoanType()).isIndividualAccount()) { |
| Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements(); |
| for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { |
| ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); |
| |
| if (loan.getStatus().isClosed()) { |
| loanCollateralManagement.setIsReleased(true); |
| BigDecimal quantity = loanCollateralManagement.getQuantity(); |
| clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); |
| loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); |
| } |
| } |
| this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanId) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes) // |
| .build(); |
| } |
| |
| private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) { |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final Locale locale = command.extractLocale(); |
| final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); |
| final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.LOAN, |
| PortfolioAccountType.LOAN, loan.getId(), loan.getTopupLoanDetails().getLoanIdToClose(), "Loan Topup", locale, fmt, |
| LoanTransactionType.DISBURSEMENT.getValue(), LoanTransactionType.REPAYMENT.getValue(), txnExternalId, loan, null); |
| AccountTransferDetails accountTransferDetails = this.accountTransfersWritePlatformService.repayLoanWithTopup(accountTransferDTO); |
| loan.getTopupLoanDetails().setAccountTransferDetails(accountTransferDetails.getId()); |
| loan.getTopupLoanDetails().setTopupAmount(amount); |
| } |
| |
| private void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) { |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final Locale locale = command.extractLocale(); |
| final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); |
| final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService |
| .retriveLoanLinkedAssociation(loan.getId()); |
| if (portfolioAccountData == null) { |
| final String errorMessage = "Disburse Loan with id:" + loan.getId() + " requires linked savings account for payment"; |
| throw new LinkedAccountRequiredException("loan.disburse.to.savings", errorMessage, loan.getId()); |
| } |
| final SavingsAccount fromSavingsAccount = null; |
| final boolean isExceptionForBalanceCheck = false; |
| final boolean isRegularTransaction = true; |
| final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount.getAmount(), PortfolioAccountType.LOAN, |
| PortfolioAccountType.SAVINGS, loan.getId(), portfolioAccountData.getId(), "Loan Disbursement", locale, fmt, paymentDetail, |
| LoanTransactionType.DISBURSEMENT.getValue(), null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, |
| txnExternalId, loan, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); |
| this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); |
| |
| } |
| |
| @Transactional |
| @Override |
| public LoanTransaction initiateLoanTransfer(final Loan loan, final LocalDate transferDate) { |
| |
| this.loanAssembler.setHelpers(loan); |
| checkClientOrGroupActive(loan); |
| validateTransactionsForTransfer(loan, transferDate); |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanInitiateTransferBusinessEvent(loan)); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); |
| ExternalId externalId = externalIdFactory.create(); |
| final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate, externalId); |
| loan.addLoanTransaction(newTransferTransaction); |
| LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; |
| loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan); |
| |
| this.loanTransactionRepository.saveAndFlush(newTransferTransaction); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanInitiateTransferBusinessEvent(loan)); |
| return newTransferTransaction; |
| } |
| |
| @Transactional |
| @Override |
| public LoanTransaction acceptLoanTransfer(final Loan loan, final LocalDate transferDate, final Office acceptedInOffice, |
| final Staff loanOfficer) { |
| this.loanAssembler.setHelpers(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanAcceptTransferBusinessEvent(loan)); |
| final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); |
| ExternalId externalId = externalIdFactory.create(); |
| final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate, |
| externalId); |
| loan.addLoanTransaction(newTransferAcceptanceTransaction); |
| LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; |
| if (loan.getTotalOverpaid() != null) { |
| loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan); |
| } else { |
| loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); |
| } |
| if (loanOfficer != null) { |
| loan.reassignLoanOfficer(loanOfficer, transferDate); |
| } |
| |
| this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanAcceptTransferBusinessEvent(loan)); |
| |
| return newTransferAcceptanceTransaction; |
| } |
| |
| @Transactional |
| @Override |
| public LoanTransaction withdrawLoanTransfer(final Loan loan, final LocalDate transferDate) { |
| this.loanAssembler.setHelpers(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan)); |
| |
| final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); |
| |
| ExternalId externalId = externalIdFactory.create(); |
| |
| final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate, |
| externalId); |
| loan.addLoanTransaction(newTransferAcceptanceTransaction); |
| LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; |
| loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan); |
| |
| this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan)); |
| |
| return newTransferAcceptanceTransaction; |
| } |
| |
| @Transactional |
| @Override |
| public void rejectLoanTransfer(final Loan loan) { |
| this.loanAssembler.setHelpers(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); |
| LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; |
| loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECT_TRANSFER, loan); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult loanReassignment(final Long loanId, final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateUpdateOfLoanOfficer(command.json()); |
| |
| final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId"); |
| final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId"); |
| |
| final Staff fromLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(fromLoanOfficerId); |
| final Staff toLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(toLoanOfficerId); |
| final LocalDate dateOfLoanOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); |
| if (!loan.hasLoanOfficer(fromLoanOfficer)) { |
| throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); |
| } |
| |
| loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); |
| |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanId) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateForBulkLoanReassignment(command.json()); |
| |
| final Long fromLoanOfficerId = command.longValueOfParameterNamed("fromLoanOfficerId"); |
| final Long toLoanOfficerId = command.longValueOfParameterNamed("toLoanOfficerId"); |
| final String[] loanIds = command.arrayValueOfParameterNamed("loans"); |
| |
| final LocalDate dateOfLoanOfficerAssignment = command.localDateValueOfParameterNamed("assignmentDate"); |
| |
| final Staff fromLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(fromLoanOfficerId); |
| final Staff toLoanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(toLoanOfficerId); |
| List<Long> lockedLoanIds = new ArrayList<>(); |
| |
| for (final String loanIdString : loanIds) { |
| final Long loanId = Long.valueOf(loanIdString); |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| if (loanAccountLockService.isLoanHardLocked(loanId)) { |
| lockedLoanIds.add(loanId); |
| } else { |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); |
| checkClientOrGroupActive(loan); |
| |
| if (!loan.hasLoanOfficer(fromLoanOfficer)) { |
| throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); |
| } |
| |
| loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); |
| } |
| } |
| if (!lockedLoanIds.isEmpty()) { |
| throw new LoanAccountLockCannotBeOverruledException("There are hard-lcoked loan accounts: " + lockedLoanIds); |
| } |
| this.loanRepositoryWrapper.flush(); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .build(); |
| } |
| |
| @Transactional |
| @Override |
| public CommandProcessingResult removeLoanOfficer(final Long loanId, final JsonCommand command) { |
| |
| final LoanUpdateCommand loanUpdateCommand = this.loanUpdateCommandFromApiJsonDeserializer.commandFromApiJson(command.json()); |
| |
| loanUpdateCommand.validate(); |
| |
| final LocalDate dateOfLoanOfficerUnassigned = command.localDateValueOfParameterNamed("unassignedDate"); |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| |
| if (loan.getLoanOfficer() == null) { |
| throw new LoanOfficerUnassignmentException(loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanRemoveOfficerBusinessEvent(loan)); |
| |
| loan.removeLoanOfficer(dateOfLoanOfficerUnassigned); |
| |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanRemoveOfficerBusinessEvent(loan)); |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loanId) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .build(); |
| } |
| |
| private void postJournalEntries(final Loan loan, final List<Long> existingTransactionIds, |
| final List<Long> existingReversedTransactionIds) { |
| |
| final MonetaryCurrency currency = loan.getCurrency(); |
| boolean isAccountTransfer = false; |
| List<Map<String, Object>> accountingBridgeData = new ArrayList<>(); |
| if (loan.isChargedOff()) { |
| accountingBridgeData = loan.deriveAccountingBridgeDataForChargeOff(currency.getCode(), existingTransactionIds, |
| existingReversedTransactionIds, isAccountTransfer); |
| } else { |
| accountingBridgeData.add(loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, |
| existingReversedTransactionIds, isAccountTransfer)); |
| } |
| for (Map<String, Object> accountingData : accountingBridgeData) { |
| this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingData); |
| } |
| |
| } |
| |
| @Transactional |
| @Override |
| public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances) { |
| |
| final Boolean rescheduleBasedOnMeetingDates = null; |
| final LocalDate presentMeetingDate = null; |
| final LocalDate newMeetingDate = null; |
| |
| applyMeetingDateChanges(calendar, loanCalendarInstances, rescheduleBasedOnMeetingDates, presentMeetingDate, newMeetingDate); |
| |
| } |
| |
| @Transactional |
| @Override |
| public void applyMeetingDateChanges(final Calendar calendar, final Collection<CalendarInstance> loanCalendarInstances, |
| final Boolean rescheduleBasedOnMeetingDates, final LocalDate presentMeetingDate, final LocalDate newMeetingDate) { |
| |
| final boolean isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); |
| final WorkingDays workingDays = this.workingDaysRepository.findOne(); |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| final Collection<Integer> loanStatuses = new ArrayList<>(Arrays.asList(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), |
| LoanStatus.APPROVED.getValue(), LoanStatus.ACTIVE.getValue())); |
| final Collection<Integer> loanTypes = new ArrayList<>(Arrays.asList(AccountType.GROUP.getValue(), AccountType.JLG.getValue())); |
| final Collection<Long> loanIds = new ArrayList<>(loanCalendarInstances.size()); |
| // loop through loanCalendarInstances to get loan ids |
| for (final CalendarInstance calendarInstance : loanCalendarInstances) { |
| loanIds.add(calendarInstance.getEntityId()); |
| } |
| |
| final List<Loan> loans = this.loanRepositoryWrapper.findByIdsAndLoanStatusAndLoanType(loanIds, loanStatuses, loanTypes); |
| List<Holiday> holidays; |
| final LocalDate recalculateFrom = null; |
| // loop through each loan to reschedule the repayment dates |
| for (final Loan loan : loans) { |
| if (loan != null) { |
| if (loan.getExpectedFirstRepaymentOnDate() != null && loan.getExpectedFirstRepaymentOnDate().equals(presentMeetingDate)) { |
| final String defaultUserMessage = "Meeting calendar date update is not supported since its a first repayment date"; |
| throw new CalendarParameterUpdateNotSupportedException("meeting.for.first.repayment.date", defaultUserMessage, |
| loan.getExpectedFirstRepaymentOnDate(), presentMeetingDate); |
| } |
| |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Loan: " + loan.getId() + " reschedule is not allowed. Loan Account is Charged-off", loan.getId()); |
| } |
| |
| Boolean isSkipRepaymentOnFirstMonth = false; |
| int numberOfDays = 0; |
| boolean isSkipRepaymentOnFirstMonthEnabled = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled(); |
| if (isSkipRepaymentOnFirstMonthEnabled) { |
| isSkipRepaymentOnFirstMonth = this.loanUtilService.isLoanRepaymentsSyncWithMeeting(loan.group(), calendar); |
| if (isSkipRepaymentOnFirstMonth) { |
| numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue(); |
| } |
| } |
| |
| holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), loan.getDisbursementDate()); |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| loan.setHelpers(null, this.loanSummaryWrapper, this.transactionProcessingStrategy); |
| loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO, existingTransactionIds, |
| existingReversedTransactionIds); |
| createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| } else if (rescheduleBasedOnMeetingDates != null && rescheduleBasedOnMeetingDates) { |
| loan.updateLoanRepaymentScheduleDates(calendar.getRecurrence(), isHolidayEnabled, holidays, workingDays, |
| presentMeetingDate, newMeetingDate, isSkipRepaymentOnFirstMonth, numberOfDays); |
| } else { |
| loan.updateLoanRepaymentScheduleDates(calendar.getStartDateLocalDate(), calendar.getRecurrence(), isHolidayEnabled, |
| holidays, workingDays, isSkipRepaymentOnFirstMonth, numberOfDays); |
| } |
| |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueCalendarChangeBusinessEvent(loan)); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| } |
| } |
| } |
| |
| private void removeLoanCycle(final Loan loan) { |
| final List<Loan> loansToUpdate; |
| if (loan.isGroupLoan()) { |
| if (loan.loanProduct().isIncludeInBorrowerCycle()) { |
| loansToUpdate = this.loanRepositoryWrapper.getGroupLoansToUpdateLoanCounter(loan.getCurrentLoanCounter(), loan.getGroupId(), |
| AccountType.GROUP.getValue()); |
| } else { |
| loansToUpdate = this.loanRepositoryWrapper.getGroupLoansToUpdateLoanProductCounter(loan.getLoanProductLoanCounter(), |
| loan.getGroupId(), AccountType.GROUP.getValue()); |
| } |
| |
| } else { |
| if (loan.loanProduct().isIncludeInBorrowerCycle()) { |
| loansToUpdate = this.loanRepositoryWrapper.getClientOrJLGLoansToUpdateLoanCounter(loan.getCurrentLoanCounter(), |
| loan.getClientId()); |
| } else { |
| loansToUpdate = this.loanRepositoryWrapper.getClientLoansToUpdateLoanProductCounter(loan.getLoanProductLoanCounter(), |
| loan.getClientId()); |
| } |
| |
| } |
| if (loansToUpdate != null) { |
| updateLoanCycleCounter(loansToUpdate, loan); |
| } |
| loan.updateClientLoanCounter(null); |
| loan.updateLoanProductLoanCounter(null); |
| |
| } |
| |
| private void updateLoanCounters(final Loan loan, final LocalDate actualDisbursementDate) { |
| |
| if (loan.isGroupLoan()) { |
| final List<Loan> loansToUpdateForLoanCounter = this.loanRepositoryWrapper.getGroupLoansDisbursedAfter(actualDisbursementDate, |
| loan.getGroupId(), AccountType.GROUP.getValue()); |
| final Integer newLoanCounter = getNewGroupLoanCounter(loan); |
| final Integer newLoanProductCounter = getNewGroupLoanProductCounter(loan); |
| updateLoanCounter(loan, loansToUpdateForLoanCounter, newLoanCounter, newLoanProductCounter); |
| } else { |
| final List<Loan> loansToUpdateForLoanCounter = this.loanRepositoryWrapper |
| .getClientOrJLGLoansDisbursedAfter(actualDisbursementDate, loan.getClientId()); |
| final Integer newLoanCounter = getNewClientOrJLGLoanCounter(loan); |
| final Integer newLoanProductCounter = getNewClientOrJLGLoanProductCounter(loan); |
| updateLoanCounter(loan, loansToUpdateForLoanCounter, newLoanCounter, newLoanProductCounter); |
| } |
| } |
| |
| private Integer getNewGroupLoanCounter(final Loan loan) { |
| |
| Integer maxClientLoanCounter = this.loanRepositoryWrapper.getMaxGroupLoanCounter(loan.getGroupId(), AccountType.GROUP.getValue()); |
| if (maxClientLoanCounter == null) { |
| maxClientLoanCounter = 1; |
| } else { |
| maxClientLoanCounter = maxClientLoanCounter + 1; |
| } |
| return maxClientLoanCounter; |
| } |
| |
| private Integer getNewGroupLoanProductCounter(final Loan loan) { |
| |
| Integer maxLoanProductLoanCounter = this.loanRepositoryWrapper.getMaxGroupLoanProductCounter(loan.loanProduct().getId(), |
| loan.getGroupId(), AccountType.GROUP.getValue()); |
| if (maxLoanProductLoanCounter == null) { |
| maxLoanProductLoanCounter = 1; |
| } else { |
| maxLoanProductLoanCounter = maxLoanProductLoanCounter + 1; |
| } |
| return maxLoanProductLoanCounter; |
| } |
| |
| private void updateLoanCounter(final Loan loan, final List<Loan> loansToUpdateForLoanCounter, Integer newLoanCounter, |
| Integer newLoanProductCounter) { |
| |
| final boolean includeInBorrowerCycle = loan.loanProduct().isIncludeInBorrowerCycle(); |
| for (final Loan loanToUpdate : loansToUpdateForLoanCounter) { |
| // Update client loan counter if loan product includeInBorrowerCycle |
| // is true |
| if (loanToUpdate.loanProduct().isIncludeInBorrowerCycle()) { |
| Integer currentLoanCounter = loanToUpdate.getCurrentLoanCounter() == null ? 1 : loanToUpdate.getCurrentLoanCounter(); |
| if (newLoanCounter > currentLoanCounter) { |
| newLoanCounter = currentLoanCounter; |
| } |
| loanToUpdate.updateClientLoanCounter(++currentLoanCounter); |
| } |
| |
| if (Objects.equals(loan.loanProduct().getId(), loanToUpdate.loanProduct().getId())) { |
| Integer loanProductLoanCounter = loanToUpdate.getLoanProductLoanCounter(); |
| if (newLoanProductCounter > loanProductLoanCounter) { |
| newLoanProductCounter = loanProductLoanCounter; |
| } |
| loanToUpdate.updateLoanProductLoanCounter(++loanProductLoanCounter); |
| } |
| } |
| |
| if (includeInBorrowerCycle) { |
| loan.updateClientLoanCounter(newLoanCounter); |
| } else { |
| loan.updateClientLoanCounter(null); |
| } |
| loan.updateLoanProductLoanCounter(newLoanProductCounter); |
| this.loanRepositoryWrapper.save(loansToUpdateForLoanCounter); |
| } |
| |
| private Integer getNewClientOrJLGLoanCounter(final Loan loan) { |
| |
| Integer maxClientLoanCounter = this.loanRepositoryWrapper.getMaxClientOrJLGLoanCounter(loan.getClientId()); |
| if (maxClientLoanCounter == null) { |
| maxClientLoanCounter = 1; |
| } else { |
| maxClientLoanCounter = maxClientLoanCounter + 1; |
| } |
| return maxClientLoanCounter; |
| } |
| |
| private Integer getNewClientOrJLGLoanProductCounter(final Loan loan) { |
| |
| Integer maxLoanProductLoanCounter = this.loanRepositoryWrapper.getMaxClientOrJLGLoanProductCounter(loan.loanProduct().getId(), |
| loan.getClientId()); |
| if (maxLoanProductLoanCounter == null) { |
| maxLoanProductLoanCounter = 1; |
| } else { |
| maxLoanProductLoanCounter = maxLoanProductLoanCounter + 1; |
| } |
| return maxLoanProductLoanCounter; |
| } |
| |
| private void updateLoanCycleCounter(final List<Loan> loansToUpdate, final Loan loan) { |
| |
| final Integer currentLoanCounter = loan.getCurrentLoanCounter(); |
| final Integer currentLoanProductCounter = loan.getLoanProductLoanCounter(); |
| |
| for (final Loan loanToUpdate : loansToUpdate) { |
| if (loan.loanProduct().isIncludeInBorrowerCycle()) { |
| Integer runningLoanCounter = loanToUpdate.getCurrentLoanCounter(); |
| if (runningLoanCounter > currentLoanCounter) { |
| loanToUpdate.updateClientLoanCounter(--runningLoanCounter); |
| } |
| } |
| if (Objects.equals(loan.loanProduct().getId(), loanToUpdate.loanProduct().getId())) { |
| Integer runningLoanProductCounter = loanToUpdate.getLoanProductLoanCounter(); |
| if (runningLoanProductCounter > currentLoanProductCounter) { |
| loanToUpdate.updateLoanProductLoanCounter(--runningLoanProductCounter); |
| } |
| } |
| } |
| this.loanRepositoryWrapper.save(loansToUpdate); |
| } |
| |
| private void checkClientOrGroupActive(final Loan loan) { |
| final Client client = loan.client(); |
| if (client != null && client.isNotActive()) { |
| throw new ClientNotActiveException(client.getId()); |
| } |
| final Group group = loan.group(); |
| if (group != null && group.isNotActive()) { |
| throw new GroupNotActiveException(group.getId()); |
| } |
| } |
| |
| @Override |
| public CommandProcessingResult undoWriteOff(Long loanId) { |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| if (!loan.isClosedWrittenOff()) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.status.not.written.off.update.not.allowed", |
| "Loan :" + loanId + " update not allowed as loan status is not written off", loanId); |
| } |
| LocalDate recalculateFrom = null; |
| LoanTransaction writeOffTransaction = loan.findWriteOffTransaction(); |
| if (writeOffTransaction == null) { |
| throw new PlatformServiceUnavailableException("error.msg.loan.write.off.transaction.not.found", |
| "Loan :" + loanId + " write off transaction not found", loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds, |
| existingReversedTransactionIds, scheduleGeneratorDTO); |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| if (writeOffTransaction != null) { |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); |
| } |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| return new CommandProcessingResultBuilder() // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .withEntityId(writeOffTransaction.getId()) // |
| .withEntityExternalId(writeOffTransaction.getExternalId()) // |
| .build(); |
| } |
| |
| private void validateMultiDisbursementData(final JsonCommand command, LocalDate expectedDisbursementDate, |
| boolean isDisallowExpectedDisbursements) { |
| final String json = command.json(); |
| final JsonElement element = this.fromApiJsonHelper.parse(json); |
| |
| final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); |
| final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan"); |
| final JsonArray disbursementDataArray = command.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); |
| |
| if (isDisallowExpectedDisbursements) { |
| if (!disbursementDataArray.isEmpty()) { |
| final String errorMessage = "For this loan product, disbursement details are not allowed"; |
| throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); |
| } |
| } else { |
| if (disbursementDataArray == null || disbursementDataArray.size() == 0) { |
| final String errorMessage = "For this loan product, disbursement details must be provided"; |
| throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); |
| } |
| } |
| |
| final BigDecimal principal = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("approvedLoanAmount", element); |
| |
| loanApplicationCommandFromApiJsonHelper.validateLoanMultiDisbursementDate(element, baseDataValidator, expectedDisbursementDate, |
| principal); |
| if (!dataValidationErrors.isEmpty()) { |
| throw new PlatformApiDataValidationException(dataValidationErrors); |
| } |
| } |
| |
| private void validateForAddAndDeleteTranche(final Loan loan) { |
| |
| BigDecimal totalDisbursedAmount = BigDecimal.ZERO; |
| Collection<LoanDisbursementDetails> loanDisburseDetails = loan.getDisbursementDetails(); |
| for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) { |
| if (disbursementDetails.actualDisbursementDate() != null) { |
| totalDisbursedAmount = totalDisbursedAmount.add(disbursementDetails.principal()); |
| } |
| } |
| if (totalDisbursedAmount.compareTo(loan.getApprovedPrincipal()) == 0) { |
| final String errorMessage = "loan.disbursement.cannot.be.a.edited"; |
| throw new LoanMultiDisbursementException(errorMessage); |
| } |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult addAndDeleteLoanDisburseDetails(Long loanId, JsonCommand command) { |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId); |
| } |
| final Map<String, Object> actualChanges = new LinkedHashMap<>(); |
| LocalDate expectedDisbursementDate = loan.getExpectedDisbursedOnLocalDate(); |
| if (!loan.loanProduct().isMultiDisburseLoan()) { |
| final String errorMessage = "loan.product.does.not.support.multiple.disbursals"; |
| throw new LoanMultiDisbursementException(errorMessage); |
| } |
| if (loan.isSubmittedAndPendingApproval() || loan.isClosed() || loan.isClosedWrittenOff() |
| || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) { |
| final String errorMessage = "cannot.modify.tranches.if.loan.is.pendingapproval.closed.overpaid.writtenoff"; |
| throw new LoanMultiDisbursementException(errorMessage); |
| } |
| validateMultiDisbursementData(command, expectedDisbursementDate, loan.loanProduct().isDisallowExpectedDisbursements()); |
| |
| this.validateForAddAndDeleteTranche(loan); |
| |
| loan.updateDisbursementDetails(command, actualChanges); |
| |
| if (loan.loanProduct().isDisallowExpectedDisbursements()) { |
| if (!loan.getDisbursementDetails().isEmpty()) { |
| final String errorMessage = "For this loan product, disbursement details are not allowed"; |
| throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); |
| } |
| } else { |
| if (loan.getDisbursementDetails().isEmpty()) { |
| final String errorMessage = "For this loan product, disbursement details must be provided"; |
| throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); |
| } |
| } |
| |
| if (loan.getDisbursementDetails().size() > loan.loanProduct().maxTrancheCount()) { |
| final String errorMessage = "Number of tranche shouldn't be greater than " + loan.loanProduct().maxTrancheCount(); |
| throw new ExceedingTrancheCountException(LoanApiConstants.disbursementDataParameterName, errorMessage, |
| loan.loanProduct().maxTrancheCount(), loan.getDisbursementDetails().size()); |
| } |
| LoanDisbursementDetails updateDetails = null; |
| CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, updateDetails); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan)); |
| return result; |
| |
| } |
| |
| private CommandProcessingResult processLoanDisbursementDetail(Loan loan, Long loanId, JsonCommand command, |
| LoanDisbursementDetails loanDisbursementDetails) { |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| LocalDate recalculateFrom = null; |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| ChangedTransactionDetail changedTransactionDetail = null; |
| |
| if (command.entityId() != null) { |
| changedTransactionDetail = loan.updateDisbursementDateAndAmountForTranche(loanDisbursementDetails, command, changes, |
| scheduleGeneratorDTO); |
| } else { |
| loan.repaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule()); |
| |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); |
| } else { |
| loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); |
| loan.processPostDisbursementTransactions(); |
| } |
| } |
| |
| if (command.entityId() != null && changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| createLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| } |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| this.loanAccountDomainService.recalculateAccruals(loan); |
| this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); |
| return new CommandProcessingResultBuilder() // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes).build(); |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult updateDisbursementDateAndAmountForTranche(final Long loanId, final Long disbursementId, |
| final JsonCommand command) { |
| |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Update Loan: " + loanId + " disbursement details is not allowed. Loan Account is Charged-off", loanId); |
| } |
| LoanDisbursementDetails loanDisbursementDetails = loan.fetchLoanDisbursementsById(disbursementId); |
| this.loanEventApiJsonValidator.validateUpdateDisbursementDateAndAmount(command.json(), loanDisbursementDetails); |
| |
| CommandProcessingResult result = processLoanDisbursementDetail(loan, loanId, command, loanDisbursementDetails); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUpdateDisbursementDataBusinessEvent(loan)); |
| return result; |
| |
| } |
| |
| @Transactional |
| @Override |
| @Retry(name = "recalculateInterest", fallbackMethod = "fallbackRecalculateInterest") |
| public void recalculateInterest(final long loanId) { |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| LocalDate recalculateFrom = loan.fetchInterestRecalculateFromDate(); |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan)); |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| |
| ChangedTransactionDetail changedTransactionDetail = loan.recalculateScheduleFromLastTransaction(generatorDTO, |
| existingTransactionIds, existingReversedTransactionIds); |
| |
| if (changedTransactionDetail != null) { |
| for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { |
| loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); |
| accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); |
| } |
| // Trigger transaction replayed event |
| replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); |
| } |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| loanAccountDomainService.recalculateAccruals(loan); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan)); |
| } |
| |
| @Override |
| public CommandProcessingResult recoverFromGuarantor(final Long loanId) { |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| this.guarantorDomainService.transferFundsFromGuarantor(loan); |
| return new CommandProcessingResultBuilder().withLoanId(loanId).withEntityId(loanId).withEntityExternalId(loan.getExternalId()) |
| .build(); |
| } |
| |
| @SuppressWarnings("unused") |
| public void fallbackRecalculateInterest(Throwable t) { |
| // NOTE: allow caller to catch the exceptions |
| // NOTE: wrap throwable only if really necessary |
| throw errorHandler.getMappable(t, null, null, "loan.recalculateinterest"); |
| } |
| |
| @Override |
| public void updateOriginalSchedule(Loan loan) { |
| if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { |
| final LocalDate recalculateFrom = null; |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); |
| createLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| } |
| } |
| |
| private void createLoanScheduleArchive(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) { |
| createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); |
| |
| } |
| |
| private void regenerateScheduleOnDisbursement(final JsonCommand command, final Loan loan, final boolean recalculateSchedule, |
| final ScheduleGeneratorDTO scheduleGeneratorDTO, final LocalDate nextPossibleRepaymentDate, |
| final LocalDate rescheduledRepaymentDate) { |
| final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); |
| BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.emiAmountParameterName); |
| loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount, |
| nextPossibleRepaymentDate, rescheduledRepaymentDate); |
| } |
| |
| private List<LoanRepaymentScheduleInstallment> retrieveRepaymentScheduleFromModel(LoanScheduleModel model) { |
| final List<LoanRepaymentScheduleInstallment> installments = new ArrayList<>(); |
| for (final LoanScheduleModelPeriod scheduledLoanInstallment : model.getPeriods()) { |
| if (scheduledLoanInstallment.isRepaymentPeriod() || scheduledLoanInstallment.isDownPaymentPeriod()) { |
| final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(null, |
| scheduledLoanInstallment.periodNumber(), scheduledLoanInstallment.periodFromDate(), |
| scheduledLoanInstallment.periodDueDate(), scheduledLoanInstallment.principalDue(), |
| scheduledLoanInstallment.interestDue(), scheduledLoanInstallment.feeChargesDue(), |
| scheduledLoanInstallment.penaltyChargesDue(), scheduledLoanInstallment.isRecalculatedInterestComponent(), |
| scheduledLoanInstallment.getLoanCompoundingDetails()); |
| installments.add(installment); |
| } |
| } |
| return installments; |
| } |
| |
| @Override |
| public CommandProcessingResult creditBalanceRefund(Long loanId, JsonCommand command) { |
| this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json()); |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| final String noteText = command.stringValueOfParameterNamedAllowingNull("note"); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| } |
| if (!externalId.isEmpty()) { |
| changes.put(LoanApiConstants.externalIdParameterName, externalId); |
| } |
| changes.put("paymentTypeId", command.longValueOfParameterNamed(LoanApiConstants.PAYMENT_TYPE_PARAMNAME)); |
| |
| PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createPaymentDetail(command, changes); |
| if (paymentDetail != null) { |
| paymentDetail = this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail); |
| } |
| |
| final LoanTransaction loanTransaction = this.loanAccountDomainService.creditBalanceRefund(loan, transactionDate, transactionAmount, |
| noteText, externalId, paymentDetail); |
| |
| return new CommandProcessingResultBuilder() // |
| .withEntityId(loanTransaction.getId()) // |
| .withEntityExternalId(loanTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withCommandId(command.commandId()) // |
| .with(changes) // |
| .build(); |
| |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult markLoanAsFraud(Long loanId, JsonCommand command) { |
| this.loanEventApiJsonValidator.validateMarkAsFraudLoan(command.json()); |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| |
| if (loan.isApproved() || loan.isSubmittedAndPendingApproval()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.mark.as.fraud.not.allowed", |
| "Loan Id: " + loanId + " mark as fraud is not allowed as loan status is not active", loan.getStatus().getCode()); |
| } |
| final boolean fraud = command.booleanPrimitiveValueOfParameterNamed(LoanApiConstants.FRAUD_ATTRIBUTE_NAME); |
| if (loan.isFraud() != fraud) { |
| loan.markAsFraud(fraud); |
| this.loanRepository.save(loan); |
| changes.put(LoanApiConstants.FRAUD_ATTRIBUTE_NAME, fraud); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loan.getId()) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult makeLoanRefund(Long loanId, JsonCommand command) { |
| |
| this.loanEventApiJsonValidator.validateNewRefundTransaction(command.json()); |
| |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); |
| ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| |
| // checkRefundDateIsAfterAtLeastOneRepayment(loanId, transactionDate); |
| |
| final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); |
| checkIfLoanIsPaidInAdvance(loanId, transactionAmount); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); |
| changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); |
| changes.put("locale", command.locale()); |
| changes.put("dateFormat", command.dateFormat()); |
| changes.put(LoanApiConstants.externalIdParameterName, externalId); |
| |
| final String noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put("note", noteText); |
| } |
| |
| final PaymentDetail paymentDetail = null; |
| |
| final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); |
| |
| LoanTransaction loanTransaction = this.loanAccountDomainService.makeRefundForActiveLoan(loanId, commandProcessingResultBuilder, |
| transactionDate, transactionAmount, paymentDetail, noteText, externalId); |
| |
| return commandProcessingResultBuilder // |
| .withCommandId(command.commandId()) // |
| .withLoanId(loanId) // |
| .withEntityId(loanTransaction.getId()) // |
| .withEntityExternalId(loanTransaction.getExternalId()) // |
| .with(changes) // |
| .build(); |
| |
| } |
| |
| private void checkIfLoanIsPaidInAdvance(final Long loanId, final BigDecimal transactionAmount) { |
| BigDecimal overpaid = this.loanReadPlatformService.retrieveTotalPaidInAdvance(loanId).getPaidInAdvance(); |
| |
| if (overpaid == null || overpaid.compareTo(BigDecimal.ZERO) == 0 || transactionAmount.floatValue() > overpaid.floatValue()) { |
| if (overpaid == null) { |
| overpaid = BigDecimal.ZERO; |
| } |
| throw new InvalidPaidInAdvanceAmountException(overpaid.toPlainString()); |
| } |
| } |
| |
| private AppUser getAppUserIfPresent() { |
| AppUser user = null; |
| if (this.context != null) { |
| user = this.context.getAuthenticatedUserIfPresent(); |
| } |
| return user; |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult undoLastLoanDisbursal(Long loanId, JsonCommand command) { |
| |
| Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final LocalDate recalculateFromDate = loan.getLastRepaymentDate(); |
| validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(loan); |
| checkClientOrGroupActive(loan); |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.charged.off", |
| "Undo Loan: " + loanId + " last disbursement is not allowed. Loan Account is Charged-off", loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoLastDisbursalBusinessEvent(loan)); |
| |
| final MonetaryCurrency currency = loan.getCurrency(); |
| final List<Long> existingTransactionIds = new ArrayList<>(); |
| final List<Long> existingReversedTransactionIds = new ArrayList<>(); |
| |
| ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFromDate); |
| |
| final Map<String, Object> changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, |
| existingReversedTransactionIds, loan); |
| if (!changes.isEmpty()) { |
| loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| String noteText; |
| if (command.hasParameter("note")) { |
| noteText = command.stringValueOfParameterNamed("note"); |
| if (StringUtils.isNotBlank(noteText)) { |
| final Note note = Note.loanNote(loan, noteText); |
| this.noteRepository.save(note); |
| } |
| } |
| boolean isAccountTransfer = false; |
| final Map<String, Object> accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, |
| existingReversedTransactionIds, isAccountTransfer); |
| journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); |
| loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoLastDisbursalBusinessEvent(loan)); |
| } |
| |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(loan.getId()) // |
| .withEntityExternalId(loan.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult forecloseLoan(final Long loanId, final JsonCommand command) { |
| final String json = command.json(); |
| final JsonElement element = fromApiJsonHelper.parse(json); |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.transactionDateParamName, element); |
| final ExternalId externalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| this.loanEventApiJsonValidator.validateLoanForeclosure(command.json()); |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| // Got changed to match with the rest of the APIs |
| changes.put("dateFormat", command.dateFormat()); |
| changes.put("transactionDate", command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName)); |
| changes.put("externalId", externalId); |
| |
| String noteText = this.fromApiJsonHelper.extractStringNamed(LoanApiConstants.noteParamName, element); |
| LoanRescheduleRequest loanRescheduleRequest = null; |
| for (LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { |
| if (!DateUtils.isAfter(loanDisbursementDetails.expectedDisbursementDateAsLocalDate(), transactionDate) |
| && loanDisbursementDetails.actualDisbursementDate() == null) { |
| final String defaultUserMessage = "The loan with undisbursed tranche before foreclosure cannot be foreclosed."; |
| throw new LoanForeclosureException("loan.with.undisbursed.tranche.before.foreclosure.cannot.be.foreclosured", |
| defaultUserMessage, transactionDate); |
| } |
| } |
| this.loanScheduleHistoryWritePlatformService.createAndSaveLoanScheduleArchive(loan.getRepaymentScheduleInstallments(), loan, |
| loanRescheduleRequest); |
| |
| LoanTransaction foreclosureTransaction = this.loanAccountDomainService.foreCloseLoan(loan, transactionDate, noteText, externalId, |
| changes); |
| |
| final CommandProcessingResultBuilder commandProcessingResultBuilder = new CommandProcessingResultBuilder(); |
| return commandProcessingResultBuilder // |
| .withLoanId(loanId) // |
| .withEntityId(foreclosureTransaction.getId()) // |
| .withEntityExternalId(foreclosureTransaction.getExternalId()) // |
| .with(changes) // |
| .build(); |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult chargeOff(JsonCommand command) { |
| |
| loanEventApiJsonValidator.validateChargeOffTransaction(command.json()); |
| |
| final Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put(LoanApiConstants.transactionDateParamName, |
| command.stringValueOfParameterNamed(LoanApiConstants.transactionDateParamName)); |
| changes.put(LoanApiConstants.localeParameterName, command.locale()); |
| changes.put(LoanApiConstants.dateFormatParameterName, command.dateFormat()); |
| final LocalDate transactionDate = command.localDateValueOfParameterNamed(LoanApiConstants.transactionDateParamName); |
| final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); |
| final AppUser currentUser = getAppUserIfPresent(); |
| |
| Loan loan = loanAssembler.assembleFrom(command.getLoanId()); |
| final Long loanId = loan.getId(); |
| if (!loan.isOpen()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.active", |
| "Loan: " + loanId + " Charge-off is not allowed. Loan Account is not Active", loanId); |
| } |
| if (loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.already.charged.off", |
| "Loan: " + loanId + " is already charged-off", loanId); |
| } |
| if (DateUtils.isBefore(transactionDate, loan.getLastUserTransactionDate())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.is.before.than.the.last.user.transaction", |
| "Loan: " + loanId + " charge-off cannot be executed. User transaction was found after the charge-off transaction date!", |
| loanId); |
| } |
| if (DateUtils.isDateInTheFuture(transactionDate)) { |
| final String errorMessage = "The transaction date cannot be in the future."; |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.transaction.cannot.be.a.future.date", errorMessage, |
| transactionDate); |
| } |
| |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanChargeOffPreBusinessEvent(loan)); |
| |
| if (command.hasParameter(LoanApiConstants.chargeOffReasonIdParamName)) { |
| Long chargeOffReasonId = command.longValueOfParameterNamed(LoanApiConstants.chargeOffReasonIdParamName); |
| CodeValue chargeOffReason = this.codeValueRepository |
| .findOneByCodeNameAndIdWithNotFoundDetection(LoanApiConstants.CHARGE_OFF_REASONS, chargeOffReasonId); |
| changes.put(LoanApiConstants.chargeOffReasonIdParamName, chargeOffReasonId); |
| loan.markAsChargedOff(transactionDate, currentUser, chargeOffReason); |
| } else { |
| loan.markAsChargedOff(transactionDate, currentUser, null); |
| } |
| |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| |
| LoanTransaction chargeOffTransaction = LoanTransaction.chargeOff(loan, transactionDate, txnExternalId); |
| loanTransactionRepository.saveAndFlush(chargeOffTransaction); |
| loan.addLoanTransaction(chargeOffTransaction); |
| saveAndFlushLoanWithDataIntegrityViolationChecks(loan); |
| |
| String noteText = command.stringValueOfParameterNamed(LoanApiConstants.noteParameterName); |
| if (StringUtils.isNotBlank(noteText)) { |
| changes.put(LoanApiConstants.noteParameterName, noteText); |
| final Note note = Note.loanTransactionNote(loan, chargeOffTransaction, noteText); |
| this.noteRepository.save(note); |
| } |
| |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanChargeOffPostBusinessEvent(chargeOffTransaction)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(chargeOffTransaction.getId()) // |
| .withEntityExternalId(chargeOffTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(command.getLoanId()) // |
| .with(changes).build(); |
| } |
| |
| @Override |
| @Transactional |
| public CommandProcessingResult undoChargeOff(JsonCommand command) { |
| this.loanEventApiJsonValidator.validateUndoChargeOff(command.json()); |
| final Long loanId = command.getLoanId(); |
| final Loan loan = this.loanAssembler.assembleFrom(loanId); |
| final List<Long> existingTransactionIds = loan.findExistingTransactionIds(); |
| final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); |
| checkClientOrGroupActive(loan); |
| if (!loan.isOpen()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.active", |
| "Loan: " + loanId + " Undo Charge-off is not allowed. Loan Account is not Active", loanId); |
| } |
| if (!loan.isChargedOff()) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.is.not.charged.off", "Loan: " + loanId + " is not charged-off", |
| loanId); |
| } |
| LoanTransaction chargedOffTransaction = loan.findChargedOffTransaction(); |
| if (chargedOffTransaction == null) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.transaction.not.found", |
| "Loan: " + loanId + " charge-off transaction was not found", loanId); |
| } |
| if (!chargedOffTransaction.equals(loan.getLastUserTransaction())) { |
| throw new GeneralPlatformDomainRuleException("error.msg.loan.charge.off.is.not.the.last.user.transaction", |
| "Loan: " + loanId + " charge-off cannot be undone. User transaction was found after charge-off!", loanId); |
| } |
| businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction)); |
| |
| // check if reversalExternalId is provided |
| final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); |
| final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId); |
| |
| chargedOffTransaction.reverse(reversalTxnExternalId); |
| chargedOffTransaction.manuallyAdjustedOrReversed(); |
| |
| loan.liftChargeOff(); |
| loanTransactionRepository.saveAndFlush(chargedOffTransaction); |
| saveLoanWithDataIntegrityViolationChecks(loan); |
| postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoChargeOffBusinessEvent(chargedOffTransaction)); |
| return new CommandProcessingResultBuilder() // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(loanId) // |
| .withEntityId(chargedOffTransaction.getId()) // |
| .withEntityExternalId(chargedOffTransaction.getExternalId()) // |
| .build(); |
| } |
| |
| private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { |
| if (!loan.isMultiDisburmentLoan()) { |
| final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; |
| throw new LoanMultiDisbursementException(errorMessage); |
| } |
| Integer trancheDisbursedCount = 0; |
| for (LoanDisbursementDetails disbursementDetails : loan.getDisbursementDetails()) { |
| if (disbursementDetails.actualDisbursementDate() != null) { |
| trancheDisbursedCount++; |
| } |
| } |
| if (trancheDisbursedCount <= 1) { |
| final String errorMessage = "tranches.should.be.disbursed.more.than.one.to.undo.last.disbursal"; |
| throw new LoanMultiDisbursementException(errorMessage); |
| } |
| } |
| |
| private void syncExpectedDateWithActualDisbursementDate(final Loan loan, LocalDate actualDisbursementDate) { |
| if (!loan.getExpectedDisbursedOnLocalDate().equals(actualDisbursementDate)) { |
| throw new DateMismatchException(actualDisbursementDate, loan.getExpectedDisbursedOnLocalDate()); |
| } |
| } |
| |
| private void validateTransactionsForTransfer(final Loan loan, final LocalDate transferDate) { |
| for (LoanTransaction transaction : loan.getLoanTransactions()) { |
| if ((DateUtils.isEqual(transferDate, transaction.getTransactionDate()) |
| && DateUtils.isEqual(transferDate, transaction.getSubmittedOnDate())) |
| || DateUtils.isBefore(transferDate, transaction.getTransactionDate())) { |
| throw new GeneralPlatformDomainRuleException(TransferApiConstants.transferClientLoanException, |
| TransferApiConstants.transferClientLoanExceptionMessage, transaction.getCreatedDateTime().toLocalDate(), |
| transferDate); |
| } |
| } |
| } |
| } |