| /** |
| * 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.reamortization; |
| |
| import static java.math.BigDecimal.ZERO; |
| |
| import java.math.BigDecimal; |
| import java.time.LocalDate; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import lombok.RequiredArgsConstructor; |
| import org.apache.fineract.infrastructure.core.api.JsonCommand; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; |
| import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; |
| import org.apache.fineract.infrastructure.core.domain.ExternalId; |
| import org.apache.fineract.infrastructure.core.service.DateUtils; |
| import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.reamortization.LoanReAmortizeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.reamortization.LoanUndoReAmortizeBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanReAmortizeTransactionBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reamortization.LoanUndoReAmortizeTransactionBusinessEvent; |
| import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; |
| import org.apache.fineract.organisation.monetary.domain.Money; |
| import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants; |
| import org.apache.fineract.portfolio.loanaccount.domain.Loan; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; |
| import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; |
| import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; |
| import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; |
| import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; |
| import org.springframework.stereotype.Service; |
| import org.springframework.transaction.annotation.Transactional; |
| |
| @Service |
| @RequiredArgsConstructor |
| @Transactional |
| public class LoanReAmortizationServiceImpl { |
| |
| private final LoanAssembler loanAssembler; |
| private final LoanReAmortizationValidator reAmortizationValidator; |
| private final ExternalIdFactory externalIdFactory; |
| private final BusinessEventNotifierService businessEventNotifierService; |
| private final LoanTransactionRepository loanTransactionRepository; |
| private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; |
| |
| public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) { |
| Loan loan = loanAssembler.assembleFrom(loanId); |
| reAmortizationValidator.validateReAmortize(loan, command); |
| |
| Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale()); |
| changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat()); |
| |
| LoanTransaction reAmortizeTransaction = createReAmortizeTransaction(loan, command); |
| |
| final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory |
| .determineProcessor(loan.transactionProcessingStrategy()); |
| loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction, |
| new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), |
| loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()))); |
| |
| loanTransactionRepository.saveAndFlush(reAmortizeTransaction); |
| |
| // delinquency recalculation will be triggered by the event in a decoupled way via a listener |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanReAmortizeBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanReAmortizeTransactionBusinessEvent(reAmortizeTransaction)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(reAmortizeTransaction.getId()) // |
| .withEntityExternalId(reAmortizeTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(command.getLoanId()) // |
| .with(changes).build(); |
| } |
| |
| public CommandProcessingResult undoReAmortize(Long loanId, JsonCommand command) { |
| Loan loan = loanAssembler.assembleFrom(loanId); |
| reAmortizationValidator.validateUndoReAmortize(loan, command); |
| |
| Map<String, Object> changes = new LinkedHashMap<>(); |
| changes.put(LoanReAmortizationApiConstants.localeParameterName, command.locale()); |
| changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat()); |
| |
| LoanTransaction reAmortizeTransaction = findLatestNonReversedReAmortizeTransaction(loan); |
| if (reAmortizeTransaction == null) { |
| // TODO: when validations implemented; throw exception if there isn't a reamortize transaction available |
| } |
| reverseReAmortizeTransaction(reAmortizeTransaction, command); |
| loanTransactionRepository.saveAndFlush(reAmortizeTransaction); |
| |
| // delinquency recalculation will be triggered by the event in a decoupled way via a listener |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAmortizeBusinessEvent(loan)); |
| businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAmortizeTransactionBusinessEvent(reAmortizeTransaction)); |
| return new CommandProcessingResultBuilder() // |
| .withCommandId(command.commandId()) // |
| .withEntityId(reAmortizeTransaction.getId()) // |
| .withEntityExternalId(reAmortizeTransaction.getExternalId()) // |
| .withOfficeId(loan.getOfficeId()) // |
| .withClientId(loan.getClientId()) // |
| .withGroupId(loan.getGroupId()) // |
| .withLoanId(command.getLoanId()) // |
| .with(changes).build(); |
| } |
| |
| private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, JsonCommand command) { |
| ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, |
| LoanReAmortizationApiConstants.externalIdParameterName); |
| reAmortizeTransaction.reverse(reversalExternalId); |
| reAmortizeTransaction.manuallyAdjustedOrReversed(); |
| reProcessLoanTransactions(reAmortizeTransaction.getLoan()); |
| } |
| |
| private void reProcessLoanTransactions(Loan loan) { |
| final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>(); |
| List<LoanTransaction> trans = loan.getLoanTransactions(); |
| for (final LoanTransaction transaction : trans) { |
| if (transaction.isNotReversed() && (transaction.isChargeOff() || !transaction.isNonMonetaryTransaction())) { |
| repaymentsOrWaivers.add(transaction); |
| } |
| } |
| repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE); |
| |
| final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory |
| .determineProcessor(loan.transactionProcessingStrategy()); |
| loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), repaymentsOrWaivers, |
| loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); |
| } |
| |
| private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) { |
| return loan.getLoanTransactions().stream() // |
| .filter(LoanTransaction::isNotReversed) // |
| .filter(LoanTransaction::isReAmortize) // |
| .max(Comparator.comparing(LoanTransaction::getTransactionDate)) // |
| .orElse(null); |
| } |
| |
| private LoanTransaction createReAmortizeTransaction(Loan loan, JsonCommand command) { |
| ExternalId txExternalId = externalIdFactory.createFromCommand(command, LoanReAmortizationApiConstants.externalIdParameterName); |
| |
| // reamortize transaction date is always the current business date |
| LocalDate transactionDate = DateUtils.getBusinessLocalDate(); |
| |
| // in case of a reamortize transaction, only the outstanding principal amount until the business date is |
| // considered |
| Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate); |
| BigDecimal txPrincipalAmount = txPrincipal.getAmount(); |
| |
| return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAMORTIZE.getValue(), transactionDate, txPrincipalAmount, |
| txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId); |
| } |
| } |