Java tutorial
/** * 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 com.gst.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import com.gst.accounting.journalentry.service.JournalEntryWritePlatformService; import com.gst.infrastructure.configuration.domain.ConfigurationDomainService; import com.gst.infrastructure.core.data.ApiParameterError; import com.gst.infrastructure.core.data.CommandProcessingResultBuilder; import com.gst.infrastructure.core.data.DataValidatorBuilder; import com.gst.infrastructure.core.exception.GeneralPlatformDomainRuleException; import com.gst.infrastructure.core.exception.PlatformApiDataValidationException; import com.gst.infrastructure.core.service.DateUtils; import com.gst.infrastructure.security.service.PlatformSecurityContext; import com.gst.organisation.holiday.domain.Holiday; import com.gst.organisation.holiday.domain.HolidayRepository; import com.gst.organisation.holiday.domain.HolidayStatusType; import com.gst.organisation.monetary.data.CurrencyData; import com.gst.organisation.monetary.domain.ApplicationCurrency; import com.gst.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import com.gst.organisation.monetary.domain.MonetaryCurrency; import com.gst.organisation.monetary.domain.Money; import com.gst.organisation.workingdays.domain.WorkingDays; import com.gst.organisation.workingdays.domain.WorkingDaysRepositoryWrapper; import com.gst.portfolio.account.domain.AccountTransferRepository; import com.gst.portfolio.account.domain.AccountTransferStandingInstruction; import com.gst.portfolio.account.domain.AccountTransferTransaction; import com.gst.portfolio.account.domain.StandingInstructionRepository; import com.gst.portfolio.account.domain.StandingInstructionStatus; import com.gst.portfolio.client.domain.Client; import com.gst.portfolio.client.exception.ClientNotActiveException; import com.gst.portfolio.common.BusinessEventNotificationConstants.BUSINESS_ENTITY; import com.gst.portfolio.common.BusinessEventNotificationConstants.BUSINESS_EVENTS; import com.gst.portfolio.common.domain.PeriodFrequencyType; import com.gst.portfolio.common.service.BusinessEventNotifierService; import com.gst.portfolio.group.domain.Group; import com.gst.portfolio.group.exception.GroupNotActiveException; import com.gst.portfolio.loanaccount.data.HolidayDetailDTO; import com.gst.portfolio.loanaccount.data.LoanScheduleAccrualData; import com.gst.portfolio.loanaccount.data.ScheduleGeneratorDTO; import com.gst.portfolio.loanaccount.service.LoanAccrualPlatformService; import com.gst.portfolio.loanaccount.service.LoanAssembler; import com.gst.portfolio.loanaccount.service.LoanUtilService; import com.gst.portfolio.note.domain.Note; import com.gst.portfolio.note.domain.NoteRepository; import com.gst.portfolio.paymentdetail.domain.PaymentDetail; import com.gst.useradministration.domain.AppUser; import org.joda.time.LocalDate; import org.joda.time.LocalDateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final LoanAssembler loanAccountAssembler; private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanTransactionRepository loanTransactionRepository; private final ConfigurationDomainService configurationDomainService; private final HolidayRepository holidayRepository; private final WorkingDaysRepositoryWrapper workingDaysRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final NoteRepository noteRepository; private final AccountTransferRepository accountTransferRepository; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository; private final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository; private final LoanAccrualPlatformService loanAccrualPlatformService; private final PlatformSecurityContext context; private final BusinessEventNotifierService businessEventNotifierService; private final LoanUtilService loanUtilService; private final StandingInstructionRepository standingInstructionRepository; @Autowired public LoanAccountDomainServiceJpa(final LoanAssembler loanAccountAssembler, final LoanRepositoryWrapper loanRepositoryWrapper, final LoanTransactionRepository loanTransactionRepository, final NoteRepository noteRepository, final ConfigurationDomainService configurationDomainService, final HolidayRepository holidayRepository, final WorkingDaysRepositoryWrapper workingDaysRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, final JournalEntryWritePlatformService journalEntryWritePlatformService, final AccountTransferRepository accountTransferRepository, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository, final LoanRepaymentScheduleInstallmentRepository repaymentScheduleInstallmentRepository, final LoanAccrualPlatformService loanAccrualPlatformService, final PlatformSecurityContext context, final BusinessEventNotifierService businessEventNotifierService, final LoanUtilService loanUtilService, final StandingInstructionRepository standingInstructionRepository) { this.loanAccountAssembler = loanAccountAssembler; this.loanRepositoryWrapper = loanRepositoryWrapper; this.loanTransactionRepository = loanTransactionRepository; this.noteRepository = noteRepository; this.configurationDomainService = configurationDomainService; this.holidayRepository = holidayRepository; this.workingDaysRepository = workingDaysRepository; this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper; this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.accountTransferRepository = accountTransferRepository; this.applicationCurrencyRepository = applicationCurrencyRepository; this.repaymentScheduleInstallmentRepository = repaymentScheduleInstallmentRepository; this.loanAccrualPlatformService = loanAccrualPlatformService; this.context = context; this.businessEventNotifierService = businessEventNotifierService; this.loanUtilService = loanUtilService; this.standingInstructionRepository = standingInstructionRepository; } @Transactional @Override public LoanTransaction makeRepayment(final Loan loan, final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone) { return makeRepayment(loan, builderResult, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false); } @Transactional @Override public LoanTransaction makeRepayment(final Loan loan, final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isRecoveryRepayment, boolean isAccountTransfer, HolidayDetailDTO holidayDetailDto, Boolean isHolidayValidationDone, final boolean isLoanToLoanTransfer) { AppUser currentUser = getAppUserIfPresent(); checkClientOrGroupActive(loan); this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); // TODO: Is it required to validate transaction date with meeting dates // if repayments is synced with meeting? /* * if(loan.isSyncDisbursementWithMeeting()){ // validate actual * disbursement date against meeting date CalendarInstance * calendarInstance = * this.calendarInstanceRepository.findCalendarInstaneByLoanId * (loan.getId(), CalendarEntityType.LOANS.getValue()); * this.loanEventApiJsonValidator * .validateRepaymentDateWithMeetingDate(transactionDate, * calendarInstance); } */ final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); final Money repaymentAmount = Money.of(loan.getCurrency(), transactionAmount); LoanTransaction newRepaymentTransaction = null; final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant(); if (isRecoveryRepayment) { newRepaymentTransaction = LoanTransaction.recoveryRepayment(loan.getOffice(), repaymentAmount, paymentDetail, transactionDate, txnExternalId, currentDateTime, currentUser); } else { newRepaymentTransaction = LoanTransaction.repayment(loan.getOffice(), repaymentAmount, paymentDetail, transactionDate, txnExternalId, currentDateTime, currentUser); } LocalDate recalculateFrom = null; if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { recalculateFrom = transactionDate; } final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDto); final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, scheduleGeneratorDTO, currentUser, isHolidayValidationDone); saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); /*** * 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 payment is made before the latest payment * recorded against the loan) ***/ saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (changedTransactionDetail != null) { for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings() .entrySet()) { saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); // update loan with references to the newly created transactions loan.addLoanTransaction(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } } if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newRepaymentTransaction, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); recalculateAccruals(loan); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_MAKE_REPAYMENT, constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRepaymentTransaction)); // disable all active standing orders linked to this loan if status changes to closed disableStandingInstructionsLinkedToClosedLoan(loan); builderResult.withEntityId(newRepaymentTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()); // return newRepaymentTransaction; } private void saveLoanTransactionWithDataIntegrityViolationChecks(LoanTransaction newRepaymentTransaction) { try { this.loanTransactionRepository.save(newRepaymentTransaction); } catch (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("externalId").value(newRepaymentTransaction.getExternalId()) .failWithCode("value.must.be.unique"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } } private void saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { try { List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); for (LoanRepaymentScheduleInstallment installment : installments) { if (installment.getId() == null) { this.repaymentScheduleInstallmentRepository.save(installment); } } this.loanRepositoryWrapper.saveAndFlush(loan); } catch (final DataIntegrityViolationException e) { final Throwable realCause = e.getCause(); final List<ApiParameterError> dataValidationErrors = new ArrayList<>(); final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) .resource("loan.transaction"); if (realCause.getMessage().toLowerCase().contains("external_id_unique")) { baseDataValidator.reset().parameter("externalId").failWithCode("value.must.be.unique"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } } @Override public void saveLoanWithDataIntegrityViolationChecks(final Loan loan) { try { List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); for (LoanRepaymentScheduleInstallment installment : installments) { if (installment.getId() == null) { this.repaymentScheduleInstallmentRepository.save(installment); } } this.loanRepositoryWrapper.save(loan); } catch (final 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("externalId").failWithCode("value.must.be.unique"); } if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", dataValidationErrors); } } } @Override @Transactional public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final Integer transactionType, Integer installmentNumber) { AppUser currentUser = getAppUserIfPresent(); boolean isAccountTransfer = true; checkClientOrGroupActive(loan); this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); final Money paymentAmout = Money.of(loan.getCurrency(), transactionAmount); final LoanTransactionType loanTransactionType = LoanTransactionType.fromInt(transactionType); final LoanTransaction newPaymentTransaction = LoanTransaction.loanPayment(null, loan.getOffice(), paymentAmout, paymentDetail, transactionDate, txnExternalId, loanTransactionType, DateUtils.getLocalDateTimeOfTenant(), currentUser); if (loanTransactionType.isRepaymentAtDisbursement()) { loan.handlePayDisbursementTransaction(chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds); } else { final boolean allowTransactionsOnHoliday = this.configurationDomainService .allowTransactionsOnHolidayEnabled(); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate( loan.getOfficeId(), transactionDate.toDate(), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService .allowTransactionsOnNonWorkingDayEnabled(); final boolean isHolidayEnabled = this.configurationDomainService .isRescheduleRepaymentsOnHolidaysEnabled(); HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, holidayDetailDTO, newPaymentTransaction, installmentNumber); } saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction); saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newPaymentTransaction, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); recalculateAccruals(loan); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_CHARGE_PAYMENT, constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newPaymentTransaction)); return newPaymentTransaction; } private void postJournalEntries(final Loan loanAccount, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, boolean isAccountTransfer) { postJournalEntries(loanAccount, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, false); } private void postJournalEntries(final Loan loanAccount, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, boolean isAccountTransfer, boolean isLoanToLoanTransfer) { final MonetaryCurrency currency = loanAccount.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepositoryWrapper .findOneWithNotFoundDetection(currency); final Map<String, Object> accountingBridgeData = loanAccount.deriveAccountingBridgeData( applicationCurrency.toData(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); accountingBridgeData.put("isLoanToLoanTransfer", isLoanToLoanTransfer); this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { final List<LoanStatus> allowedLoanStatuses = Arrays.asList(LoanStatus.values()); return new DefaultLoanLifecycleStateMachine(allowedLoanStatuses); } private void checkClientOrGroupActive(final Loan loan) { final Client client = loan.client(); if (client != null) { if (client.isNotActive()) { throw new ClientNotActiveException(client.getId()); } } final Group group = loan.group(); if (group != null) { if (group.isNotActive()) { throw new GroupNotActiveException(group.getId()); } } } @Override public LoanTransaction makeRefund(final Long accountId, final CommandProcessingResultBuilder builderResult, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId) { AppUser currentUser = getAppUserIfPresent(); boolean isAccountTransfer = true; final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND, constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount); final LoanTransaction newRefundTransaction = LoanTransaction.refund(loan.getOffice(), refundAmount, paymentDetail, transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser); final boolean allowTransactionsOnHoliday = this.configurationDomainService .allowTransactionsOnHolidayEnabled(); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate.toDate(), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService .allowTransactionsOnNonWorkingDayEnabled(); loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction); this.loanRepositoryWrapper.save(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newRefundTransaction, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND, constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction)); builderResult.withEntityId(newRefundTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()); // return newRefundTransaction; } @Transactional @Override public LoanTransaction makeDisburseTransaction(final Long loanId, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId) { return makeDisburseTransaction(loanId, transactionDate, transactionAmount, paymentDetail, noteText, txnExternalId, false); } @Transactional @Override public LoanTransaction makeDisburseTransaction(final Long loanId, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, final boolean isLoanToLoanTransfer) { AppUser currentUser = getAppUserIfPresent(); final Loan loan = this.loanAccountAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); boolean isAccountTransfer = true; final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); final Money amount = Money.of(loan.getCurrency(), transactionAmount); LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail, transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser); disbursementTransaction.updateLoan(loan); loan.addLoanTransaction(disbursementTransaction); saveLoanTransactionWithDataIntegrityViolationChecks(disbursementTransaction); saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, disbursementTransaction, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); return disbursementTransaction; } @Override public void reverseTransfer(final LoanTransaction loanTransaction) { loanTransaction.reverse(); saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); } /* * (non-Javadoc) * * @see * com.gst.portfolio.loanaccount.domain.LoanAccountDomainService * #recalculateAccruals(com.gst.portfolio.loanaccount.domain.Loan) */ @Override public void recalculateAccruals(Loan loan) { boolean isInterestCalcualtionHappened = loan.repaymentScheduleDetail().isInterestRecalculationEnabled(); recalculateAccruals(loan, isInterestCalcualtionHappened); } @Override public void recalculateAccruals(Loan loan, boolean isInterestCalcualtionHappened) { LocalDate accruedTill = loan.getAccruedTill(); if (!loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() || !isInterestCalcualtionHappened || accruedTill == null || loan.isNpa() || !loan.status().isActive()) { return; } boolean isOrganisationDateEnabled = this.configurationDomainService.isOrganisationstartDateEnabled(); Date organisationStartDate = new Date(); if (isOrganisationDateEnabled) { organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate(); } Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas = new ArrayList<>(); List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); Long loanId = loan.getId(); Long officeId = loan.getOfficeId(); LocalDate accrualStartDate = null; PeriodFrequencyType repaymentFrequency = loan.repaymentScheduleDetail().getRepaymentPeriodFrequencyType(); Integer repayEvery = loan.repaymentScheduleDetail().getRepayEvery(); LocalDate interestCalculatedFrom = loan.getInterestChargedFromDate(); Long loanProductId = loan.productId(); MonetaryCurrency currency = loan.getCurrency(); ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository .findOneWithNotFoundDetection(currency); CurrencyData currencyData = applicationCurrency.toData(); Set<LoanCharge> loanCharges = loan.charges(); for (LoanRepaymentScheduleInstallment installment : installments) { if (installment.getDueDate().isAfter(loan.getMaturityDate())) { accruedTill = DateUtils.getLocalDateOfTenant(); } if (!isOrganisationDateEnabled || new LocalDate(organisationStartDate).isBefore(installment.getDueDate())) { generateLoanScheduleAccrualData(accruedTill, loanScheduleAccrualDatas, loanId, officeId, accrualStartDate, repaymentFrequency, repayEvery, interestCalculatedFrom, loanProductId, currency, currencyData, loanCharges, installment); } } if (!loanScheduleAccrualDatas.isEmpty()) { String error = this.loanAccrualPlatformService.addPeriodicAccruals(accruedTill, loanScheduleAccrualDatas); if (error.length() > 0) { String globalisationMessageCode = "error.msg.accrual.exception"; throw new GeneralPlatformDomainRuleException(globalisationMessageCode, error, error); } } } private void generateLoanScheduleAccrualData(final LocalDate accruedTill, final Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas, final Long loanId, Long officeId, final LocalDate accrualStartDate, final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate interestCalculatedFrom, final Long loanProductId, final MonetaryCurrency currency, final CurrencyData currencyData, final Set<LoanCharge> loanCharges, final LoanRepaymentScheduleInstallment installment) { if (!accruedTill.isBefore(installment.getDueDate()) || (accruedTill.isAfter(installment.getFromDate()) && !accruedTill.isAfter(installment.getDueDate()))) { BigDecimal dueDateFeeIncome = BigDecimal.ZERO; BigDecimal dueDatePenaltyIncome = BigDecimal.ZERO; LocalDate chargesTillDate = installment.getDueDate(); if (!accruedTill.isAfter(installment.getDueDate())) { chargesTillDate = accruedTill; } for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(installment.getFromDate(), chargesTillDate)) { if (loanCharge.isFeeCharge()) { dueDateFeeIncome = dueDateFeeIncome.add(loanCharge.amount()); } else if (loanCharge.isPenaltyCharge()) { dueDatePenaltyIncome = dueDatePenaltyIncome.add(loanCharge.amount()); } } } LoanScheduleAccrualData accrualData = new LoanScheduleAccrualData(loanId, officeId, installment.getInstallmentNumber(), accrualStartDate, repaymentFrequency, repayEvery, installment.getDueDate(), installment.getFromDate(), installment.getId(), loanProductId, installment.getInterestCharged(currency).getAmount(), installment.getFeeChargesCharged(currency).getAmount(), installment.getPenaltyChargesCharged(currency).getAmount(), installment.getInterestAccrued(currency).getAmount(), installment.getFeeAccrued(currency).getAmount(), installment.getPenaltyAccrued(currency).getAmount(), currencyData, interestCalculatedFrom, installment.getInterestWaived(currency).getAmount()); loanScheduleAccrualDatas.add(accrualData); } } private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository .findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) { transferTransaction.updateToLoanTransaction(newLoanTransaction); this.accountTransferRepository.save(transferTransaction); } } private AppUser getAppUserIfPresent() { AppUser user = null; if (this.context != null) { user = this.context.getAuthenticatedUserIfPresent(); } return user; } @Override public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, String txnExternalId) { final Loan loan = this.loanAccountAssembler.assembleFrom(accountId); checkClientOrGroupActive(loan); this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_REFUND, constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); AppUser currentUser = getAppUserIfPresent(); final Money refundAmount = Money.of(loan.getCurrency(), transactionAmount); final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail, transactionDate, txnExternalId, DateUtils.getLocalDateTimeOfTenant(), currentUser); final boolean allowTransactionsOnHoliday = this.configurationDomainService .allowTransactionsOnHolidayEnabled(); final List<Holiday> holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate.toDate(), HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService .allowTransactionsOnNonWorkingDayEnabled(); loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); this.loanTransactionRepository.save(newRefundTransaction); this.loanRepositoryWrapper.save(loan); if (StringUtils.isNotBlank(noteText)) { final Note note = Note.loanTransactionNote(loan, newRefundTransaction, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); recalculateAccruals(loan); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_REFUND, constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, newRefundTransaction)); builderResult.withEntityId(newRefundTransaction.getId()) // .withOfficeId(loan.getOfficeId()) // .withClientId(loan.getClientId()) // .withGroupId(loan.getGroupId()); // return newRefundTransaction; } @Override public Map<String, Object> foreCloseLoan(final Loan loan, final LocalDate foreClosureDate, final String noteText) { this.businessEventNotifierService.notifyBusinessEventToBeExecuted(BUSINESS_EVENTS.LOAN_FORECLOSURE, constructEntityMap(BUSINESS_ENTITY.LOAN, loan)); MonetaryCurrency currency = loan.getCurrency(); LocalDateTime createdDate = DateUtils.getLocalDateTimeOfTenant(); final Map<String, Object> changes = new LinkedHashMap<>(); List<LoanTransaction> newTransactions = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>(); existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); final ScheduleGeneratorDTO scheduleGeneratorDTO = null; AppUser appUser = getAppUserIfPresent(); final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct() && (loan.getAccruedTill() == null || !foreClosureDate.isEqual(loan.getAccruedTill()))) { loan.reverseAccrualsAfter(foreClosureDate); Money[] accruedReceivables = loan.getReceivableIncome(foreClosureDate); Money interestPortion = foreCloseDetail.getInterestCharged(currency).minus(accruedReceivables[0]); Money feePortion = foreCloseDetail.getFeeChargesCharged(currency).minus(accruedReceivables[1]); Money penaltyPortion = foreCloseDetail.getPenaltyChargesCharged(currency).minus(accruedReceivables[2]); Money total = interestPortion.plus(feePortion).plus(penaltyPortion); if (total.isGreaterThanZero()) { LoanTransaction accrualTransaction = LoanTransaction.accrueTransaction(loan, loan.getOffice(), foreClosureDate, total.getAmount(), interestPortion.getAmount(), feePortion.getAmount(), penaltyPortion.getAmount(), appUser); LocalDate fromDate = loan.getDisbursementDate(); if (loan.getAccruedTill() != null) { fromDate = loan.getAccruedTill(); } createdDate = createdDate.plusSeconds(1); newTransactions.add(accrualTransaction); loan.addLoanTransaction(accrualTransaction); Set<LoanChargePaidBy> accrualCharges = accrualTransaction.getLoanChargesPaid(); for (LoanCharge loanCharge : loan.charges()) { if (loanCharge.isActive() && !loanCharge.isPaid() && (loanCharge.isDueForCollectionFromAndUpToAndIncluding(fromDate, foreClosureDate) || loanCharge.isInstalmentFee())) { final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(accrualTransaction, loanCharge, loanCharge.getAmountOutstanding(currency).getAmount(), null); accrualCharges.add(loanChargePaidBy); } } } } Money interestPayable = foreCloseDetail.getInterestCharged(currency); Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency); Money payPrincipal = foreCloseDetail.getPrincipal(currency); loan.updateInstallmentsPostDate(foreClosureDate); LoanTransaction payment = null; if (payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable).isGreaterThanZero()) { final PaymentDetail paymentDetail = null; String externalId = null; final LocalDateTime currentDateTime = DateUtils.getLocalDateTimeOfTenant(); payment = LoanTransaction.repayment(loan.getOffice(), payPrincipal.plus(interestPayable).plus(feePayable).plus(penaltyPayable), paymentDetail, foreClosureDate, externalId, currentDateTime, appUser); createdDate = createdDate.plusSeconds(1); payment.updateCreatedDate(createdDate.toDate()); payment.updateLoan(loan); newTransactions.add(payment); } List<Long> transactionIds = new ArrayList<>(); final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment, defaultLoanLifecycleStateMachine(), scheduleGeneratorDTO, appUser); /*** * 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 payment is made before the latest payment * recorded against the loan) ***/ for (LoanTransaction newTransaction : newTransactions) { saveLoanTransactionWithDataIntegrityViolationChecks(newTransaction); transactionIds.add(newTransaction.getId()); } changes.put("transactions", transactionIds); changes.put("eventAmount", payPrincipal.getAmount().negate()); if (changedTransactionDetail != null) { for (Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings() .entrySet()) { saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); // update loan with references to the newly created transactions loan.getLoanTransactions().add(mapEntry.getValue()); updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); } } saveAndFlushLoanWithDataIntegrityViolationChecks(loan); if (StringUtils.isNotBlank(noteText)) { changes.put("note", noteText); final Note note = Note.loanNote(loan, noteText); this.noteRepository.save(note); } postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); this.businessEventNotifierService.notifyBusinessEventWasExecuted(BUSINESS_EVENTS.LOAN_FORECLOSURE, constructEntityMap(BUSINESS_ENTITY.LOAN_TRANSACTION, payment)); return changes; } private Map<BUSINESS_ENTITY, Object> constructEntityMap(final BUSINESS_ENTITY entityEvent, Object entity) { Map<BUSINESS_ENTITY, Object> map = new HashMap<>(1); map.put(entityEvent, entity); return map; } @Override @Transactional public void disableStandingInstructionsLinkedToClosedLoan(Loan loan) { if ((loan != null) && (loan.status() != null) && loan.status().isClosed()) { final Integer standingInstructionStatus = StandingInstructionStatus.ACTIVE.getValue(); final Collection<AccountTransferStandingInstruction> accountTransferStandingInstructions = this.standingInstructionRepository .findByLoanAccountAndStatus(loan, standingInstructionStatus); if (!accountTransferStandingInstructions.isEmpty()) { for (AccountTransferStandingInstruction accountTransferStandingInstruction : accountTransferStandingInstructions) { accountTransferStandingInstruction.updateStatus(StandingInstructionStatus.DISABLED.getValue()); this.standingInstructionRepository.save(accountTransferStandingInstruction); } } } } }