FINERACT-1905 Loan Charge Reverse Replay with Advanced Payment Allocation
This commit is contained in:
+64
-33
@@ -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) {
|
||||
|
||||
+88
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* 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 com.google.common.collect.Collections2;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class ChargeOrTransactionTest {
|
||||
|
||||
@Test
|
||||
public void testCompareToEqual() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) == 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) == 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareToCreatedDateTime() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompareToSubmittedOnDate() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorEffectiveDate() {
|
||||
ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
Assertions.assertTrue(charge.compareTo(transaction) > 0);
|
||||
Assertions.assertTrue(transaction.compareTo(charge) < 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorOnDifferentSubmittedDay() {
|
||||
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00");
|
||||
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00");
|
||||
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00");
|
||||
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
|
||||
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
|
||||
for (List<ChargeOrTransaction> permutation : permutations) {
|
||||
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComparatorOnSameDay() {
|
||||
ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00");
|
||||
ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00");
|
||||
ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00");
|
||||
Collection<List<ChargeOrTransaction>> permutations = Collections2.permutations(List.of(cot1, cot2, cot3));
|
||||
List<ChargeOrTransaction> expected = List.of(cot1, cot3, cot2);
|
||||
for (List<ChargeOrTransaction> permutation : permutations) {
|
||||
Assertions.assertEquals(expected, permutation.stream().sorted().toList());
|
||||
}
|
||||
}
|
||||
|
||||
private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) {
|
||||
LoanCharge charge = Mockito.mock(LoanCharge.class);
|
||||
Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate));
|
||||
Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
|
||||
Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime)));
|
||||
return new ChargeOrTransaction(charge);
|
||||
}
|
||||
|
||||
private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) {
|
||||
LoanTransaction transaction = Mockito.mock(LoanTransaction.class);
|
||||
Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate));
|
||||
Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate));
|
||||
Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime));
|
||||
return new ChargeOrTransaction(transaction);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user