FINERACT-1905 Loan Charge Reverse Replay with Advanced Payment Allocation

This commit is contained in:
Peter Bagrij
2023-10-27 22:10:14 +02:00
committed by Adam Saghy
parent c5932ec7a5
commit 937098c4ee
10 changed files with 1430 additions and 325 deletions
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
@@ -37,13 +38,14 @@ import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Profile;
//TODO: remove `test` profile when it is finished
@@ -53,6 +55,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
private final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
@Override
public String getCode() {
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
@@ -91,10 +95,44 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
throw new NotImplementedException();
}
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges,
ChangedTransactionDetail changedTransactionDetail) {
if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency));
loanTransaction.adjustInterestComponent(currency);
} else {
/*
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
* changed.<br>
*/
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
// Reset derived component of new loan transaction and
// re-process transaction
processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency));
newLoanTransaction.adjustInterestComponent(currency);
/*
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
* changedTransactionDetail accordingly
*/
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else {
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
}
}
}
private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
LocalDate disbursementDate) {
loanChargeProcessor.reprocess(currency, disbursementDate, installments, loanCharge);
}
@Override
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate,
List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
// TODO: rewrite this whole logic step by step
if (charges != null) {
@@ -107,44 +145,37 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
currentInstallment.resetChargesFields();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
// TODO: Remove this reprocess and add the charges to the installment in chronological order
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(currency, disbursementDate, installments, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency));
loanTransaction.adjustInterestComponent(currency);
} else {
/**
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
* changed.<br>
**/
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
// Reset derived component of new loan transaction and
// re-process transaction
processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency));
newLoanTransaction.adjustInterestComponent(currency);
/**
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
* changedTransactionDetail accordingly
**/
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else {
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
}
}
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
installments, charges, changedTransactionDetail));
chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
}
reprocessInstallments(installments, currency);
return changedTransactionDetail;
}
@NotNull
private List<ChargeOrTransaction> createSortedChargesAndTransactionsList(List<LoanTransaction> loanTransactions,
Set<LoanCharge> charges) {
List<ChargeOrTransaction> chargeOrTransactions = new ArrayList<>();
if (charges != null) {
chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList());
}
if (loanTransactions != null) {
chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList());
}
Collections.sort(chargeOrTransactions);
return chargeOrTransactions;
}
@Override
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
@@ -0,0 +1,88 @@
/**
* 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.domain.transactionprocessor.impl;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.Optional;
import lombok.Getter;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.jetbrains.annotations.NotNull;
@Getter
public class ChargeOrTransaction implements Comparable<ChargeOrTransaction> {
private final Optional<LoanCharge> loanCharge;
private final Optional<LoanTransaction> loanTransaction;
public ChargeOrTransaction(LoanCharge loanCharge) {
this.loanCharge = Optional.of(loanCharge);
this.loanTransaction = Optional.empty();
}
public ChargeOrTransaction(LoanTransaction loanTransaction) {
this.loanTransaction = Optional.of(loanTransaction);
this.loanCharge = Optional.empty();
}
private LocalDate getEffectiveDate() {
if (loanCharge.isPresent()) {
return loanCharge.get().getDueDate();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getTransactionDate();
} else {
throw new RuntimeException("Either charge or transaction should be present");
}
}
private LocalDate getSubmittedOnDate() {
if (loanCharge.isPresent()) {
return loanCharge.get().getSubmittedOnDate();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getSubmittedOnDate();
} else {
throw new RuntimeException("Either charge or transaction should be present");
}
}
private OffsetDateTime getCreatedDateTime() {
if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) {
return loanCharge.get().getCreatedDate().get();
} else if (loanTransaction.isPresent()) {
return loanTransaction.get().getCreatedDateTime();
} else {
throw new RuntimeException("Either charge with createdDate or transaction created datetime should be present");
}
}
@Override
public int compareTo(@NotNull ChargeOrTransaction o) {
int datePortion = this.getEffectiveDate().compareTo(o.getEffectiveDate());
if (datePortion == 0) {
int submittedDate = this.getSubmittedOnDate().compareTo(o.getSubmittedOnDate());
if (submittedDate == 0) {
return this.getCreatedDateTime().compareTo(o.getCreatedDateTime());
}
return submittedDate;
}
return datePortion;
}
}