FINERACT-2060: Accrual reverse replay logic and Handling
This commit is contained in:
committed by
Adam Saghy
parent
8d25b0c765
commit
d57e72ec2f
+2
-1
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.charge.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.charge.data.ChargeData;
|
||||
import org.apache.fineract.portfolio.charge.domain.ChargeTimeType;
|
||||
|
||||
@@ -53,7 +54,7 @@ public interface ChargeReadPlatformService {
|
||||
* Excludes Given List of Charge Types from the response
|
||||
* @return
|
||||
*/
|
||||
Collection<ChargeData> retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes);
|
||||
List<ChargeData> retrieveLoanAccountApplicableCharges(Long loanId, ChargeTimeType[] excludeChargeTimes);
|
||||
|
||||
/**
|
||||
* Returns all charges applicable for a given loan product (filter based on Currency of Selected Loan Product)
|
||||
|
||||
+35
-22
@@ -193,11 +193,23 @@ public final class DateUtils {
|
||||
}
|
||||
|
||||
public static int compare(OffsetDateTime first, OffsetDateTime second, ChronoUnit truncate) {
|
||||
return compare(first, second, truncate, true);
|
||||
}
|
||||
|
||||
public static int compareWithNullsLast(OffsetDateTime first, OffsetDateTime second) {
|
||||
return compare(first, second, null, false);
|
||||
}
|
||||
|
||||
public static int compareWithNullsLast(@NotNull Optional<OffsetDateTime> first, @NotNull Optional<OffsetDateTime> second) {
|
||||
return compareWithNullsLast(first.orElse(null), second.orElse(null));
|
||||
}
|
||||
|
||||
public static int compare(OffsetDateTime first, OffsetDateTime second, ChronoUnit truncate, boolean nullFirst) {
|
||||
if (first == null) {
|
||||
return second == null ? 0 : -1;
|
||||
return second == null ? 0 : (nullFirst ? -1 : 1);
|
||||
}
|
||||
if (second == null) {
|
||||
return 1;
|
||||
return nullFirst ? 1 : -1;
|
||||
}
|
||||
first = first.withOffsetSameInstant(ZoneOffset.UTC);
|
||||
second = second.withOffsetSameInstant(ZoneOffset.UTC);
|
||||
@@ -291,7 +303,23 @@ public final class DateUtils {
|
||||
}
|
||||
|
||||
public static int compare(LocalDate first, LocalDate second) {
|
||||
return first == null ? (second == null ? 0 : -1) : (second == null ? 1 : first.compareTo(second));
|
||||
return compare(first, second, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparing dates. Null will be considered as last elements
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
public static int compareWithNullsLast(LocalDate first, LocalDate second) {
|
||||
return compare(first, second, false);
|
||||
}
|
||||
|
||||
public static int compare(LocalDate first, LocalDate second, boolean nullFirst) {
|
||||
return first == null ? (second == null ? 0 : (nullFirst ? -1 : 1))
|
||||
: (second == null ? (nullFirst ? 1 : -1) : first.compareTo(second));
|
||||
}
|
||||
|
||||
public static boolean isEqual(LocalDate first, LocalDate second) {
|
||||
@@ -325,6 +353,10 @@ public final class DateUtils {
|
||||
return getExactDifference(first, second, DAYS);
|
||||
}
|
||||
|
||||
public static LocalDate minusDays(LocalDate first, int days) {
|
||||
return first == null ? null : first.minusDays(days);
|
||||
}
|
||||
|
||||
// Parse, format
|
||||
|
||||
public static LocalDate parseLocalDate(String stringDate) {
|
||||
@@ -426,23 +458,4 @@ public final class DateUtils {
|
||||
}
|
||||
return formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Comparing dates. Null will be considered as last elements
|
||||
*
|
||||
* @param first
|
||||
* @param second
|
||||
* @return
|
||||
*/
|
||||
public static int compareWithNullsLast(LocalDate first, LocalDate second) {
|
||||
return first == null ? (second == null ? 0 : 1) : (second == null ? -1 : first.compareTo(second));
|
||||
}
|
||||
|
||||
public static int compareWithNullsLast(@NotNull Optional<OffsetDateTime> first, @NotNull Optional<OffsetDateTime> second) {
|
||||
return DateUtils.compareWithNullsLast(first.orElse(null), second.orElse(null));
|
||||
}
|
||||
|
||||
public static int compareWithNullsLast(OffsetDateTime first, OffsetDateTime second) {
|
||||
return first == null ? (second == null ? 0 : 1) : (second == null ? -1 : first.compareTo(second));
|
||||
}
|
||||
}
|
||||
|
||||
+20
-1
@@ -232,7 +232,7 @@ public final class MathUtil {
|
||||
* if true then null parameter is omitted, otherwise returns null
|
||||
*/
|
||||
public static BigDecimal min(BigDecimal first, BigDecimal second, boolean notNull) {
|
||||
return notNull ? first == null ? second : second == null ? first : min(first, second, false)
|
||||
return notNull ? (first == null ? second : (second == null ? first : min(first, second, false)))
|
||||
: isLessThan(first, second) ? first : second;
|
||||
}
|
||||
|
||||
@@ -291,6 +291,11 @@ public final class MathUtil {
|
||||
return subtract(first, second, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
/** @return first minus the others considering null values, maybe negative */
|
||||
public static BigDecimal subtract(BigDecimal first, BigDecimal second, BigDecimal third) {
|
||||
return subtract(subtract(first, second), third);
|
||||
}
|
||||
|
||||
/** @return first minus second considering null values, maybe negative */
|
||||
public static BigDecimal subtract(BigDecimal first, BigDecimal second, MathContext mc) {
|
||||
return first == null ? null : second == null ? first : first.subtract(second, mc);
|
||||
@@ -336,6 +341,10 @@ public final class MathUtil {
|
||||
return amount == null ? null : amount.toPlainString();
|
||||
}
|
||||
|
||||
public static Money toMoney(BigDecimal amount, @NotNull MonetaryCurrency currency) {
|
||||
return amount == null ? null : Money.of(currency, amount);
|
||||
}
|
||||
|
||||
// ----------------- Money -----------------
|
||||
|
||||
public static BigDecimal toBigDecimal(Money value) {
|
||||
@@ -454,6 +463,16 @@ public final class MathUtil {
|
||||
return min(min(first, second, notNull), third, notNull);
|
||||
}
|
||||
|
||||
/** @return Money null safe negate */
|
||||
public static Money negate(Money amount) {
|
||||
return negate(amount, MoneyHelper.getMathContext());
|
||||
}
|
||||
|
||||
/** @return Money null safe negate */
|
||||
public static Money negate(Money amount, MathContext mc) {
|
||||
return isEmpty(amount) ? amount : amount.negated(mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage of a value
|
||||
*
|
||||
|
||||
+1
@@ -61,6 +61,7 @@ public enum DefaultLoanProduct implements LoanProduct {
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SARP_TILL_PRECLOSE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_REST_FREQUENCY_DATE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SARP_TILL_REST_FREQUENCY_DATE, //
|
||||
LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT, //
|
||||
|
||||
+41
-13
@@ -447,13 +447,26 @@ public final class ErrorMessageHelper {
|
||||
}
|
||||
|
||||
public static String wrongNumberOfLinesInRepaymentSchedule(int actual, int expected) {
|
||||
return wrongNumberOfLinesInRepaymentSchedule(null, actual, expected);
|
||||
}
|
||||
|
||||
public static String wrongNumberOfLinesInRepaymentSchedule(String resourceId, int actual, int expected) {
|
||||
String actualStr = String.valueOf(actual);
|
||||
String expectedStr = String.valueOf(expected);
|
||||
return String.format("Number of lines in Repayment schedule is not correct. Actual value is: %s - Expected value is: %s", actualStr,
|
||||
expectedStr);
|
||||
String prefx = "Number of lines in Repayment schedule";
|
||||
String postfx = " is not correct. Actual value is: %s - Expected value is: %s";
|
||||
if (resourceId != null) {
|
||||
return String.format(prefx + " of resource %s" + postfx, resourceId, actualStr, expectedStr);
|
||||
}
|
||||
return String.format(prefx + postfx, actualStr, expectedStr);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInRepaymentSchedule(int line, List<List<String>> actual, List<String> expected) {
|
||||
return wrongValueInLineInRepaymentSchedule(null, line, actual, expected);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInRepaymentSchedule(String resourceId, int line, List<List<String>> actual,
|
||||
List<String> expected) {
|
||||
String lineStr = String.valueOf(line);
|
||||
String expectedStr = expected.toString();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -461,13 +474,19 @@ public final class ErrorMessageHelper {
|
||||
sb.append(innerList.toString());
|
||||
sb.append(System.lineSeparator());
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"%nWrong value in Repayment schedule tab line %s. %nActual values in line (with the same due date) are: %n%s %nExpected values in line: %n%s",
|
||||
lineStr, sb.toString(), expectedStr);
|
||||
String prefx = "%nWrong value in Repayment schedule";
|
||||
String postfx = " tab line %s. %nActual values in line (with the same due date) are: %n%s %nExpected values in line: %n%s";
|
||||
if (resourceId != null) {
|
||||
return String.format(prefx + " of resource %s" + postfx, resourceId, lineStr, sb.toString(), expectedStr);
|
||||
}
|
||||
return String.format(prefx + postfx, lineStr, sb.toString(), expectedStr);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInTransactionsTab(int line, List<List<String>> actual, List<String> expected) {
|
||||
return wrongValueInLineInTransactionsTab(null, line, actual, expected);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInTransactionsTab(String resourceId, int line, List<List<String>> actual, List<String> expected) {
|
||||
String lineStr = String.valueOf(line);
|
||||
String expectedStr = expected.toString();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
@@ -475,19 +494,28 @@ public final class ErrorMessageHelper {
|
||||
sb.append(innerList.toString());
|
||||
sb.append(System.lineSeparator());
|
||||
}
|
||||
|
||||
return String.format(
|
||||
"%nWrong value in Transactions tab line %s. %nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s",
|
||||
lineStr, sb.toString(), expectedStr);
|
||||
String prefx = "%nWrong value in Transactions tab";
|
||||
String postfx = " line %s. %nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s";
|
||||
if (resourceId != null) {
|
||||
return String.format(prefx + " of resource %s" + postfx, resourceId, lineStr, sb.toString(), expectedStr);
|
||||
}
|
||||
return String.format(prefx + postfx, lineStr, sb.toString(), expectedStr);
|
||||
}
|
||||
|
||||
public static String nrOfLinesWrongInTransactionsTab(int actual, int expected) {
|
||||
return nrOfLinesWrongInTransactionsTab(null, actual, expected);
|
||||
}
|
||||
|
||||
public static String nrOfLinesWrongInTransactionsTab(String resourceId, int actual, int expected) {
|
||||
String actualStr = String.valueOf(actual);
|
||||
String expectedStr = String.valueOf(expected);
|
||||
|
||||
return String.format(
|
||||
"%nNumber of lines does not match in Transactions tab and expected datatable. %nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n",
|
||||
actualStr, expectedStr);
|
||||
String prefx = "%nNumber of lines does not match in Transactions tab and expected datatable";
|
||||
String postfx = ". %nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n";
|
||||
if (resourceId != null) {
|
||||
return String.format(prefx + " of resource %s" + postfx, resourceId, actualStr, expectedStr);
|
||||
}
|
||||
return String.format(prefx + postfx, actualStr, expectedStr);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInChargesTab(int line, List<List<String>> actual, List<String> expected) {
|
||||
|
||||
+43
@@ -1045,6 +1045,49 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT,
|
||||
responseLoanProductsRequestLP2AdvancedpaymentInterestRecalculation36030MultiDisburseDownPayment);
|
||||
|
||||
// LP2 with progressive loan schedule + horizontal + interest recalculation daily EMI + 360/30 + multi
|
||||
// disbursement + custom default payment allocation order
|
||||
// (LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE)
|
||||
String name50 = DefaultLoanProduct.LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE.getName();
|
||||
PostLoanProductsRequest loanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDailyEmi36030MultiDisburse = loanProductsRequestFactory
|
||||
.defaultLoanProductsRequestLP2Emi()//
|
||||
.name(name50)//
|
||||
.daysInYearType(DaysInYearType.DAYS360.value)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS30.value)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.preClosureInterestCalculationStrategy(1)//
|
||||
.rescheduleStrategyMethod(4)//
|
||||
.interestRecalculationCompoundingMethod(0)//
|
||||
.recalculationRestFrequencyType(2)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.paymentAllocation(List.of(//
|
||||
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT",
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST, //
|
||||
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL), //
|
||||
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
|
||||
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
|
||||
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")))//
|
||||
.multiDisburseLoan(true)//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.maxTrancheCount(10)//
|
||||
.outstandingLoanBalance(10000.0);//
|
||||
Response<PostLoanProductsResponse> responseLoanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDaily36030MultiDisburse = loanProductsApi
|
||||
.createLoanProduct(loanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDailyEmi36030MultiDisburse)
|
||||
.execute();
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE,
|
||||
responseLoanProductsRequestLP2AdvCustomPaymentAllocationInterestRecalculationDaily36030MultiDisburse);
|
||||
}
|
||||
|
||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||
|
||||
+13
-7
@@ -1613,8 +1613,10 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
|
||||
@Then("Loan Repayment schedule has {int} periods, with the following data for periods:")
|
||||
public void loanRepaymentSchedulePeriodsCheck(int linesExpected, DataTable table) throws IOException {
|
||||
|
||||
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
long loanId = loanCreateResponse.body().getLoanId();
|
||||
String resourceId = String.valueOf(loanId);
|
||||
|
||||
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "repaymentSchedule", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
@@ -1634,9 +1636,9 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
|
||||
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
|
||||
assertThat(containsExpectedValues)
|
||||
.as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(i, actualValuesList, expectedValues)).isTrue();
|
||||
.as(ErrorMessageHelper.wrongValueInLineInRepaymentSchedule(resourceId, i, actualValuesList, expectedValues)).isTrue();
|
||||
|
||||
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(linesActual, linesExpected))
|
||||
assertThat(linesActual).as(ErrorMessageHelper.wrongNumberOfLinesInRepaymentSchedule(resourceId, linesActual, linesExpected))
|
||||
.isEqualTo(linesExpected);
|
||||
}
|
||||
}
|
||||
@@ -1660,6 +1662,7 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
public void loanTransactionsTransactionWithGivenDateDataCheck(String date, DataTable table) throws IOException {
|
||||
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
long loanId = loanCreateResponse.body().getLoanId();
|
||||
String resourceId = String.valueOf(loanId);
|
||||
|
||||
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
@@ -1673,14 +1676,16 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
.map(t -> fetchValuesOfTransaction(data.get(0), t)).collect(Collectors.toList());
|
||||
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
|
||||
|
||||
assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(1, actualValuesList, expectedValues))
|
||||
.isTrue();
|
||||
assertThat(containsExpectedValues)
|
||||
.as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, 1, actualValuesList, expectedValues)).isTrue();
|
||||
}
|
||||
|
||||
@Then("Loan Transactions tab has the following data:")
|
||||
public void loanTransactionsTabCheck(DataTable table) throws IOException {
|
||||
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
long loanId = loanCreateResponse.body().getLoanId();
|
||||
String resourceId = String.valueOf(loanId);
|
||||
|
||||
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
|
||||
List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
|
||||
@@ -1694,10 +1699,11 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
.collect(Collectors.toList());//
|
||||
boolean containsExpectedValues = actualValuesList.stream()//
|
||||
.anyMatch(actualValues -> actualValues.equals(expectedValues));//
|
||||
assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(i, actualValuesList, expectedValues))
|
||||
.isTrue();
|
||||
assertThat(containsExpectedValues)
|
||||
.as(ErrorMessageHelper.wrongValueInLineInTransactionsTab(resourceId, i, actualValuesList, expectedValues)).isTrue();
|
||||
}
|
||||
assertThat(transactions.size()).as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(transactions.size(), data.size() - 1))
|
||||
assertThat(transactions.size())
|
||||
.as(ErrorMessageHelper.nrOfLinesWrongInTransactionsTab(resourceId, transactions.size(), data.size() - 1))
|
||||
.isEqualTo(data.size() - 1);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -96,6 +96,7 @@ public abstract class TestContextKey {
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_SAME_AS_REP_TILL_REST_FREQUENCY_DATE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationSameAsRepTillRestFrequencyDate";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_REFUND_FULL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRefundFull";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030MultiDisburse";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE = "loanProductCreateResponseLP2AdvancedCustomPaymentInterestRecalculationDailyEmi36030MultiDisburse";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_MULTIDISBURSE_DOWNPAYMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030MultiDisburseDownPayment";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_365_ACTUAL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi365Actual";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_DOWNPAYMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030DownPayment";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5989,6 +5989,7 @@ Feature: Loan
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
|
||||
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
|
||||
| 15 February 2024 | Repayment | 83.81 | 83.57 | 0.24 | 0.0 | 0.0 | 0.0 | false |
|
||||
| 15 February 2024 | Accrual | 0.82 | 0.0 | 0.82 | 0.0 | 0.0 | 0.0 | false |
|
||||
Then Loan's all installments have obligations met
|
||||
|
||||
Scenario: Early pay-off loan with interest, TILL_REST_FREQUENCY_DATE product
|
||||
@@ -6049,6 +6050,7 @@ Feature: Loan
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | false |
|
||||
| 01 February 2024 | Repayment | 17.01 | 16.43 | 0.58 | 0.0 | 0.0 | 83.57 | false |
|
||||
| 15 February 2024 | Repayment | 84.06 | 83.57 | 0.49 | 0.0 | 0.0 | 0.0 | false |
|
||||
| 15 February 2024 | Accrual | 1.07 | 0.0 | 1.07 | 0.0 | 0.0 | 0.0 | false |
|
||||
Then Loan's all installments have obligations met
|
||||
When Admin set "LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALCULATION_DAILY_TILL_REST_FREQUENCY_DATE" loan product "DEFAULT" transaction type to "LAST_INSTALLMENT" future installment allocation rule
|
||||
|
||||
|
||||
+192
-24
@@ -1329,12 +1329,12 @@ Feature: LoanAccrualActivity
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 4.1 | 0.0 | 0.0 | 1004.1 | 0.0 | 0.0 | 0.0 | 1004.1 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
# --- Accrual activity ---
|
||||
When Admin sets the business date to "07 January 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
@@ -1349,15 +1349,15 @@ Feature: LoanAccrualActivity
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 4.1 | 0.0 | 0.0 | 1004.1 | 0.0 | 0.0 | 0.0 | 1004.1 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 05 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 06 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 02 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 03 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 04 January 2024 | Repayment | 1004.1 | 1000.0 | 4.1 | 0.0 | 0.0 | 0.0 | true | false |
|
||||
| 04 January 2024 | Accrual | 0.32 | 0.0 | 0.32 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 05 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 06 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
|
||||
@TestRailId:C3188
|
||||
Scenario: Verify accrual activity - UC16: Preclose, loan account partially paid before first installment due date, fully paid after first installment date, reopen by undo 1st repayment
|
||||
@@ -1775,15 +1775,15 @@ Feature: LoanAccrualActivity
|
||||
When Admin sets the business date to "07 January 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 5 | 06 January 2024 | | 750.64 | 249.36 | 1.64 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 2 | 5 | 11 January 2024 | | 500.96 | 249.68 | 1.32 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 3 | 5 | 16 January 2024 | | 250.78 | 250.18 | 0.82 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 4 | 5 | 21 January 2024 | | 0.0 | 250.78 | 0.41 | 0.0 | 0.0 | 251.19| 0.0 | 0.0 | 0.0 | 251.19 |
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 5 | 06 January 2024 | | 750.64 | 249.36 | 1.64 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 2 | 5 | 11 January 2024 | | 500.96 | 249.68 | 1.32 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 3 | 5 | 16 January 2024 | | 250.78 | 250.18 | 0.82 | 0.0 | 0.0 | 251.0 | 0.0 | 0.0 | 0.0 | 251.0 |
|
||||
| 4 | 5 | 21 January 2024 | | 0.0 | 250.78 | 0.41 | 0.0 | 0.0 | 251.19 | 0.0 | 0.0 | 0.0 | 251.19 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 4.19 | 0.0 | 0.0 | 1004.19| 0.0 | 0.0 | 0.0 | 1004.19 |
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 4.19 | 0.0 | 0.0 | 1004.19 | 0.0 | 0.0 | 0.0 | 1004.19 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
@@ -2045,7 +2045,175 @@ Feature: LoanAccrualActivity
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 22 April 2024 | Disbursement | 400.0 | 0.0 | 0.0 | 0.0 | 0.0 | 400.0 | false | false |
|
||||
| 22 April 2024 | Accrual | 30.0 | 0.0 | 0.0 | 0.0 | 30.0 | 0.0 | false | false |
|
||||
| 22 April 2024 | Repayment | 600.0 | 400.0 | 0.0 | 0.0 | 30.0 | 0.0 | true | true |
|
||||
| 22 April 2024 | Goodwill Credit | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 400.0 | false | true |
|
||||
| 09 October 2024 | Accrual | 30.0 | 0.0 | 0.0 | 0.0 | 30.0 | 0.0 | false | false |
|
||||
| 10 October 2024 | Charge Adjustment | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 400.0 | false | true |
|
||||
|
||||
@TestRailId:C3274
|
||||
Scenario: Verify accrual activity for repayment reversal on the progressive loan
|
||||
When Admin sets the business date to "01 January 2024"
|
||||
When Admin creates a client with random data
|
||||
When Admin creates a fully customized loan with the following data:
|
||||
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
|
||||
| LP2_ADV_CUSTOM_PAYMENT_ALLOC_INTEREST_RECALCULATION_DAILY_EMI_360_30_MULTIDISBURSE | 01 January 2024 | 1000 | 26 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
|
||||
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
|
||||
When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
When Admin sets the business date to "14 January 2024"
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Admin sets the business date to "15 January 2024"
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Customer makes "AUTOPAY" repayment on "15 January 2024" with 263.69 EUR transaction amount
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | 15 January 2024 | 746.1 | 253.9 | 9.79 | 0.0 | 0.0 | 263.69 | 263.69 | 263.69 | 0.0 | 0.0 |
|
||||
| 2 | 29 | 01 March 2024 | | 507.44 | 238.66 | 25.03 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 254.74 | 252.7 | 10.99 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 254.74 | 5.52 | 0.0 | 0.0 | 260.26 | 0.0 | 0.0 | 0.0 | 260.26 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 51.33 | 0.0 | 0.0 | 1051.33 | 263.69 | 263.69 | 0.0 | 787.64 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | false | false |
|
||||
When Admin sets the business date to "21 January 2024"
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | false | false |
|
||||
| 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Customer undo "1"th repayment on "15 January 2024"
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false |
|
||||
| 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Admin sets the business date to "23 January 2024"
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false |
|
||||
| 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 21 January 2024 | Accrual | 1.58 | 0.0 | 1.58 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 22 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
When Admin sets the business date to "28 January 2024"
|
||||
And Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 4 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2024 | | 757.98 | 242.02 | 21.67 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 2 | 29 | 01 March 2024 | | 510.71 | 247.27 | 16.42 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 3 | 31 | 01 April 2024 | | 258.09 | 252.62 | 11.07 | 0.0 | 0.0 | 263.69 | 0.0 | 0.0 | 0.0 | 263.69 |
|
||||
| 4 | 30 | 01 May 2024 | | 0.0 | 258.09 | 5.59 | 0.0 | 0.0 | 263.68 | 0.0 | 0.0 | 0.0 | 263.68 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 54.75 | 0.0 | 0.0 | 1054.75 | 0.0 | 0.0 | 0.0 | 1054.75 |
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed |
|
||||
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | false |
|
||||
| 13 January 2024 | Accrual | 8.39 | 0.0 | 8.39 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 14 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 15 January 2024 | Repayment | 263.69 | 253.9 | 9.79 | 0.0 | 0.0 | 746.1 | true | false |
|
||||
| 15 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 16 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 17 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 18 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 19 January 2024 | Accrual | 0.52 | 0.0 | 0.52 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 20 January 2024 | Accrual | 0.53 | 0.0 | 0.53 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 21 January 2024 | Accrual | 1.58 | 0.0 | 1.58 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 22 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 23 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 24 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 25 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 26 January 2024 | Accrual | 0.7 | 0.0 | 0.7 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
| 27 January 2024 | Accrual | 0.69 | 0.0 | 0.69 | 0.0 | 0.0 | 0.0 | false | false |
|
||||
+5
-4
@@ -881,7 +881,8 @@ Feature: LoanAccrualTransaction
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 17 April 2024 | Disbursement | 750.0 | 0.0 | 0.0 | 0.0 | 0.0 | 750.0 |
|
||||
| 17 April 2024 | Waive loan charges | 20.0 | 0.0 | 0.0 | 0.0 | 0.0 | 750.0 |
|
||||
| 17 April 2024 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 |
|
||||
| 17 April 2024 | Waive loan charges | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 750.0 |
|
||||
| 19 April 2024 | Accrual | 55.0 | 0.0 | 0.0 | 0.0 | 55.0 | 0.0 |
|
||||
| 20 April 2024 | Repayment | 55.0 | 55.0 | 0.0 | 0.0 | 0.0 | 695.0 |
|
||||
| 20 April 2024 | Repayment | 810.0 | 695.0 | 0.0 | 0.0 | 115.0 | 0.0 |
|
||||
@@ -921,8 +922,8 @@ Feature: LoanAccrualTransaction
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 05 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 500.0 |
|
||||
| 20 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 |
|
||||
| 24 April 2024 | Accrual | 0.66 | 0.0 | 0.66 | 0.0 | 0.0 | 0.0 |
|
||||
| 25 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 500.0 | 5.77 | 0.0 | 0.0 | 505.77 | 0.0 | 0.0 | 0.0 | 505.77 |
|
||||
@@ -938,9 +939,9 @@ Feature: LoanAccrualTransaction
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 05 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 500.0 |
|
||||
| 25 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 |
|
||||
| 20 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 |
|
||||
| 24 April 2024 | Accrual | 1.05 | 0.0 | 1.05 | 0.0 | 0.0 | 0.0 |
|
||||
| 26 April 2024 | Disbursement | 500.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 26 April 2024 | Accrual | 2.47 | 0.0 | 2.47 | 0.0 | 0.0 | 0.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000.0 | 8.06 | 0.0 | 0.0 | 1008.06 | 0.0 | 0.0 | 0.0 | 1008.06 |
|
||||
|
||||
@@ -926,8 +926,8 @@ Feature: Charge-off
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 01 June 2023 | Charge-off | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 0.0 |
|
||||
When Admin sets the business date to "10 June 2023"
|
||||
When Customer undo "1"th "Repayment" transaction made on "10 February 2023"
|
||||
@@ -942,8 +942,8 @@ Feature: Charge-off
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 01 June 2023 | Charge-off | 1035.0 | 1000.0 | 0.0 | 20.0 | 15.0 | 0.0 |
|
||||
Then On Loan Transactions tab the "Repayment" Transaction with date "10 February 2023" is reverted
|
||||
|
||||
@@ -973,8 +973,8 @@ Feature: Charge-off
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 01 June 2023 | Charge-off | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 0.0 |
|
||||
When Admin sets the business date to "10 June 2023"
|
||||
When Customer undo "1"th "Repayment" transaction made on "10 February 2023"
|
||||
@@ -989,8 +989,8 @@ Feature: Charge-off
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 February 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 01 June 2023 | Charge-off | 1035.0 | 1000.0 | 0.0 | 20.0 | 15.0 | 0.0 |
|
||||
Then On Loan Transactions tab the "Repayment" Transaction with date "10 February 2023" is reverted
|
||||
|
||||
|
||||
@@ -2514,10 +2514,6 @@ Feature: LoanChargeback
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 250.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 250.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 30.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 30.0 |
|
||||
Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | | 280.0 |
|
||||
@@ -2563,10 +2559,6 @@ Feature: LoanChargeback
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 250.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 250.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 30.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 30.0 |
|
||||
Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | | 280.0 |
|
||||
@@ -2967,7 +2959,6 @@ Feature: LoanChargeback
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 19 January 2024 | Merchant Issued Refund | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 90.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 90.0 | 0.0 | 0.0 | 5.0 | 0.0 | 10.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 15 April 2024 | Chargeback | 7.0 | 2.0 | 0.0 | 0.0 | 5.0 | 0.0 | 7.0 |
|
||||
| 16 April 2024 | Chargeback | 7.0 | 7.0 | 0.0 | 0.0 | 0.0 | 4.0 | 3.0 |
|
||||
Then Loan Transactions tab has a "CHARGEBACK" transaction with date "16 April 2024" which has the following Journal entries:
|
||||
@@ -3041,7 +3032,6 @@ Feature: LoanChargeback
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 March 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 |
|
||||
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
@@ -3052,10 +3042,6 @@ Feature: LoanChargeback
|
||||
| ASSET | 112601 | Loans Receivable | | 100.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 5.0 |
|
||||
Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 March 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | | 3.0 |
|
||||
@@ -3079,7 +3065,6 @@ Feature: LoanChargeback
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 March 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 |
|
||||
| 05 March 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 8.0 | 0.0 |
|
||||
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
|
||||
@@ -3091,10 +3076,6 @@ Feature: LoanChargeback
|
||||
| ASSET | 112601 | Loans Receivable | | 100.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 5.0 |
|
||||
Then Loan Transactions tab has a "CHARGEBACK" transaction with date "01 March 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | | 3.0 |
|
||||
@@ -3156,6 +3137,8 @@ Feature: LoanChargeback
|
||||
# ---Chargeback with amount < fee ---
|
||||
When Admin sets the business date to "15 April 2024"
|
||||
When Admin makes "REPAYMENT_ADJUSTMENT_REFUND" chargeback with 3 EUR transaction amount for Payment nr. 1
|
||||
When Admin sets the business date to "16 April 2024"
|
||||
When Admin runs inline COB job for Loan
|
||||
Then Loan Repayment schedule has 5 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2024 | | 100.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
@@ -3171,8 +3154,8 @@ Feature: LoanChargeback
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 |
|
||||
| 15 April 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | 100.0 | |
|
||||
@@ -3182,7 +3165,7 @@ Feature: LoanChargeback
|
||||
| ASSET | 112601 | Loans Receivable | | 100.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 5.0 |
|
||||
@@ -3210,8 +3193,8 @@ Feature: LoanChargeback
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 100.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 0.0 |
|
||||
| 15 April 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 20 April 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 8.0 | 0.0 |
|
||||
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
@@ -3222,7 +3205,7 @@ Feature: LoanChargeback
|
||||
| ASSET | 112601 | Loans Receivable | | 100.0 |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 5.0 |
|
||||
@@ -3370,7 +3353,6 @@ Feature: LoanChargeback
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment |
|
||||
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 |
|
||||
| 19 January 2024 | Merchant Issued Refund | 10.0 | 10.0 | 0.0 | 0.0 | 0.0 | 90.0 | 0.0 |
|
||||
| 01 February 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | 0.0 |
|
||||
| 01 February 2024 | Repayment | 105.0 | 90.0 | 0.0 | 0.0 | 5.0 | 0.0 | 10.0 |
|
||||
| 15 April 2024 | Chargeback | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 3.0 |
|
||||
| 20 April 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 3.0 | 7.0 |
|
||||
@@ -3382,10 +3364,6 @@ Feature: LoanChargeback
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 10.0 |
|
||||
| LIABILITY | 145023 | Suspense/Clearing account | 10.0 | |
|
||||
Then Loan Transactions tab has a "ACCRUAL" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
|
||||
| INCOME | 404007 | Fee Income | | 5.0 |
|
||||
Then Loan Transactions tab has a "REPAYMENT" transaction with date "01 February 2024" which has the following Journal entries:
|
||||
| Type | Account code | Account name | Debit | Credit |
|
||||
| ASSET | 112601 | Loans Receivable | | 90.0 |
|
||||
|
||||
@@ -875,9 +875,9 @@ Feature: LoanRepayment
|
||||
Then Loan Repayment schedule has 3 periods, with the following data for periods:
|
||||
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| | | 01 January 2023 | | 1000.0 | | | 0.0 | | 0.0 | 0.0 | | | |
|
||||
| 1 | 31 | 01 February 2023 | | 666.67 | 333.33 | 10.0 | 103.0 | 10.0 | 456.33 | 400.0 | 400.0 | 0.0 | 56.33 |
|
||||
| 2 | 28 | 01 March 2023 | | 333.34 | 333.33 | 10.0 | 0.0 | 0.0 | 343.33 | 0.0 | 0.0 | 0.0 | 343.33 |
|
||||
| 3 | 31 | 01 April 2023 | | 0.0 | 333.34 | 10.0 | 0.0 | 0.0 | 343.34 | 0.0 | 0.0 | 0.0 | 343.34 |
|
||||
| 1 | 31 | 01 February 2023 | | 667.0 | 333.0 | 10.0 | 103.0 | 10.0 | 456.0 | 400.0 | 400.0 | 0.0 | 56.0 |
|
||||
| 2 | 28 | 01 March 2023 | | 334.0 | 333.0 | 10.0 | 0.0 | 0.0 | 343.0 | 0.0 | 0.0 | 0.0 | 343.0 |
|
||||
| 3 | 31 | 01 April 2023 | | 0.0 | 334.0 | 10.0 | 0.0 | 0.0 | 344.0 | 0.0 | 0.0 | 0.0 | 344.0 |
|
||||
Then Loan Repayment schedule has the following data in Total row:
|
||||
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||
| 1000 | 30 | 103 | 10 | 1143 | 400 | 400 | 0 | 743 |
|
||||
@@ -1281,6 +1281,7 @@ Feature: LoanRepayment
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 500.0 | 500.0 | 0.0 | 0.0 | 0.0 | 500.0 |
|
||||
| 17 January 2023 | Repayment | 550.0 | 500.0 | 0.0 | 0.0 | 50.0 | 0.0 |
|
||||
| 17 January 2023 | Accrual | 50.0 | 0.0 | 0.0 | 0.0 | 50.0 | 0.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
| NSF fee | true | Specified due date | 20 January 2023 | Flat | 50.0 | 50.0 | 0.0 | 0.0 |
|
||||
@@ -1887,15 +1888,15 @@ Feature: LoanRepayment
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 04 January 2023 | Repayment | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 800.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 500.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 290.0 | 0.0 | 10.0 | 0.0 | 510.0 |
|
||||
And Customer makes "AUTOPAY" repayment on "12 January 2023" with 510 EUR transaction amount
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 04 January 2023 | Repayment | 200.0 | 200.0 | 0.0 | 0.0 | 0.0 | 800.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 500.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 290.0 | 0.0 | 10.0 | 0.0 | 510.0 |
|
||||
| 12 January 2023 | Accrual | 10.0 | 0.0 | 0.0 | 10.0 | 0.0 | 0.0 |
|
||||
| 12 January 2023 | Repayment | 510.0 | 500.0 | 0.0 | 10.0 | 0.0 | 0.0 |
|
||||
| 12 January 2023 | Repayment | 510.0 | 510.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||
|
||||
@TestRailId:C2694 @PaymentStrategyDueInAdvancePenaltyInterestPrincipalFee
|
||||
Scenario: Verify the due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee-strategy payment strategy: UC1 - no fees or penalties, due payment
|
||||
@@ -2358,8 +2359,8 @@ Feature: LoanRepayment
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
| NSF fee | true | Specified due date | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
|
||||
@@ -2472,8 +2473,8 @@ Feature: LoanRepayment
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
| NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
|
||||
@@ -2563,8 +2564,8 @@ Feature: LoanRepayment
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
| NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
|
||||
@@ -2601,10 +2602,10 @@ Feature: LoanRepayment
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 10 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 10 January 2023 | Repayment | 1020.0 | 1000.0 | 0.0 | 20.0 | 0.0 | 0.0 |
|
||||
| 28 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 |
|
||||
| 28 January 2023 | Repayment | 1040.0 | 1000.0 | 0.0 | 20.0 | 20.0 | 0.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
| NSF fee | true | Specified due date | 01 February 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
|
||||
@@ -2750,8 +2751,8 @@ Feature: LoanRepayment
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 |
|
||||
| 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
Then Loan Charges tab has the following data:
|
||||
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
|
||||
@@ -2770,8 +2771,8 @@ Feature: LoanRepayment
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 |
|
||||
And Customer makes "AUTOPAY" repayment on "01 February 2023" with 1000 EUR transaction amount
|
||||
@@ -2780,8 +2781,8 @@ Feature: LoanRepayment
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 01 February 2023 | Repayment | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||
@@ -2791,8 +2792,8 @@ Feature: LoanRepayment
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 10 January 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 15 January 2023 | Repayment | 500.0 | 470.0 | 10.0 | 0.0 | 20.0 | 530.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 25 January 2023 | Accrual | 50.0 | 0.0 | 10.0 | 20.0 | 20.0 | 0.0 |
|
||||
| 25 January 2023 | Repayment | 530.0 | 500.0 | 10.0 | 0.0 | 20.0 | 500.0 |
|
||||
| 01 February 2023 | Repayment | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 1000.0 |
|
||||
| 01 February 2023 | Repayment | 10.0 | 0.0 | 10.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 01 February 2023 | Repayment | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||
@@ -2830,15 +2831,15 @@ Feature: LoanRepayment
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 04 January 2023 | Repayment | 200.0 | 190.0 | 10.0 | 0.0 | 0.0 | 810.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 510.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 289.9 | 0.0 | 10.1 | 0.0 | 520.1 |
|
||||
And Customer makes "AUTOPAY" repayment on "12 January 2023" with 520.10 EUR transaction amount
|
||||
Then Loan Transactions tab has the following data:
|
||||
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
|
||||
| 01 January 2023 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
|
||||
| 04 January 2023 | Repayment | 200.0 | 190.0 | 10.0 | 0.0 | 0.0 | 810.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 300.0 | 0.0 | 0.0 | 0.0 | 510.0 |
|
||||
| 12 January 2023 | Repayment | 300.0 | 289.9 | 0.0 | 10.1 | 0.0 | 520.1 |
|
||||
| 12 January 2023 | Repayment | 520.1 | 520.1 | 0.0 | 0.0 | 0.0 | 0.0 |
|
||||
| 12 January 2023 | Accrual | 20.1 | 0.0 | 10.0 | 10.1 | 0.0 | 0.0 |
|
||||
| 12 January 2023 | Repayment | 520.1 | 510.0 | 0.0 | 10.1 | 0.0 | 0.0 |
|
||||
|
||||
@TestRailId:C2810
|
||||
Scenario: As a user I would like to adjust an existing repayment and validate the event
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ public class ExternalAssetOwnerLoanStatusChangePlatformServiceImpl implements Ex
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class, event -> {
|
||||
final Loan loan = event.get();
|
||||
if (configurationReadPlatformService.retrieveGlobalConfiguration(ASSET_EXTERNALIZATION_OF_NON_ACTIVE_LOANS).isEnabled()
|
||||
&& (loan.isClosed() || loan.getStatus().isOverpaid())) {
|
||||
&& (event.getOldStatus().isActive() && (loan.isClosed() || loan.getStatus().isOverpaid()))) {
|
||||
loanAccountOwnerTransferService.handleLoanClosedOrOverpaid(loan);
|
||||
}
|
||||
});
|
||||
|
||||
+8
-1
@@ -19,17 +19,24 @@
|
||||
package org.apache.fineract.infrastructure.event.business.domain.loan;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
|
||||
public class LoanStatusChangedBusinessEvent extends LoanBusinessEvent {
|
||||
|
||||
private static final String TYPE = "LoanStatusChangedBusinessEvent";
|
||||
private final LoanStatus oldStatus;
|
||||
|
||||
public LoanStatusChangedBusinessEvent(Loan value) {
|
||||
public LoanStatusChangedBusinessEvent(Loan value, LoanStatus oldStatus) {
|
||||
super(value);
|
||||
this.oldStatus = oldStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
public LoanStatus getOldStatus() {
|
||||
return oldStatus;
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* 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.infrastructure.event.business.domain.loan.transaction;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public class LoanAccrualAdjustmentTransactionBusinessEvent extends LoanTransactionBusinessEvent {
|
||||
|
||||
private static final String TYPE = "LoanAccrualAdjustmentTransactionBusinessEvent";
|
||||
|
||||
public LoanAccrualAdjustmentTransactionBusinessEvent(LoanTransaction value) {
|
||||
super(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return TYPE;
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@RequiredArgsConstructor
|
||||
public class AccrualChargeData {
|
||||
|
||||
private final Long loanChargeId;
|
||||
private final Long loanInstallmentChargeId;
|
||||
private final boolean isPenalty;
|
||||
private Money chargeAmount;
|
||||
private Money chargeAccruable;
|
||||
private Money chargeAccrued;
|
||||
}
|
||||
+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.data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@RequiredArgsConstructor
|
||||
public class AccrualPeriodData {
|
||||
|
||||
private final Integer installmentNumber;
|
||||
private final boolean isFirstPeriod;
|
||||
private final LocalDate startDate;
|
||||
private final LocalDate dueDate;
|
||||
private Money interestAmount;
|
||||
private Money interestAccruable;
|
||||
private Money interestAccrued;
|
||||
private Money unrecognizedWaive;
|
||||
private Money transactionAccrued;
|
||||
private final List<AccrualChargeData> charges = new ArrayList<>();
|
||||
|
||||
public AccrualPeriodData addCharge(AccrualChargeData charge) {
|
||||
charges.add(charge);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Money getChargeAmount() {
|
||||
return charges.stream().map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getFeeAmount() {
|
||||
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getPenaltyAmount() {
|
||||
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAmount).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getChargeAccrued() {
|
||||
return charges.stream().map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getFeeAccrued() {
|
||||
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getPenaltyAccrued() {
|
||||
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getChargeAccruable() {
|
||||
return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getFeeAccruable() {
|
||||
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccruable).reduce(null,
|
||||
MathUtil::plus);
|
||||
}
|
||||
|
||||
public Money getPenaltyAccruable() {
|
||||
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccruable).reduce(null,
|
||||
MathUtil::plus);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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.data;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@RequiredArgsConstructor
|
||||
public class AccrualPeriodsData {
|
||||
|
||||
private final MonetaryCurrency currency;
|
||||
private final List<AccrualPeriodData> periods = new ArrayList<>();
|
||||
|
||||
public AccrualPeriodsData addPeriod(AccrualPeriodData period) {
|
||||
periods.add(period);
|
||||
return this;
|
||||
}
|
||||
|
||||
public static AccrualPeriodsData create(@NotNull List<LoanRepaymentScheduleInstallment> installments, Integer firstInstallmentNumber,
|
||||
MonetaryCurrency currency) {
|
||||
AccrualPeriodsData accrualPeriods = new AccrualPeriodsData(currency);
|
||||
for (LoanRepaymentScheduleInstallment installment : installments) {
|
||||
Integer installmentNumber = installment.getInstallmentNumber();
|
||||
boolean isFirst = installmentNumber.equals(firstInstallmentNumber);
|
||||
accrualPeriods
|
||||
.addPeriod(new AccrualPeriodData(installmentNumber, isFirst, installment.getFromDate(), installment.getDueDate()));
|
||||
}
|
||||
return accrualPeriods;
|
||||
}
|
||||
|
||||
public AccrualPeriodData getPeriodByInstallmentNumber(Integer installmentNumber) {
|
||||
return installmentNumber == null ? null
|
||||
: periods.stream().filter(p -> installmentNumber.equals(p.getInstallmentNumber())).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public Integer getFirstInstallmentNumber() {
|
||||
return periods.stream().filter(AccrualPeriodData::isFirstPeriod).map(AccrualPeriodData::getInstallmentNumber).findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
+9
-9
@@ -20,7 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
@@ -66,7 +66,7 @@ public class LoanChargeData {
|
||||
|
||||
private final BigDecimal amountOrPercentage;
|
||||
|
||||
private final Collection<ChargeData> chargeOptions;
|
||||
private final List<ChargeData> chargeOptions;
|
||||
|
||||
private final boolean penalty;
|
||||
|
||||
@@ -84,7 +84,7 @@ public class LoanChargeData {
|
||||
|
||||
private final BigDecimal maxCap;
|
||||
|
||||
private final Collection<LoanInstallmentChargeData> installmentChargeData;
|
||||
private final List<LoanInstallmentChargeData> installmentChargeData;
|
||||
|
||||
private BigDecimal amountAccrued;
|
||||
|
||||
@@ -94,7 +94,7 @@ public class LoanChargeData {
|
||||
|
||||
private final ExternalId externalLoanId;
|
||||
|
||||
public static LoanChargeData template(final Collection<ChargeData> chargeOptions) {
|
||||
public static LoanChargeData template(final List<ChargeData> chargeOptions) {
|
||||
return new LoanChargeData(null, null, null, null, null, null, null, null, chargeOptions, false, null, false, false, null,
|
||||
ExternalId.empty(), null, null, null, null, ExternalId.empty());
|
||||
}
|
||||
@@ -116,7 +116,7 @@ public class LoanChargeData {
|
||||
final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage,
|
||||
final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
|
||||
final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap,
|
||||
final BigDecimal amountOrPercentage, Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
final BigDecimal amountOrPercentage, List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
this.id = id;
|
||||
this.chargeId = chargeId;
|
||||
this.name = name;
|
||||
@@ -160,9 +160,9 @@ public class LoanChargeData {
|
||||
|
||||
private LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount,
|
||||
final BigDecimal percentage, final EnumOptionData chargeTimeType, final EnumOptionData chargeCalculationType,
|
||||
final Collection<ChargeData> chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
|
||||
final List<ChargeData> chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid,
|
||||
final boolean waived, final Long loanId, final ExternalId externalLoanId, final BigDecimal minCap, final BigDecimal maxCap,
|
||||
final BigDecimal amountOrPercentage, Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
final BigDecimal amountOrPercentage, List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
this.id = id;
|
||||
this.chargeId = chargeId;
|
||||
this.name = name;
|
||||
@@ -207,7 +207,7 @@ public class LoanChargeData {
|
||||
|
||||
public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final LocalDate submittedOnDate, final BigDecimal amountOutstanding,
|
||||
EnumOptionData chargeTimeType, final Long loanId, final ExternalId externalLoanId,
|
||||
Collection<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
List<LoanInstallmentChargeData> installmentChargeData, final ExternalId externalId) {
|
||||
this.id = id;
|
||||
this.chargeId = null;
|
||||
this.name = null;
|
||||
@@ -308,7 +308,7 @@ public class LoanChargeData {
|
||||
this.externalId = chargeData.externalId;
|
||||
}
|
||||
|
||||
public LoanChargeData(LoanChargeData chargeData, Collection<LoanInstallmentChargeData> installmentChargeData) {
|
||||
public LoanChargeData(LoanChargeData chargeData, List<LoanInstallmentChargeData> installmentChargeData) {
|
||||
this.id = chargeData.id;
|
||||
this.chargeId = chargeData.chargeId;
|
||||
this.name = chargeData.name;
|
||||
|
||||
+2
@@ -61,6 +61,7 @@ public class LoanTransactionEnumData {
|
||||
private final boolean reAmortize;
|
||||
private final boolean accrualActivity;
|
||||
private final boolean interestRefund;
|
||||
private final boolean accrualAdjustment;
|
||||
|
||||
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
||||
this.id = id;
|
||||
@@ -96,6 +97,7 @@ public class LoanTransactionEnumData {
|
||||
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
|
||||
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
|
||||
this.interestRefund = Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id);
|
||||
this.accrualAdjustment = Long.valueOf(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue()).equals(this.id);
|
||||
}
|
||||
|
||||
public boolean isRepaymentType() {
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
|
||||
|
||||
if (isNotLoanCreation(loanEvent)) {
|
||||
// in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan, oldStatus));
|
||||
}
|
||||
|
||||
// set mandatory field states based on new status after the transition
|
||||
|
||||
+83
-150
@@ -749,11 +749,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail reprocessTransactions() {
|
||||
ChangedTransactionDetail changedTransactionDetail;
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
|
||||
getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
@@ -778,10 +778,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) {
|
||||
final Money chargeAmount = loanCharge.getAmount(getCurrency());
|
||||
Money feeCharges = chargeAmount;
|
||||
Money penaltyCharges = Money.zero(loanCurrency());
|
||||
Money penaltyCharges = Money.zero(getCurrency());
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyCharges = chargeAmount;
|
||||
feeCharges = Money.zero(loanCurrency());
|
||||
feeCharges = Money.zero(getCurrency());
|
||||
}
|
||||
|
||||
LocalDate transactionDate;
|
||||
@@ -898,8 +898,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
removeOrModifyTransactionAssociatedWithLoanChargeIfDueAtDisbursement(loanCharge);
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) {
|
||||
/*
|
||||
* TODO Vishwas Currently we do not allow removing a loan charge after a loan is approved (hence there is no
|
||||
* need to adjust any loan transactions).
|
||||
@@ -907,9 +906,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
* Consider removing this block of code or logically completing it for the future by getting the list of
|
||||
* affected Transactions
|
||||
*/
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
reprocessTransactions();
|
||||
}
|
||||
this.charges.remove(loanCharge);
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -922,7 +919,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
for (final LoanTransaction transaction : transactions) {
|
||||
if (transaction.isRepaymentAtDisbursement()
|
||||
&& doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanCharge)) {
|
||||
final MonetaryCurrency currency = loanCurrency();
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
final Money chargeAmount = Money.of(currency, loanCharge.amount());
|
||||
if (transaction.isGreaterThan(chargeAmount)) {
|
||||
final Money principalPortion = Money.zero(currency);
|
||||
@@ -969,9 +966,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
* Consider removing this block of code or logically completing it for the future by getting the list of
|
||||
* affected Transactions
|
||||
*/
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
reprocessTransactions();
|
||||
} else {
|
||||
// reprocess loan schedule based on charge been waived.
|
||||
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
|
||||
@@ -1067,7 +1062,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
final ExternalId externalId) {
|
||||
validateLoanIsNotClosed(loanCharge);
|
||||
|
||||
final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber);
|
||||
final Money amountWaived = loanCharge.waive(getCurrency(), loanInstallmentNumber);
|
||||
changes.put("amount", amountWaived.getAmount());
|
||||
|
||||
Money unrecognizedIncome = amountWaived.zero();
|
||||
@@ -1089,10 +1084,10 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
}
|
||||
Money feeChargesWaived = chargeComponent;
|
||||
Money penaltyChargesWaived = Money.zero(loanCurrency());
|
||||
Money penaltyChargesWaived = Money.zero(getCurrency());
|
||||
if (loanCharge.isPenaltyCharge()) {
|
||||
penaltyChargesWaived = chargeComponent;
|
||||
feeChargesWaived = Money.zero(loanCurrency());
|
||||
feeChargesWaived = Money.zero(getCurrency());
|
||||
}
|
||||
|
||||
LocalDate transactionDate = getDisbursementDate();
|
||||
@@ -1132,7 +1127,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
// Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule
|
||||
// to be reprocessed.
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
|
||||
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) {
|
||||
/*
|
||||
* TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan
|
||||
* charges only waives the remaining amount.
|
||||
@@ -1140,9 +1135,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
* Consider removing this block of code or logically completing it for the future by getting the list of
|
||||
* affected Transactions
|
||||
*/
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
reprocessTransactions();
|
||||
} else {
|
||||
// reprocess loan schedule based on charge been waived.
|
||||
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
|
||||
@@ -1289,7 +1282,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
addLoanRepaymentScheduleInstallment(installment);
|
||||
}
|
||||
}
|
||||
|
||||
updateLoanScheduleDependentDerivedFields();
|
||||
updateLoanSummaryDerivedFields();
|
||||
}
|
||||
@@ -1310,7 +1302,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
updateLoanScheduleDependentDerivedFields();
|
||||
updateLoanSummaryDerivedFields();
|
||||
|
||||
}
|
||||
|
||||
private LoanRepaymentScheduleInstallment findByInstallmentNumber(Collection<LoanRepaymentScheduleInstallment> installments,
|
||||
@@ -1347,8 +1338,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.totalRecovered = recoveredAmount.getAmountDefaultedToNullIfZero();
|
||||
|
||||
final Money principal = this.loanRepaymentScheduleDetail.getPrincipal();
|
||||
this.summary.updateSummary(loanCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper,
|
||||
this.charges);
|
||||
this.summary.updateSummary(getCurrency(), principal, getRepaymentScheduleInstallments(), this.loanSummaryWrapper, this.charges);
|
||||
updateLoanOutstandingBalances();
|
||||
}
|
||||
}
|
||||
@@ -1923,7 +1913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) {
|
||||
repaymentPeriod.updateObligationsMet(loanCurrency(), actualDisbursementDate);
|
||||
repaymentPeriod.updateObligationsMet(getCurrency(), actualDisbursementDate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2002,7 +1992,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
* TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too?
|
||||
*/
|
||||
|
||||
final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(loanCurrency());
|
||||
final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(getCurrency());
|
||||
/*
|
||||
* all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other
|
||||
* fees ( which are created during disbursal but not repaid)
|
||||
@@ -2221,7 +2211,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
if (charge.isOverdueInstallmentCharge()) {
|
||||
charge.setActive(false);
|
||||
} else {
|
||||
charge.resetToOriginal(loanCurrency());
|
||||
charge.resetToOriginal(getCurrency());
|
||||
}
|
||||
}
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
@@ -2354,7 +2344,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
loanTransaction.updateLoan(this);
|
||||
|
||||
if (loanTransaction.isNotZero(loanCurrency())) {
|
||||
if (loanTransaction.isNotZero()) {
|
||||
addLoanTransaction(loanTransaction);
|
||||
}
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -2371,7 +2361,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
if (loanTransaction.isRecoveryRepayment()
|
||||
&& loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
|
||||
&& loanTransaction.getAmount(getCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
|
||||
final String errorMessage = "The transaction amount cannot greater than the remaining written off amount.";
|
||||
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage);
|
||||
}
|
||||
@@ -2380,7 +2370,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction);
|
||||
|
||||
if (loanTransaction.isNotZero(loanCurrency())) {
|
||||
if (loanTransaction.isNotZero()) {
|
||||
addLoanTransaction(loanTransaction);
|
||||
}
|
||||
|
||||
@@ -2400,12 +2390,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan();
|
||||
if (adjustedTransaction != null) {
|
||||
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loanCurrency()));
|
||||
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(getCurrency()));
|
||||
}
|
||||
if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
|
||||
if (loanTransaction.getAmount(getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
|
||||
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
|
||||
throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage,
|
||||
loanTransaction.getAmount(loanCurrency()), totalInterestOutstandingOnLoan.getAmount());
|
||||
loanTransaction.getAmount(getCurrency()), totalInterestOutstandingOnLoan.getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2450,17 +2440,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
&& !getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
}
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
/*
|
||||
* Commented since throwing exception if external id present for one of the transactions. for this need to
|
||||
* save the reversed transactions first and then new transactions.
|
||||
*/
|
||||
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
changedTransactionDetail = reprocessTransactions();
|
||||
}
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -2531,7 +2511,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else if (this.summary.isRepaidInFull(loanCurrency())) {
|
||||
} else if (this.summary.isRepaidInFull(getCurrency())) {
|
||||
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else {
|
||||
@@ -2645,7 +2625,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
LocalDate lastTransactionDate = null;
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if (transaction.isRepaymentLikeType() && transaction.isNonZero()) {
|
||||
if (transaction.isRepaymentLikeType() && transaction.isGreaterThanZero()) {
|
||||
lastTransactionDate = transaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
@@ -2669,7 +2649,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
|
||||
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
|
||||
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency());
|
||||
if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate()) && scheduledRepayment.isNotFullyPaidOff()
|
||||
&& outstandingForPeriod.isGreaterThanZero()) {
|
||||
transactionDate = scheduledRepayment.getDueDate();
|
||||
@@ -2692,16 +2672,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
final LoanTransaction writeOffTransaction = findWriteOffTransaction();
|
||||
writeOffTransaction.reverse();
|
||||
loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, this);
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
}
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
|
||||
getActiveCharges());
|
||||
updateLoanSummaryDerivedFields();
|
||||
return changedTransactionDetail;
|
||||
return reprocessTransactions();
|
||||
}
|
||||
|
||||
public LoanTransaction findWriteOffTransaction() {
|
||||
@@ -2718,7 +2689,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
private Money calculateTotalOverpayment() {
|
||||
Money totalPaidInRepayments = getTotalPaidInRepayments();
|
||||
|
||||
final MonetaryCurrency currency = loanCurrency();
|
||||
final MonetaryCurrency currency = getCurrency();
|
||||
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
|
||||
Money cumulativeTotalWaivedOnInstallments = Money.zero(currency);
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
@@ -2759,10 +2730,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getTotalRecoveredPayments();
|
||||
}
|
||||
|
||||
public MonetaryCurrency loanCurrency() {
|
||||
return this.loanRepaymentScheduleDetail.getCurrency();
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine,
|
||||
final Map<String, Object> changes, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
|
||||
final AppUser currentUser, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
|
||||
@@ -2822,7 +2789,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -2844,14 +2811,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
|
||||
}
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
addLoanTransaction(mapEntry.getValue());
|
||||
}
|
||||
updateLoanSummaryDerivedFields();
|
||||
changedTransactionDetail = reprocessTransactions();
|
||||
LocalDate lastLoanTransactionDate = getLatestTransactionDate();
|
||||
doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine);
|
||||
}
|
||||
@@ -2904,7 +2864,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
LoanTransaction loanTransaction = null;
|
||||
if (isOpen()) {
|
||||
final Money totalOutstanding = this.summary.getTotalOutstanding(loanCurrency());
|
||||
final Money totalOutstanding = this.summary.getTotalOutstanding(getCurrency());
|
||||
if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) {
|
||||
|
||||
this.closedOnDate = closureDate;
|
||||
@@ -2923,7 +2883,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
addLoanTransaction(loanTransaction);
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(loanCurrency(),
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -3123,11 +3083,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
private Money getTotalPaidInRepayments() {
|
||||
Money cumulativePaid = Money.zero(loanCurrency());
|
||||
Money cumulativePaid = Money.zero(getCurrency());
|
||||
|
||||
for (final LoanTransaction repayment : this.loanTransactions) {
|
||||
if (repayment.isRepaymentLikeType() && !repayment.isReversed()) {
|
||||
cumulativePaid = cumulativePaid.plus(repayment.getAmount(loanCurrency()));
|
||||
cumulativePaid = cumulativePaid.plus(repayment.getAmount(getCurrency()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3135,11 +3095,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public Money getTotalRecoveredPayments() {
|
||||
Money cumulativePaid = Money.zero(loanCurrency());
|
||||
Money cumulativePaid = Money.zero(getCurrency());
|
||||
|
||||
for (final LoanTransaction recoveredPayment : this.loanTransactions) {
|
||||
if (recoveredPayment.isRecoveryRepayment()) {
|
||||
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(loanCurrency()));
|
||||
cumulativePaid = cumulativePaid.plus(recoveredPayment.getAmount(getCurrency()));
|
||||
}
|
||||
}
|
||||
return cumulativePaid;
|
||||
@@ -3148,16 +3108,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public Money getTotalPrincipalOutstandingUntil(LocalDate date) {
|
||||
return getRepaymentScheduleInstallments().stream()
|
||||
.filter(installment -> installment.getDueDate().isBefore(date) || installment.getDueDate().isEqual(date))
|
||||
.map(installment -> installment.getPrincipalOutstanding(loanCurrency())).reduce(Money.zero(loanCurrency()), Money::add);
|
||||
.map(installment -> installment.getPrincipalOutstanding(getCurrency())).reduce(Money.zero(getCurrency()), Money::add);
|
||||
|
||||
}
|
||||
|
||||
private Money getTotalInterestOutstandingOnLoan() {
|
||||
Money cumulativeInterest = Money.zero(loanCurrency());
|
||||
Money cumulativeInterest = Money.zero(getCurrency());
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
|
||||
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency()));
|
||||
}
|
||||
|
||||
return cumulativeInterest;
|
||||
@@ -3169,7 +3129,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) {
|
||||
|
||||
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
|
||||
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(getCurrency());
|
||||
if (scheduledRepayment.isOverdueOn(DateUtils.getBusinessLocalDate())) {
|
||||
cumulativeInterestOverdue = cumulativeInterestOverdue.plus(interestOutstandingForPeriod);
|
||||
}
|
||||
@@ -3515,7 +3475,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
&& !DateUtils.isAfter(transaction.getTransactionDate(), tillDate)) {
|
||||
if (transaction.isAccrual()) {
|
||||
receivableInterest = receivableInterest.plus(transaction.getInterestPortion(getCurrency()));
|
||||
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver()) {
|
||||
} else if (transaction.isRepaymentLikeType() || transaction.isInterestWaiver() || transaction.isAccrualAdjustment()) {
|
||||
receivableInterest = receivableInterest.minus(transaction.getInterestPortion(getCurrency()));
|
||||
}
|
||||
}
|
||||
@@ -3841,8 +3801,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public LocalDate getLastUserTransactionDate() {
|
||||
LocalDate currentTransactionDate = getDisbursementDate();
|
||||
for (final LoanTransaction previousTransaction : this.loanTransactions) {
|
||||
if (!(previousTransaction.isReversed() || previousTransaction.isAccrual() || previousTransaction.isIncomePosting()
|
||||
|| previousTransaction.isAccrualActivity())
|
||||
if (!(previousTransaction.isReversed() || previousTransaction.isAccrualRelated() || previousTransaction.isIncomePosting())
|
||||
&& DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())) {
|
||||
currentTransactionDate = previousTransaction.getTransactionDate();
|
||||
}
|
||||
@@ -4131,17 +4090,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
regenerateRepaymentSchedule(scheduleGeneratorDTO);
|
||||
}
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
|
||||
getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
addLoanTransaction(mapEntry.getValue());
|
||||
}
|
||||
|
||||
return changedTransactionDetail;
|
||||
return reprocessTransactions();
|
||||
}
|
||||
|
||||
public BigDecimal getPrincipalAmountForRepaymentSchedule() {
|
||||
@@ -4219,7 +4168,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
} else {
|
||||
regenerateRepaymentSchedule(generatorDTO);
|
||||
}
|
||||
return processTransactions();
|
||||
return reprocessTransactions();
|
||||
|
||||
}
|
||||
|
||||
@@ -4229,33 +4178,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
} else {
|
||||
regenerateRepaymentSchedule(generatorDTO);
|
||||
}
|
||||
return processTransactions();
|
||||
return reprocessTransactions();
|
||||
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
|
||||
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
|
||||
return processTransactions();
|
||||
|
||||
}
|
||||
|
||||
public ChangedTransactionDetail processTransactions() {
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor();
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
|
||||
getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
/*
|
||||
* Commented since throwing exception if external id present for one of the transactions. for this need to save
|
||||
* the reversed transactions first and then new transactions.
|
||||
*/
|
||||
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
updateLoanSummaryDerivedFields();
|
||||
|
||||
return changedTransactionDetail;
|
||||
return reprocessTransactions();
|
||||
}
|
||||
|
||||
public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) {
|
||||
@@ -4417,17 +4346,17 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
private OutstandingAmountsDTO getTotalOutstandingOnLoan() {
|
||||
Money totalPrincipal = Money.zero(loanCurrency());
|
||||
Money totalInterest = Money.zero(loanCurrency());
|
||||
Money feeCharges = Money.zero(loanCurrency());
|
||||
Money penaltyCharges = Money.zero(loanCurrency());
|
||||
Money totalPrincipal = Money.zero(getCurrency());
|
||||
Money totalInterest = Money.zero(getCurrency());
|
||||
Money feeCharges = Money.zero(getCurrency());
|
||||
Money penaltyCharges = Money.zero(getCurrency());
|
||||
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
|
||||
List<LoanRepaymentScheduleInstallment> repaymentSchedule = getRepaymentScheduleInstallments();
|
||||
for (final LoanRepaymentScheduleInstallment scheduledRepayment : repaymentSchedule) {
|
||||
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(loanCurrency()));
|
||||
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
|
||||
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(loanCurrency()));
|
||||
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(loanCurrency()));
|
||||
totalPrincipal = totalPrincipal.plus(scheduledRepayment.getPrincipalOutstanding(getCurrency()));
|
||||
totalInterest = totalInterest.plus(scheduledRepayment.getInterestOutstanding(getCurrency()));
|
||||
feeCharges = feeCharges.plus(scheduledRepayment.getFeeChargesOutstanding(getCurrency()));
|
||||
penaltyCharges = penaltyCharges.plus(scheduledRepayment.getPenaltyChargesOutstanding(getCurrency()));
|
||||
}
|
||||
return new OutstandingAmountsDTO(totalPrincipal.getCurrency()).principal(totalPrincipal).interest(totalInterest)
|
||||
.feeCharges(feeCharges).penaltyCharges(penaltyCharges);
|
||||
@@ -4533,6 +4462,16 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return getRepaymentScheduleInstallments().stream().filter(predicate).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param predicate
|
||||
* filter of the installments
|
||||
* @return the installments matching the filter
|
||||
**/
|
||||
public List<LoanRepaymentScheduleInstallment> getRepaymentScheduleInstallments(
|
||||
@NotNull Predicate<LoanRepaymentScheduleInstallment> predicate) {
|
||||
return getRepaymentScheduleInstallments().stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return loan disbursement data
|
||||
**/
|
||||
@@ -4691,17 +4630,18 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.createStandingInstructionAtDisbursement != null && this.createStandingInstructionAtDisbursement;
|
||||
}
|
||||
|
||||
public Collection<LoanCharge> getLoanCharges(LocalDate dueDate) {
|
||||
Collection<LoanCharge> loanCharges = new ArrayList<>();
|
||||
public List<LoanCharge> getLoanChargesByDueDate(LocalDate dueDate) {
|
||||
return getLoanCharges(
|
||||
loanCharge -> loanCharge.getDueLocalDate() != null && DateUtils.isEqual(loanCharge.getDueLocalDate(), dueDate));
|
||||
}
|
||||
|
||||
for (LoanCharge loanCharge : charges) {
|
||||
|
||||
if (loanCharge.getDueLocalDate() != null && loanCharge.getDueLocalDate().equals(dueDate)) {
|
||||
loanCharges.add(loanCharge);
|
||||
}
|
||||
}
|
||||
|
||||
return loanCharges;
|
||||
/**
|
||||
* @param predicate
|
||||
* filter of the charges
|
||||
* @return the loan charges matching the filter
|
||||
**/
|
||||
public List<LoanCharge> getLoanCharges(@NotNull Predicate<LoanCharge> predicate) {
|
||||
return getLoanCharges().stream().filter(predicate).toList();
|
||||
}
|
||||
|
||||
public void setGuaranteeAmount(BigDecimal guaranteeAmountDerived) {
|
||||
@@ -4796,7 +4736,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage);
|
||||
}
|
||||
|
||||
if (loanTransaction.isNotZero(loanCurrency())) {
|
||||
if (loanTransaction.isNotZero()) {
|
||||
addLoanTransaction(loanTransaction);
|
||||
}
|
||||
if (loanTransaction.isNotRefundForActiveLoan()) {
|
||||
@@ -4828,13 +4768,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(),
|
||||
getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null));
|
||||
} else {
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(this);
|
||||
}
|
||||
|
||||
changedTransactionDetail = reprocessTransactions();
|
||||
}
|
||||
|
||||
updateLoanSummaryDerivedFields();
|
||||
@@ -4869,7 +4803,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
LocalDate lastTransactionDate = null;
|
||||
for (final LoanTransaction transaction : this.loanTransactions) {
|
||||
if ((transaction.isRepaymentLikeType() || transaction.isRefundForActiveLoan() || transaction.isCreditBalanceRefund())
|
||||
&& transaction.isNonZero() && transaction.isNotReversed()) {
|
||||
&& transaction.isGreaterThanZero() && transaction.isNotReversed()) {
|
||||
lastTransactionDate = transaction.getTransactionDate();
|
||||
}
|
||||
}
|
||||
@@ -4979,7 +4913,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
charge.setActive(false);
|
||||
} else if (charge.isTrancheDisbursementCharge() && disbursementDetail.getDisbursementDate()
|
||||
.equals(charge.getTrancheDisbursementCharge().getloanDisbursementDetails().actualDisbursementDate())) {
|
||||
charge.resetToOriginal(loanCurrency());
|
||||
charge.resetToOriginal(getCurrency());
|
||||
}
|
||||
}
|
||||
this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount().subtract(disbursementDetail.principal()));
|
||||
@@ -5211,7 +5145,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public void validateForForeclosure(final LocalDate transactionDate) {
|
||||
if (isInterestRecalculationEnabledForProduct()) {
|
||||
if (getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
|
||||
final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed.";
|
||||
throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage,
|
||||
getId());
|
||||
@@ -5461,8 +5395,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
public LoanTransaction getLastUserTransaction() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isNotReversed) //
|
||||
.filter(t -> !(t.isAccrualTransaction() || t.isIncomePosting())) //
|
||||
.filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) //
|
||||
.reduce((first, second) -> second) //
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
+17
-53
@@ -28,6 +28,7 @@ import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -40,7 +41,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
@@ -58,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInstallmentChargeData;
|
||||
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "m_loan_charge", uniqueConstraints = { @UniqueConstraint(columnNames = { "external_id" }, name = "external_id") })
|
||||
public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
@@ -305,10 +308,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
}
|
||||
|
||||
public BigDecimal getAmountPercentageAppliedTo() {
|
||||
return this.amountPercentageAppliedTo;
|
||||
}
|
||||
|
||||
private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) {
|
||||
return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount();
|
||||
}
|
||||
@@ -509,15 +508,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public LocalDate getDueLocalDate() {
|
||||
return this.dueDate;
|
||||
}
|
||||
|
||||
public LocalDate getDueDate() {
|
||||
return this.dueDate;
|
||||
}
|
||||
|
||||
public LocalDate getSubmittedOnDate() {
|
||||
return submittedOnDate;
|
||||
return this.dueDate; // TODO delete duplicated method
|
||||
}
|
||||
|
||||
private boolean determineIfFullyPaid() {
|
||||
@@ -597,11 +588,11 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public BigDecimal amount() {
|
||||
return this.amount;
|
||||
return this.amount; // TODO delete duplicated method
|
||||
}
|
||||
|
||||
public BigDecimal amountOutstanding() {
|
||||
return this.amountOutstanding;
|
||||
return this.amountOutstanding; // TODO delete duplicated method
|
||||
}
|
||||
|
||||
public Money getAmountOutstanding(final MonetaryCurrency currency) {
|
||||
@@ -644,14 +635,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.waived;
|
||||
}
|
||||
|
||||
public BigDecimal getMinCap() {
|
||||
return this.minCap;
|
||||
}
|
||||
|
||||
public BigDecimal getMaxCap() {
|
||||
return this.maxCap;
|
||||
}
|
||||
|
||||
public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) {
|
||||
|
||||
final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency));
|
||||
@@ -729,10 +712,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return this.charge.getCurrencyCode();
|
||||
}
|
||||
|
||||
public Charge getCharge() {
|
||||
return this.charge;
|
||||
}
|
||||
|
||||
/*
|
||||
* @Override public boolean equals(final Object obj) { if (obj == null) { return false; } if (obj == this) { return
|
||||
* true; } if (obj.getClass() != getClass()) { return false; } final LoanCharge rhs = (LoanCharge) obj; return new
|
||||
@@ -756,10 +735,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return ChargeCalculationType.fromInt(this.chargeCalculation);
|
||||
}
|
||||
|
||||
public BigDecimal getPercentage() {
|
||||
return this.percentage;
|
||||
}
|
||||
|
||||
public void updateAmount(final BigDecimal amount) {
|
||||
this.amount = amount;
|
||||
calculateOutstanding();
|
||||
@@ -851,7 +826,7 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
}
|
||||
|
||||
public BigDecimal amountOrPercentage() {
|
||||
return this.amountOrPercentage;
|
||||
return this.amountOrPercentage; // TODO delete duplicated method
|
||||
}
|
||||
|
||||
public BigDecimal chargeAmount() {
|
||||
@@ -906,12 +881,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
}
|
||||
|
||||
public LoanOverdueInstallmentCharge getOverdueInstallmentCharge() {
|
||||
return this.overdueInstallmentCharge;
|
||||
}
|
||||
|
||||
public LoanTrancheDisbursementCharge getTrancheDisbursementCharge() {
|
||||
return this.loanTrancheDisbursementCharge;
|
||||
return this.loanTrancheDisbursementCharge; // TODO delete duplicated method
|
||||
}
|
||||
|
||||
public Money undoPaidOrPartiallyAmountBy(final Money incrementBy, final Integer installmentNumber, final Money feeAmount) {
|
||||
@@ -960,14 +931,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return paidChargePerInstallment;
|
||||
}
|
||||
|
||||
public Set<LoanChargePaidBy> getLoanChargePaidBySet() {
|
||||
return this.loanChargePaidBySet;
|
||||
}
|
||||
|
||||
public Loan getLoan() {
|
||||
return this.loan;
|
||||
}
|
||||
|
||||
public boolean isDisbursementCharge() {
|
||||
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT);
|
||||
}
|
||||
@@ -988,10 +951,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
this.waived = false;
|
||||
}
|
||||
|
||||
public ExternalId getExternalId() {
|
||||
return externalId;
|
||||
}
|
||||
|
||||
public ChargeTimeType getChargeTimeType() {
|
||||
return ChargeTimeType.fromInt(this.chargeTime);
|
||||
}
|
||||
@@ -1013,6 +972,11 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
return dueDate;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<LoanChargePaidBy> getLoanChargePaidBy(@NotNull Predicate<LoanChargePaidBy> filter) {
|
||||
return getLoanChargePaidBySet().stream().filter(filter).toList();
|
||||
}
|
||||
|
||||
public LoanChargeData toData() {
|
||||
EnumOptionData chargeTimeTypeData = new EnumOptionData((long) getChargeTimeType().ordinal(), getChargeTimeType().getCode(),
|
||||
String.valueOf(getChargeTimeType().getValue()));
|
||||
@@ -1020,8 +984,8 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
getChargeCalculation().getCode(), String.valueOf(getChargeCalculation().getValue()));
|
||||
EnumOptionData chargePaymentModeData = new EnumOptionData((long) getChargePaymentMode().ordinal(), getChargePaymentMode().getCode(),
|
||||
String.valueOf(getChargePaymentMode().getValue()));
|
||||
Set<LoanInstallmentChargeData> loanInstallmentChargeDataSet = installmentCharges().stream().map(LoanInstallmentCharge::toData)
|
||||
.collect(Collectors.toSet());
|
||||
List<LoanInstallmentChargeData> loanInstallmentChargeDataList = installmentCharges().stream().map(LoanInstallmentCharge::toData)
|
||||
.toList();
|
||||
|
||||
return LoanChargeData.builder().id(getId()).chargeId(getCharge().getId()).name(getCharge().getName())
|
||||
.currency(getCharge().toData().getCurrency()).amount(amount).amountPaid(amountPaid).amountWaived(amountWaived)
|
||||
@@ -1029,6 +993,6 @@ public class LoanCharge extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
.submittedOnDate(submittedOnDate).dueDate(dueDate).chargeCalculationType(chargeCalculationTypeData).percentage(percentage)
|
||||
.amountPercentageAppliedTo(amountPercentageAppliedTo).amountOrPercentage(amountOrPercentage).penalty(penaltyCharge)
|
||||
.chargePaymentMode(chargePaymentModeData).paid(paid).waived(waived).loanId(loan.getId()).minCap(minCap).maxCap(maxCap)
|
||||
.installmentChargeData(loanInstallmentChargeDataSet).externalId(externalId).build();
|
||||
.installmentChargeData(loanInstallmentChargeDataList).externalId(externalId).build();
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -188,7 +188,7 @@ public class LoanInterestRecalculationDetails extends AbstractPersistableCustom<
|
||||
}
|
||||
|
||||
public boolean isCompoundingToBePostedAsTransaction() {
|
||||
return null == this.isCompoundingToBePostedAsTransaction ? false : this.isCompoundingToBePostedAsTransaction;
|
||||
return this.isCompoundingToBePostedAsTransaction != null && this.isCompoundingToBePostedAsTransaction;
|
||||
}
|
||||
|
||||
public boolean allowCompoundingOnEod() {
|
||||
|
||||
+19
-15
@@ -34,6 +34,7 @@ import java.util.Set;
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
@@ -692,29 +693,32 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableWithUTCDa
|
||||
|
||||
public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
|
||||
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
|
||||
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.getAmount());
|
||||
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.getAmount());
|
||||
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.getAmount());
|
||||
this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.getAmount());
|
||||
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount());
|
||||
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount());
|
||||
this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesDue));
|
||||
this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWaived));
|
||||
this.feeChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeChargesWrittenOff));
|
||||
this.penaltyCharges = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesDue));
|
||||
this.penaltyChargesWaived = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWaived));
|
||||
this.penaltyChargesWrittenOff = MathUtil.zeroToNull(MathUtil.toBigDecimal(penaltyChargesWrittenOff));
|
||||
}
|
||||
|
||||
public void addToChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
|
||||
final Money penaltyChargesDue, final Money penaltyChargesWaived, final Money penaltyChargesWrittenOff) {
|
||||
this.feeChargesCharged = defaultToNullIfZero(feeChargesDue.plus(this.feeChargesCharged).getAmount());
|
||||
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.plus(this.feeChargesWaived).getAmount());
|
||||
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.plus(this.feeChargesWrittenOff).getAmount());
|
||||
this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.plus(this.penaltyCharges).getAmount());
|
||||
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.plus(this.penaltyChargesWaived).getAmount());
|
||||
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.plus(this.penaltyChargesWrittenOff).getAmount());
|
||||
this.feeChargesCharged = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesDue), this.feeChargesCharged));
|
||||
this.feeChargesWaived = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWaived), this.feeChargesWaived));
|
||||
this.feeChargesWrittenOff = MathUtil
|
||||
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(feeChargesWrittenOff), this.feeChargesWrittenOff));
|
||||
this.penaltyCharges = MathUtil.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesDue), this.penaltyCharges));
|
||||
this.penaltyChargesWaived = MathUtil
|
||||
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWaived), this.penaltyChargesWaived));
|
||||
this.penaltyChargesWrittenOff = MathUtil
|
||||
.zeroToNull(MathUtil.add(MathUtil.toBigDecimal(penaltyChargesWrittenOff), this.penaltyChargesWrittenOff));
|
||||
checkIfRepaymentPeriodObligationsAreMet(getObligationsMetOnDate(), feeChargesDue.getCurrency());
|
||||
}
|
||||
|
||||
public void updateAccrualPortion(final Money interest, final Money feeCharges, final Money penalityCharges) {
|
||||
this.interestAccrued = defaultToNullIfZero(interest.getAmount());
|
||||
this.feeAccrued = defaultToNullIfZero(feeCharges.getAmount());
|
||||
this.penaltyAccrued = defaultToNullIfZero(penalityCharges.getAmount());
|
||||
this.interestAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(interest));
|
||||
this.feeAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(feeCharges));
|
||||
this.penaltyAccrued = MathUtil.zeroToNull(MathUtil.toBigDecimal(penalityCharges));
|
||||
}
|
||||
|
||||
public void updateObligationsMet(final MonetaryCurrency currency, final LocalDate transactionDate) {
|
||||
|
||||
+9
@@ -253,6 +253,15 @@ public class LoanRepaymentScheduleProcessingWrapper {
|
||||
: DateUtils.isDateInRangeFromExclusiveToInclusive(targetDate, fromDate, toDate);
|
||||
}
|
||||
|
||||
public static boolean isBeforePeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment, boolean isFirstPeriod) {
|
||||
LocalDate fromDate = installment.getFromDate();
|
||||
return isFirstPeriod ? DateUtils.isBefore(targetDate, fromDate) : !DateUtils.isAfter(targetDate, fromDate);
|
||||
}
|
||||
|
||||
public static boolean isAfterPeriod(LocalDate targetDate, LoanRepaymentScheduleInstallment installment) {
|
||||
return DateUtils.isAfter(targetDate, installment.getDueDate());
|
||||
}
|
||||
|
||||
public static Optional<LoanRepaymentScheduleInstallment> findInPeriod(LocalDate targetDate,
|
||||
List<LoanRepaymentScheduleInstallment> installments) {
|
||||
int firstNumber = fetchFirstNormalInstallmentNumber(installments);
|
||||
|
||||
+20
@@ -101,6 +101,18 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
||||
|
||||
String FIND_ALL_LOAN_IDS_BY_STATUS_ID = "SELECT loan.id FROM Loan loan WHERE loan.loanStatus = :statusId";
|
||||
|
||||
String LOANS_FOR_ACCRUAL = "select l from Loan l left join l.loanInterestRecalculationDetails recalcDetails "
|
||||
+ "where l.loanStatus = 300 and l.isNpa = false and l.chargedOff = false "
|
||||
+ "and l.loanProduct.accountingRule = :accountingType "
|
||||
+ "and (recalcDetails.isCompoundingToBePostedAsTransaction is null or recalcDetails.isCompoundingToBePostedAsTransaction = false) "
|
||||
+ "and (exists (select ls.id from LoanRepaymentScheduleInstallment ls where ls.loan.id = l.id and ls.isDownPayment = false "
|
||||
+ "and ((coalesce(ls.interestCharged, 0) - coalesce(ls.interestWaived, 0)) <> coalesce(ls.interestAccrued, 0) "
|
||||
+ "or (coalesce(ls.feeChargesCharged, 0) - coalesce(ls.feeChargesWaived, 0)) <> coalesce(ls.feeAccrued, 0) "
|
||||
+ "or (coalesce(ls.penaltyCharges, 0) - coalesce(ls.penaltyChargesWaived, 0)) <> coalesce(ls.penaltyAccrued, 0)) ";
|
||||
String FIND_LOANS_FOR_PERIODIC_ACCRUAL = LOANS_FOR_ACCRUAL
|
||||
+ "and (:futureCharges = true or ls.fromDate < :tillDate or (ls.installmentNumber = (select min(lsi.installmentNumber) from LoanRepaymentScheduleInstallment lsi where lsi.loan.id = l.id and lsi.isDownPayment = false) and ls.fromDate = :tillDate))))";
|
||||
String FIND_LOANS_FOR_ADD_ACCRUAL = LOANS_FOR_ACCRUAL + "and (:futureCharges = true or ls.dueDate <= :tillDate)))";
|
||||
|
||||
@Query(FIND_GROUP_LOANS_DISBURSED_AFTER)
|
||||
List<Loan> getGroupLoansDisbursedAfter(@Param("disbursementDate") LocalDate disbursementDate, @Param("groupId") Long groupId,
|
||||
@Param("loanType") Integer loanType);
|
||||
@@ -227,4 +239,12 @@ public interface LoanRepository extends JpaRepository<Loan, Long>, JpaSpecificat
|
||||
|
||||
@Query(FIND_ALL_LOAN_IDS_BY_STATUS_ID)
|
||||
List<Long> findLoanIdByStatusId(@Param("statusId") Integer statusId);
|
||||
|
||||
@Query(FIND_LOANS_FOR_PERIODIC_ACCRUAL)
|
||||
List<Loan> findLoansForPeriodicAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
|
||||
@Param("futureCharges") boolean futureCharges);
|
||||
|
||||
@Query(FIND_LOANS_FOR_ADD_ACCRUAL)
|
||||
List<Loan> findLoansForAddAccrual(@Param("accountingType") Integer accountingType, @Param("tillDate") LocalDate tillDate,
|
||||
@Param("futureCharges") boolean futureCharges);
|
||||
}
|
||||
|
||||
+8
@@ -263,4 +263,12 @@ public class LoanRepositoryWrapper {
|
||||
return repository.findLoanIdByStatusId(statusId);
|
||||
}
|
||||
|
||||
public List<Loan> findLoansForPeriodicAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) {
|
||||
return repository.findLoansForPeriodicAccrual(accountingType, tillDate, futureCharges);
|
||||
}
|
||||
|
||||
public List<Loan> findLoansForAddAccrual(Integer accountingType, LocalDate tillDate, boolean futureCharges) {
|
||||
return repository.findLoansForAddAccrual(accountingType, tillDate, futureCharges);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+69
-91
@@ -252,26 +252,29 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
|
||||
public static LoanTransaction accrueInterest(final Office office, final Loan loan, final Money amount,
|
||||
final LocalDate interestAppliedDate, final ExternalId externalId) {
|
||||
BigDecimal principalPortion = null;
|
||||
BigDecimal feesPortion = null;
|
||||
BigDecimal penaltiesPortion = null;
|
||||
BigDecimal interestPortion = amount.getAmount();
|
||||
BigDecimal overPaymentPortion = null;
|
||||
boolean reversed = false;
|
||||
PaymentDetail paymentDetail = null;
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), interestAppliedDate, interestPortion,
|
||||
principalPortion, interestPortion, feesPortion, penaltiesPortion, overPaymentPortion, reversed, paymentDetail, externalId);
|
||||
return accrueTransaction(loan, office, interestAppliedDate, interestPortion, interestPortion, null, null, externalId);
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
||||
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
|
||||
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
|
||||
externalId);
|
||||
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
|
||||
return applyCharge;
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueTransaction(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
|
||||
final BigDecimal interestPortion, final BigDecimal feeChargesPortion, final BigDecimal penaltyChargesPortion,
|
||||
final ExternalId externalId) {
|
||||
BigDecimal principalPortion = null;
|
||||
BigDecimal overPaymentPortion = null;
|
||||
boolean reversed = false;
|
||||
PaymentDetail paymentDetail = null;
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, principalPortion, interestPortion,
|
||||
feeChargesPortion, penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId);
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL.getValue(), dateOf, amount, null, interestPortion,
|
||||
feeChargesPortion, penaltyChargesPortion, null, false, null, externalId);
|
||||
}
|
||||
|
||||
public static LoanTransaction accrualAdjustment(final Loan loan, final Office office, final LocalDate dateOf, final BigDecimal amount,
|
||||
final BigDecimal interestPortion, final BigDecimal feePortion, final BigDecimal penaltyPortion, final ExternalId externalId) {
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue(), dateOf, amount, null, interestPortion,
|
||||
feePortion, penaltyPortion, null, false, null, externalId);
|
||||
}
|
||||
|
||||
public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate,
|
||||
@@ -325,14 +328,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate,
|
||||
final Money feeCharges, final Money penaltyCharges, final ExternalId externalId) {
|
||||
final LoanTransaction applyCharge = new LoanTransaction(loan, office, LoanTransactionType.ACCRUAL, amount.getAmount(), applyDate,
|
||||
externalId);
|
||||
applyCharge.updateChargesComponents(feeCharges, penaltyCharges);
|
||||
return applyCharge;
|
||||
}
|
||||
|
||||
public static LoanTransaction creditBalanceRefund(final Loan loan, final Office office, final Money amount, final LocalDate paymentDate,
|
||||
final ExternalId externalId, PaymentDetail paymentDetail) {
|
||||
return new LoanTransaction(loan, office, LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate, amount.getAmount(),
|
||||
@@ -492,47 +487,41 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
* penaltyCharges
|
||||
*/
|
||||
public void updateComponents(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = principal.getCurrency();
|
||||
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal).getAmount());
|
||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount());
|
||||
this.principalPortion = MathUtil.zeroToNull(MathUtil.add(getPrincipalPortion(), MathUtil.toBigDecimal(principal)));
|
||||
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest)));
|
||||
updateChargesComponents(feeCharges, penaltyCharges);
|
||||
}
|
||||
|
||||
public void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
|
||||
final MonetaryCurrency currency = feeCharges.getCurrency();
|
||||
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount());
|
||||
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount());
|
||||
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges)));
|
||||
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges)));
|
||||
}
|
||||
|
||||
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges, final Money unrecognizedCharges) {
|
||||
final MonetaryCurrency currency = feeCharges.getCurrency();
|
||||
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount());
|
||||
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount());
|
||||
this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedCharges).getAmount());
|
||||
this.feeChargesPortion = MathUtil.zeroToNull(MathUtil.add(getFeeChargesPortion(), MathUtil.toBigDecimal(feeCharges)));
|
||||
this.penaltyChargesPortion = MathUtil.zeroToNull(MathUtil.add(getPenaltyChargesPortion(), MathUtil.toBigDecimal(penaltyCharges)));
|
||||
this.unrecognizedIncomePortion = MathUtil
|
||||
.zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedCharges)));
|
||||
}
|
||||
|
||||
private void updateInterestComponent(final Money interest, final Money unrecognizedInterest) {
|
||||
final MonetaryCurrency currency = interest.getCurrency();
|
||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount());
|
||||
this.unrecognizedIncomePortion = defaultToNullIfZero(getUnrecognizedIncomePortion(currency).plus(unrecognizedInterest).getAmount());
|
||||
this.interestPortion = MathUtil.zeroToNull(MathUtil.add(getInterestPortion(), MathUtil.toBigDecimal(interest)));
|
||||
this.unrecognizedIncomePortion = MathUtil
|
||||
.zeroToNull(MathUtil.add(getUnrecognizedIncomePortion(), MathUtil.toBigDecimal(unrecognizedInterest)));
|
||||
}
|
||||
|
||||
public void adjustInterestComponent(final MonetaryCurrency currency) {
|
||||
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).minus(getUnrecognizedIncomePortion(currency)).getAmount());
|
||||
public void adjustInterestComponent() {
|
||||
this.interestPortion = MathUtil.zeroToNull(MathUtil.subtract(getInterestPortion(), getUnrecognizedIncomePortion()));
|
||||
}
|
||||
|
||||
public void updateComponentsAndTotal(final Money principal, final Money interest, final Money feeCharges, final Money penaltyCharges) {
|
||||
updateComponents(principal, interest, feeCharges, penaltyCharges);
|
||||
|
||||
final MonetaryCurrency currency = principal.getCurrency();
|
||||
this.amount = getPrincipalPortion(currency).plus(getInterestPortion(currency)).plus(getFeeChargesPortion(currency))
|
||||
.plus(getPenaltyChargesPortion(currency)).getAmount();
|
||||
this.amount = MathUtil
|
||||
.nullToZero(MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion()));
|
||||
}
|
||||
|
||||
public void setOverPayments(final Money overPayment) {
|
||||
if (overPayment != null) {
|
||||
this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount());
|
||||
}
|
||||
this.overPaymentPortion = MathUtil.zeroToNull(MathUtil.toBigDecimal(overPayment));
|
||||
}
|
||||
|
||||
public Money getPrincipalPortion(final MonetaryCurrency currency) {
|
||||
@@ -720,48 +709,20 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return getTypeOf().isReAmortize() && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isAccrualActivity() {
|
||||
return getTypeOf().isAccrualActivity();
|
||||
}
|
||||
|
||||
public boolean isIdentifiedBy(final Long identifier) {
|
||||
return getId().equals(identifier);
|
||||
}
|
||||
|
||||
public boolean isBelongingToLoanOf(final Loan check) {
|
||||
return this.loan.getId().equals(check.getId());
|
||||
}
|
||||
|
||||
public boolean isNotBelongingToLoanOf(final Loan check) {
|
||||
return !isBelongingToLoanOf(check);
|
||||
}
|
||||
|
||||
public boolean isNonZero() {
|
||||
return this.amount.subtract(BigDecimal.ZERO).doubleValue() > 0;
|
||||
}
|
||||
|
||||
public boolean isGreaterThan(final Money monetaryAmount) {
|
||||
return getAmount(monetaryAmount.getCurrency()).isGreaterThan(monetaryAmount);
|
||||
return MathUtil.isGreaterThan(amount, MathUtil.toBigDecimal(monetaryAmount));
|
||||
}
|
||||
|
||||
public boolean isGreaterThanZero(final MonetaryCurrency currency) {
|
||||
return getAmount(currency).isGreaterThanZero();
|
||||
public boolean isGreaterThanZero() {
|
||||
return MathUtil.isGreaterThanZero(amount);
|
||||
}
|
||||
|
||||
public boolean isGreaterThanZeroAndLessThanOrEqualTo(BigDecimal totalOverpaid) {
|
||||
return isNonZero() && this.amount.compareTo(totalOverpaid) <= 0;
|
||||
return isGreaterThanZero() && !MathUtil.isGreaterThan(amount, totalOverpaid);
|
||||
}
|
||||
|
||||
public boolean isNotZero(final MonetaryCurrency currency) {
|
||||
return !getAmount(currency).isZero();
|
||||
}
|
||||
|
||||
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
|
||||
BigDecimal result = value;
|
||||
if (BigDecimal.ZERO.compareTo(value) == 0) {
|
||||
result = null;
|
||||
}
|
||||
return result;
|
||||
public boolean isNotZero() {
|
||||
return !MathUtil.isEmpty(amount);
|
||||
}
|
||||
|
||||
public LoanTransactionData toData(final CurrencyData currencyData, final AccountTransferData transfer) {
|
||||
@@ -785,7 +746,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
thisTransactionData.put("id", getId());
|
||||
thisTransactionData.put("officeId", this.office.getId());
|
||||
thisTransactionData.put("type", transactionType);
|
||||
thisTransactionData.put("reversed", Boolean.valueOf(isReversed()));
|
||||
thisTransactionData.put("reversed", isReversed());
|
||||
thisTransactionData.put("date", getTransactionDate());
|
||||
thisTransactionData.put("currencyCode", currencyCode);
|
||||
thisTransactionData.put("amount", this.amount);
|
||||
@@ -871,16 +832,37 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
}
|
||||
|
||||
public boolean isAccrual() {
|
||||
return LoanTransactionType.ACCRUAL.equals(getTypeOf()) && isNotReversed();
|
||||
return getTypeOf().isAccrual();
|
||||
}
|
||||
|
||||
public boolean isAccrualAdjustment() {
|
||||
return getTypeOf().isAccrualAdjustment();
|
||||
}
|
||||
|
||||
public boolean isAccrualActivity() {
|
||||
return getTypeOf().isAccrualActivity();
|
||||
}
|
||||
|
||||
public boolean isAccrualRelated() {
|
||||
return isAccrual() || isAccrualAdjustment() || isAccrualActivity();
|
||||
}
|
||||
|
||||
public boolean isWaiveCharge() {
|
||||
return getTypeOf().isWaiveCharges();
|
||||
}
|
||||
|
||||
public boolean isWaiveInterest() {
|
||||
return getTypeOf().isWaiveInterest();
|
||||
}
|
||||
|
||||
public boolean isNonMonetaryTransaction() {
|
||||
return isNotReversed() && (LoanTransactionType.CONTRA.equals(getTypeOf())
|
||||
|| LoanTransactionType.MARKED_FOR_RESCHEDULING.equals(getTypeOf()) || LoanTransactionType.ACCRUAL.equals(getTypeOf())
|
||||
|| LoanTransactionType.ACCRUAL_ACTIVITY.equals(getTypeOf()) || LoanTransactionType.APPROVE_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.INITIATE_TRANSFER.equals(getTypeOf()) || LoanTransactionType.REJECT_TRANSFER.equals(getTypeOf())
|
||||
|| LoanTransactionType.WITHDRAW_TRANSFER.equals(getTypeOf()) || LoanTransactionType.CHARGE_OFF.equals(getTypeOf())
|
||||
|| LoanTransactionType.REAMORTIZE.equals(getTypeOf()) || LoanTransactionType.REAGE.equals(getTypeOf()));
|
||||
LoanTransactionType type = getTypeOf();
|
||||
return isNotReversed() && (type == LoanTransactionType.CONTRA || type == LoanTransactionType.MARKED_FOR_RESCHEDULING
|
||||
|| type == LoanTransactionType.ACCRUAL || type == LoanTransactionType.ACCRUAL_ADJUSTMENT
|
||||
|| type == LoanTransactionType.ACCRUAL_ACTIVITY || type == LoanTransactionType.APPROVE_TRANSFER
|
||||
|| type == LoanTransactionType.INITIATE_TRANSFER || type == LoanTransactionType.REJECT_TRANSFER
|
||||
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type == LoanTransactionType.CHARGE_OFF
|
||||
|| type == LoanTransactionType.REAMORTIZE || type == LoanTransactionType.REAGE);
|
||||
}
|
||||
|
||||
public void updateOutstandingLoanBalance(BigDecimal outstandingLoanBalance) {
|
||||
@@ -985,11 +967,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
}
|
||||
|
||||
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
|
||||
return isDisbursement() || isAccrual() || isRepaymentAtDisbursement() || isRepayment() || isAccrualActivity();
|
||||
}
|
||||
|
||||
public boolean isAccrualTransaction() {
|
||||
return isAccrual();
|
||||
return isNotReversed() && (isDisbursement() || isAccrualActivity() || isRepaymentAtDisbursement() || isRepayment());
|
||||
}
|
||||
|
||||
public Money getOutstandingLoanBalanceMoney(final MonetaryCurrency currency) {
|
||||
|
||||
+14
-14
@@ -24,12 +24,12 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
|
||||
|
||||
@Entity
|
||||
@Table(name = "m_loan_transaction_repayment_schedule_mapping")
|
||||
@@ -86,21 +86,13 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
return (value == null || value.isZero()) ? null : value.getAmount();
|
||||
}
|
||||
|
||||
private BigDecimal defaultToZeroIfNull(final BigDecimal value) {
|
||||
BigDecimal result = value;
|
||||
if (value == null) {
|
||||
result = BigDecimal.ZERO;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public LoanRepaymentScheduleInstallment getLoanRepaymentScheduleInstallment() {
|
||||
return this.installment;
|
||||
}
|
||||
|
||||
public void updateComponents(@NotNull Money principal, @NotNull Money interest, @NotNull Money feeCharges,
|
||||
@NotNull Money penaltyCharges) {
|
||||
updateComponents(principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
|
||||
public void updateComponents(Money principal, Money interest, Money feeCharges, Money penaltyCharges) {
|
||||
updateComponents(MathUtil.toBigDecimal(principal), MathUtil.toBigDecimal(interest), MathUtil.toBigDecimal(feeCharges),
|
||||
MathUtil.toBigDecimal(penaltyCharges));
|
||||
}
|
||||
|
||||
void updateComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
|
||||
@@ -112,8 +104,7 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
}
|
||||
|
||||
private void updateAmount() {
|
||||
this.amount = defaultToZeroIfNull(getPrincipalPortion()).add(defaultToZeroIfNull(getInterestPortion()))
|
||||
.add(defaultToZeroIfNull(getFeeChargesPortion())).add(defaultToZeroIfNull(getPenaltyChargesPortion()));
|
||||
this.amount = MathUtil.add(getPrincipalPortion(), getInterestPortion(), getFeeChargesPortion(), getPenaltyChargesPortion());
|
||||
}
|
||||
|
||||
public void setComponents(final BigDecimal principal, final BigDecimal interest, final BigDecimal feeCharges,
|
||||
@@ -146,6 +137,15 @@ public class LoanTransactionToRepaymentScheduleMapping extends AbstractPersistab
|
||||
return Money.of(currency, this.penaltyChargesPortion);
|
||||
}
|
||||
|
||||
public BigDecimal getPortion(AllocationType allocationType) {
|
||||
return switch (allocationType) {
|
||||
case PRINCIPAL -> getPrincipalPortion();
|
||||
case INTEREST -> getInterestPortion();
|
||||
case FEE -> getFeeChargesPortion();
|
||||
case PENALTY -> getPenaltyChargesPortion();
|
||||
};
|
||||
}
|
||||
|
||||
public LoanTransaction getLoanTransaction() {
|
||||
return loanTransaction;
|
||||
}
|
||||
|
||||
+5
@@ -66,6 +66,7 @@ public enum LoanTransactionType {
|
||||
INTEREST_PAYMENT_WAIVER(31, "loanTransactionType.interestPaymentWaiver"), //
|
||||
ACCRUAL_ACTIVITY(32, "loanTransactionType.accrualActivity"), //
|
||||
INTEREST_REFUND(33, "loanTransactionType.interestRefund"), //
|
||||
ACCRUAL_ADJUSTMENT(34, "loanTransactionType.accrualAdjustment"), //
|
||||
;
|
||||
|
||||
private final Integer value;
|
||||
@@ -115,6 +116,7 @@ public enum LoanTransactionType {
|
||||
case 31 -> LoanTransactionType.INTEREST_PAYMENT_WAIVER;
|
||||
case 32 -> LoanTransactionType.ACCRUAL_ACTIVITY;
|
||||
case 33 -> LoanTransactionType.INTEREST_REFUND;
|
||||
case 34 -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
|
||||
default -> LoanTransactionType.INVALID;
|
||||
};
|
||||
}
|
||||
@@ -228,4 +230,7 @@ public enum LoanTransactionType {
|
||||
return this.equals(LoanTransactionType.INTEREST_REFUND);
|
||||
}
|
||||
|
||||
public boolean isAccrualAdjustment() {
|
||||
return this == LoanTransactionType.ACCRUAL_ADJUSTMENT;
|
||||
}
|
||||
}
|
||||
|
||||
+16
-9
@@ -45,14 +45,14 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
|
||||
totalInterest = totalInterest.plus(installment.getInterestCharged(currency));
|
||||
totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency));
|
||||
}
|
||||
LoanChargePaidBy accrualBy = null;
|
||||
List<LoanChargePaidBy> accruals = null;
|
||||
if (loanCharge.isSpecifiedDueDate()) {
|
||||
LoanRepaymentScheduleInstallment addedPeriod = addChargeOnlyRepaymentInstallmentIfRequired(loanCharge, installments);
|
||||
if (addedPeriod != null) {
|
||||
addedPeriod.updateObligationsMet(currency, disbursementDate);
|
||||
}
|
||||
accrualBy = loanCharge.getLoanChargePaidBySet().stream().filter(e -> e.getLoanTransaction().isAccrual()).findFirst()
|
||||
.orElse(null);
|
||||
accruals = loanCharge.getLoanChargePaidBySet().stream().filter(e -> !e.getLoanTransaction().isReversed()
|
||||
&& (e.getLoanTransaction().isAccrual() || e.getLoanTransaction().isAccrualAdjustment())).toList();
|
||||
}
|
||||
LocalDate startDate = disbursementDate;
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
@@ -61,7 +61,8 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
|
||||
continue;
|
||||
}
|
||||
boolean installmentChargeApplicable = !installment.isRecalculatedInterestComponent();
|
||||
boolean isFirstPeriod = installment.getInstallmentNumber().equals(firstNormalInstallmentNumber);
|
||||
Integer installmentNumber = installment.getInstallmentNumber();
|
||||
boolean isFirstPeriod = installmentNumber.equals(firstNormalInstallmentNumber);
|
||||
Predicate<LoanCharge> feePredicate = e -> e.isFeeCharge() && !e.isDueAtDisbursement();
|
||||
LocalDate dueDate = installment.getDueDate();
|
||||
final Money feeChargesDue = calcChargeDue(startDate, dueDate, loanCharge, currency, installment, totalPrincipal, totalInterest,
|
||||
@@ -82,13 +83,19 @@ public class SingleLoanChargeRepaymentScheduleProcessingWrapper {
|
||||
installment.addToChargePortion(feeChargesDue, feeChargesWaived, feeChargesWrittenOff, penaltyChargesDue, penaltyChargesWaived,
|
||||
penaltyChargesWrittenOff);
|
||||
|
||||
if (accrualBy != null && installment.isAdditional() && loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) {
|
||||
Money amount = Money.of(currency, accrualBy.getAmount());
|
||||
if (accruals != null && !accruals.isEmpty() && installment.isAdditional()
|
||||
&& loanCharge.isDueInPeriod(startDate, dueDate, isFirstPeriod)) {
|
||||
BigDecimal amount = null;
|
||||
for (LoanChargePaidBy accrual : accruals) {
|
||||
accrual.setInstallmentNumber(installmentNumber);
|
||||
amount = accrual.getLoanTransaction().isAccrual() ? MathUtil.add(amount, accrual.getAmount())
|
||||
: MathUtil.subtract(amount, accrual.getAmount());
|
||||
}
|
||||
Money accruedAmount = Money.of(currency, MathUtil.negativeToZero(amount));
|
||||
boolean isFee = loanCharge.isFeeCharge();
|
||||
installment.updateAccrualPortion(installment.getInterestAccrued(currency),
|
||||
MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? amount : null)),
|
||||
MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : amount)));
|
||||
accrualBy.setInstallmentNumber(installment.getInstallmentNumber());
|
||||
MathUtil.plus(installment.getFeeAccrued(currency), (isFee ? accruedAmount : null)),
|
||||
MathUtil.plus(installment.getPenaltyAccrued(currency), (isFee ? null : accruedAmount)));
|
||||
}
|
||||
startDate = dueDate;
|
||||
}
|
||||
|
||||
+2
-2
@@ -166,7 +166,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
// pass through for new transactions
|
||||
if (loanTransaction.getId() == null) {
|
||||
processLatestTransaction(loanTransaction, new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
|
||||
loanTransaction.adjustInterestComponent(currency);
|
||||
loanTransaction.adjustInterestComponent();
|
||||
} else {
|
||||
/**
|
||||
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties)
|
||||
@@ -178,7 +178,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
|
||||
// re-process transaction
|
||||
processLatestTransaction(newLoanTransaction,
|
||||
new TransactionCtx(currency, installments, charges, overpaymentHolder, null));
|
||||
newLoanTransaction.adjustInterestComponent(currency);
|
||||
newLoanTransaction.adjustInterestComponent();
|
||||
/**
|
||||
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
|
||||
* changedTransactionDetail accordingly
|
||||
|
||||
+39
@@ -18,6 +18,10 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isAfterPeriod;
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isBeforePeriod;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -2805,4 +2809,39 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
|
||||
.feeCharges(feeCharges) //
|
||||
.penaltyCharges(penaltyCharges);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) {
|
||||
if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) {
|
||||
return null;
|
||||
}
|
||||
if (isBeforePeriod(targetDate, installment, false)) {
|
||||
return null;
|
||||
}
|
||||
Loan loan = installment.getLoan();
|
||||
MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency();
|
||||
BigDecimal interest = installment.getInterestCharged();
|
||||
Money zero = Money.zero(currency);
|
||||
if (MathUtil.isEmpty(interest)) {
|
||||
return zero;
|
||||
}
|
||||
if (isAfterPeriod(targetDate, installment) || DateUtils.isEqual(targetDate, installment.getDueDate())) {
|
||||
return installment.getInterestCharged(currency);
|
||||
}
|
||||
|
||||
BigDecimal interestPortion;
|
||||
LocalDate startDate = installment.getFromDate();
|
||||
LocalDate dueDate = installment.getDueDate();
|
||||
if (DateUtils.isBefore(startDate, loan.getInterestChargedFromDate())) {
|
||||
startDate = loan.getInterestChargedFromDate();
|
||||
}
|
||||
if (!DateUtils.isBefore(startDate, dueDate) || !DateUtils.isBefore(startDate, targetDate)) {
|
||||
return zero;
|
||||
}
|
||||
int totalNumberOfDays = DateUtils.getExactDifferenceInDays(startDate, dueDate);
|
||||
int daysToBeAccrued = DateUtils.getExactDifferenceInDays(startDate, targetDate);
|
||||
double interestPerDay = interest.doubleValue() / totalNumberOfDays;
|
||||
interestPortion = BigDecimal.valueOf(interestPerDay * daysToBeAccrued);
|
||||
return Money.of(currency, interestPortion);
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -18,14 +18,17 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
|
||||
@@ -42,4 +45,5 @@ public interface LoanScheduleGenerator {
|
||||
MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
|
||||
|
||||
Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate);
|
||||
}
|
||||
|
||||
+15
-12
@@ -18,30 +18,33 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.exception.MultiException;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
public interface LoanAccrualsProcessingService {
|
||||
|
||||
void addPeriodicAccruals(LocalDate tilldate) throws MultiException;
|
||||
void addPeriodicAccruals(@NotNull LocalDate tilldate) throws MultiException;
|
||||
|
||||
void addPeriodicAccruals(LocalDate tilldate, Loan loan) throws MultiException;
|
||||
void addPeriodicAccruals(@NotNull LocalDate tilldate, @NotNull Loan loan) throws MultiException;
|
||||
|
||||
void addAccrualAccounting(Long loanId, Collection<LoanScheduleAccrualData> loanScheduleAccrualDatas) throws Exception;
|
||||
void addAccruals(@NotNull LocalDate tilldate) throws MultiException;
|
||||
|
||||
void reprocessExistingAccruals(@NotNull Loan loan);
|
||||
|
||||
void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled);
|
||||
|
||||
void processIncomePostingAndAccruals(@NotNull Loan loan);
|
||||
|
||||
void addIncomeAndAccrualTransactions(Long loanId) throws Exception;
|
||||
|
||||
void reprocessExistingAccruals(Loan loan);
|
||||
void processAccrualsOnLoanClosure(@NotNull Loan loan);
|
||||
|
||||
void processAccrualsForInterestRecalculation(Loan loan, boolean isInterestRecalculationEnabled);
|
||||
void processAccrualsOnLoanReopen(@NotNull Loan loan);
|
||||
|
||||
void processIncomePostingAndAccruals(Loan loan);
|
||||
|
||||
void processAccrualsForLoanClosure(Loan loan);
|
||||
|
||||
void processAccrualsForLoanForeClosure(Loan loan, LocalDate foreClosureDate, Collection<LoanTransaction> newAccrualTransactions);
|
||||
void processAccrualsOnLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate,
|
||||
@NotNull List<LoanTransaction> newAccrualTransactions);
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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 java.util.Map;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||
import org.apache.fineract.portfolio.fund.domain.Fund;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
public interface LoanAssembler {
|
||||
|
||||
Loan assembleFrom(Long accountId);
|
||||
|
||||
Loan assembleFrom(JsonCommand command);
|
||||
|
||||
void setHelpers(Loan loanAccount);
|
||||
|
||||
void accountNumberGeneration(JsonCommand command, Loan loan);
|
||||
|
||||
CodeValue findCodeValueByIdIfProvided(Long codeValueId);
|
||||
|
||||
Fund findFundByIdIfProvided(Long fundId);
|
||||
|
||||
Staff findLoanOfficerByIdIfProvided(Long loanOfficerId);
|
||||
|
||||
Map<String, Object> updateFrom(JsonCommand command, Loan loan);
|
||||
|
||||
Map<String, Object> updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser);
|
||||
|
||||
Map<String, Object> updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser);
|
||||
}
|
||||
+2
-3
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.portfolio.charge.data.ChargeData;
|
||||
import org.apache.fineract.portfolio.charge.domain.Charge;
|
||||
@@ -38,12 +39,10 @@ public interface LoanChargeReadPlatformService {
|
||||
|
||||
Collection<LoanChargeData> retrieveLoanChargesForFeePayment(Integer paymentMode, Integer loanStatus);
|
||||
|
||||
Collection<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges);
|
||||
List<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges);
|
||||
|
||||
Collection<Integer> retrieveOverdueInstallmentChargeFrequencyNumber(Loan loan, Charge charge, Integer periodNumber);
|
||||
|
||||
Collection<LoanChargeData> retrieveLoanChargesForAccrual(Long loanId);
|
||||
|
||||
Collection<LoanChargePaidByData> retrieveLoanChargesPaidBy(Long chargeId, LoanTransactionType transactionType,
|
||||
Integer installmentNumber);
|
||||
|
||||
|
||||
+2
@@ -323,6 +323,8 @@ public final class LoanEnumerations {
|
||||
LoanTransactionType.ACCRUAL_ACTIVITY.getCode(), "Accrual Activity");
|
||||
case INTEREST_REFUND -> new LoanTransactionEnumData(LoanTransactionType.INTEREST_REFUND.getValue().longValue(),
|
||||
LoanTransactionType.INTEREST_REFUND.getCode(), "Interest Refund");
|
||||
case ACCRUAL_ADJUSTMENT -> new LoanTransactionEnumData(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue().longValue(),
|
||||
LoanTransactionType.ACCRUAL_ADJUSTMENT.getCode(), "Accrual Adjustment");
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -46,4 +46,5 @@
|
||||
<include relativeToChangelogFile="true" file="parts/1021_add_loan_status_change_history.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1022_add_interest_refund_support.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1023_add_charge_off_behaviour.xml"/>
|
||||
<include relativeToChangelogFile="true" file="parts/1024_accrual_adjustment_transaction_type.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanAccrualAdjustmentTransactionBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
<changeSet author="fineract" id="2">
|
||||
<insert tableName="r_enum_value">
|
||||
<column name="enum_name" value="transaction_type_enum"/>
|
||||
<column name="enum_id" valueNumeric="34"/>
|
||||
<column name="enum_message_property" value="Accrual Adjustment"/>
|
||||
<column name="enum_value" value="Accrual Adjustment"/>
|
||||
<column name="enum_type" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+228
-197
@@ -73,6 +73,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule
|
||||
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.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
|
||||
@@ -95,6 +96,8 @@ import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocat
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -104,6 +107,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy";
|
||||
|
||||
public final EMICalculator emiCalculator;
|
||||
public final LoanRepositoryWrapper loanRepositoryWrapper;
|
||||
public final InterestRefundService interestRefundService;
|
||||
|
||||
@Override
|
||||
@@ -152,7 +156,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
// only for progressive loans
|
||||
public Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessProgressiveLoanTransactions(
|
||||
LocalDate disbursementDate, LocalDate currentDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
LocalDate disbursementDate, LocalDate targetDate, List<LoanTransaction> loanTransactions, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
|
||||
if (loanTransactions.isEmpty()) {
|
||||
@@ -179,8 +183,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
final Loan loan = loanTransactions.get(0).getLoan();
|
||||
final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateModel(loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, installments, overpaymentHolder.getMoneyObject().getMc());
|
||||
ProgressiveLoanInterestScheduleModel scheduleModel = emiCalculator.generateInstallmentInterestScheduleModel(installments,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, overpaymentHolder.getMoneyObject().getMc());
|
||||
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder,
|
||||
changedTransactionDetail, scheduleModel);
|
||||
|
||||
@@ -217,7 +221,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId);
|
||||
createNewTransaction(oldTransaction, newTransaction, ctx);
|
||||
}
|
||||
recalculateInterestForDate(currentDate, ctx);
|
||||
recalculateInterestForDate(targetDate, ctx);
|
||||
List<LoanTransaction> txs = changeOperations.stream() //
|
||||
.filter(ChangeOperation::isTransaction) //
|
||||
.map(e -> e.getLoanTransaction().get()).toList();
|
||||
@@ -225,6 +229,32 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return Pair.of(changedTransactionDetail, scheduleModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
|
||||
.getLeft();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
|
||||
public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId, LocalDate targetDate) {
|
||||
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
|
||||
List<LoanTransaction> transactions = loan.retrieveListOfTransactionsForReprocessing();
|
||||
MonetaryCurrency currency = loan.getLoanRepaymentScheduleDetail().getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
Set<LoanCharge> charges = loan.getActiveCharges();
|
||||
return reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), targetDate, transactions, currency, installments, charges)
|
||||
.getRight();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) {
|
||||
LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId());
|
||||
return newTransaction == null ? transaction : newTransaction;
|
||||
}
|
||||
|
||||
private void processInterestRateChange(final List<LoanRepaymentScheduleInstallment> installments,
|
||||
final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) {
|
||||
final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom();
|
||||
@@ -249,29 +279,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> loanTransactions,
|
||||
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
LocalDate currentDate = DateUtils.getBusinessLocalDate();
|
||||
return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges)
|
||||
.getLeft();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) {
|
||||
LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId());
|
||||
return newTransaction == null ? transaction : newTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
switch (loanTransaction.getTypeOf()) {
|
||||
case DISBURSEMENT -> handleDisbursement(loanTransaction, ctx);
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges());
|
||||
case WRITEOFF -> handleWriteOff(loanTransaction, ctx);
|
||||
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, ctx);
|
||||
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
|
||||
case CREDIT_BALANCE_REFUND ->
|
||||
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||
case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, ctx);
|
||||
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
|
||||
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
|
||||
handleRepayment(loanTransaction, ctx);
|
||||
@@ -281,7 +296,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
||||
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx);
|
||||
case REAGE -> handleReAge(loanTransaction, ctx);
|
||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx);
|
||||
// TODO: Cover rest of the transaction types
|
||||
default -> {
|
||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||
@@ -357,9 +372,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
processCreditTransaction(loanTransaction, ctx);
|
||||
}
|
||||
|
||||
protected void handleCreditBalanceRefund(LoanTransaction transaction, TransactionCtx ctx) {
|
||||
super.handleCreditBalanceRefund(transaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
|
||||
}
|
||||
|
||||
private boolean hasNoCustomCreditAllocationRule(LoanTransaction loanTransaction) {
|
||||
return (loanTransaction.getLoan().getCreditAllocationRules() == null || !loanTransaction.getLoan().getCreditAllocationRules()
|
||||
.stream().anyMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
||||
List<LoanCreditAllocationRule> creditAllocationRules = loanTransaction.getLoan().getCreditAllocationRules();
|
||||
return (creditAllocationRules == null || creditAllocationRules.stream()
|
||||
.noneMatch(e -> e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
|
||||
}
|
||||
|
||||
protected LoanTransaction findChargebackOriginalTransaction(LoanTransaction chargebackTransaction, TransactionCtx ctx) {
|
||||
@@ -396,77 +416,76 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
super.processCreditTransaction(loanTransaction, ctx.getOverpaymentHolder(), ctx.getCurrency(), ctx.getInstallments());
|
||||
} else {
|
||||
loanTransaction.resetDerivedComponents();
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
|
||||
ctx.getInstallments().sort(byDate);
|
||||
final Money zeroMoney = Money.zero(ctx.getCurrency());
|
||||
Money transactionAmount = loanTransaction.getAmount(ctx.getCurrency());
|
||||
final Money zeroMoney = Money.zero(currency);
|
||||
Money transactionAmount = loanTransaction.getAmount(currency);
|
||||
Money totalOverpaid = ctx.getOverpaymentHolder().getMoneyObject();
|
||||
Money amountToDistribute = MathUtil.negativeToZero(loanTransaction.getAmount(ctx.getCurrency()).minus(totalOverpaid));
|
||||
Money amountToDistribute = MathUtil.negativeToZero(transactionAmount).minus(totalOverpaid);
|
||||
Money overpaymentAmount = MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
|
||||
loanTransaction.setOverPayments(overpaymentAmount);
|
||||
if (!transactionAmount.isGreaterThanZero()) {
|
||||
return;
|
||||
}
|
||||
if (!loanTransaction.isChargeback()) {
|
||||
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
|
||||
}
|
||||
|
||||
if (transactionAmount.isGreaterThanZero()) {
|
||||
if (loanTransaction.isChargeback()) {
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||
// get the original allocation from the opriginal transaction
|
||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction,
|
||||
ctx.getCurrency());
|
||||
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||
LoanTransaction originalTransaction = findChargebackOriginalTransaction(loanTransaction, ctx);
|
||||
// get the original allocation from the opriginal transaction
|
||||
Map<AllocationType, Money> originalAllocationNotAdjusted = getOriginalAllocation(originalTransaction, currency);
|
||||
LoanCreditAllocationRule chargeBackAllocationRule = getChargebackAllocationRules(loanTransaction);
|
||||
|
||||
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
|
||||
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
|
||||
originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule);
|
||||
// if there were earlier chargebacks then let's calculate the remaining amounts for each portion
|
||||
Map<AllocationType, Money> originalAllocation = adjustOriginalAllocationWithFormerChargebacks(originalTransaction,
|
||||
originalAllocationNotAdjusted, loanTransaction, ctx, chargeBackAllocationRule);
|
||||
|
||||
// calculate the current chargeback allocation
|
||||
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
|
||||
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), ctx.getCurrency());
|
||||
// calculate the current chargeback allocation
|
||||
Map<AllocationType, Money> chargebackAllocation = calculateChargebackAllocationMap(originalAllocation,
|
||||
transactionAmount.getAmount(), chargeBackAllocationRule.getAllocationTypes(), currency);
|
||||
|
||||
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
||||
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
||||
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL), chargebackAllocation.get(INTEREST),
|
||||
chargebackAllocation.get(FEE), chargebackAllocation.get(PENALTY));
|
||||
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
boolean loanTransactionMapped = false;
|
||||
LocalDate pastDueDate = null;
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
|
||||
pastDueDate = currentInstallment.getDueDate();
|
||||
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
||||
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
final LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
boolean loanTransactionMapped = false;
|
||||
LocalDate pastDueDate = null;
|
||||
for (final LoanRepaymentScheduleInstallment currentInstallment : ctx.getInstallments()) {
|
||||
pastDueDate = currentInstallment.getDueDate();
|
||||
if (!currentInstallment.isAdditional() && DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
|
||||
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
|
||||
// If already exists an additional installment just update the due date and
|
||||
// principal from the Loan chargeback / CBR transaction
|
||||
} else if (currentInstallment.isAdditional()) {
|
||||
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
|
||||
currentInstallment.updateDueDate(transactionDate);
|
||||
}
|
||||
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
}
|
||||
// If already exists an additional installment just update the due date and
|
||||
// principal from the Loan chargeback / CBR transaction
|
||||
} else if (currentInstallment.isAdditional()) {
|
||||
if (DateUtils.isAfter(transactionDate, currentInstallment.getDueDate())) {
|
||||
currentInstallment.updateDueDate(transactionDate);
|
||||
}
|
||||
|
||||
// New installment will be added (N+1 scenario)
|
||||
if (!loanTransactionMapped) {
|
||||
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
||||
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments()
|
||||
.get(ctx.getInstallments().size() - 1);
|
||||
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, currentInstallment, chargebackAllocation);
|
||||
} else {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
|
||||
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(),
|
||||
zeroMoney.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
|
||||
recognizeAmountsAfterChargeback(ctx.getCurrency(), transactionDate, installment, chargebackAllocation);
|
||||
installment.markAsAdditional();
|
||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||
}
|
||||
}
|
||||
allocateOverpayment(loanTransaction, ctx);
|
||||
} else {
|
||||
throw new RuntimeException("Unsupported transaction " + loanTransaction.getTypeOf().name());
|
||||
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
|
||||
loanTransactionMapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// New installment will be added (N+1 scenario)
|
||||
if (!loanTransactionMapped) {
|
||||
if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
|
||||
LoanRepaymentScheduleInstallment currentInstallment = ctx.getInstallments().get(ctx.getInstallments().size() - 1);
|
||||
recognizeAmountsAfterChargeback(ctx, transactionDate, currentInstallment, chargebackAllocation);
|
||||
} else {
|
||||
Loan loan = loanTransaction.getLoan();
|
||||
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
|
||||
(ctx.getInstallments().size() + 1), pastDueDate, transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(),
|
||||
zeroMoney.getAmount(), zeroMoney.getAmount(), false, null);
|
||||
recognizeAmountsAfterChargeback(ctx, transactionDate, installment, chargebackAllocation);
|
||||
installment.markAsAdditional();
|
||||
loan.addLoanRepaymentScheduleInstallment(installment);
|
||||
}
|
||||
}
|
||||
allocateOverpayment(loanTransaction, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,14 +530,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return allocation;
|
||||
}
|
||||
|
||||
private void recognizeAmountsAfterChargeback(MonetaryCurrency currency, LocalDate localDate,
|
||||
private void recognizeAmountsAfterChargeback(TransactionCtx ctx, LocalDate transactionDate,
|
||||
LoanRepaymentScheduleInstallment installment, Map<AllocationType, Money> chargebackAllocation) {
|
||||
Money principal = chargebackAllocation.get(PRINCIPAL);
|
||||
if (principal.isGreaterThanZero()) {
|
||||
installment.addToCreditedPrincipal(principal.getAmount());
|
||||
installment.addToPrincipal(localDate, principal);
|
||||
installment.addToPrincipal(transactionDate, principal);
|
||||
}
|
||||
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money fee = chargebackAllocation.get(FEE);
|
||||
if (fee.isGreaterThanZero()) {
|
||||
installment.addToCreditedFee(fee.getAmount());
|
||||
@@ -577,9 +597,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return relation -> relation.getRelationType().equals(typeEnum) && Objects.equals(relation.getToTransaction().getId(), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
|
||||
protected void handleRefund(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money zero = Money.zero(currency);
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
|
||||
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
|
||||
@@ -604,18 +623,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Collectors.groupingBy(PaymentAllocationType::getDueType, LinkedHashMap::new, mapping(Function.identity(), toList())));
|
||||
|
||||
for (Map.Entry<DueType, List<PaymentAllocationType>> paymentAllocationsEntry : paymentAllocationsMap.entrySet()) {
|
||||
transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, currency, installments,
|
||||
transactionAmountUnprocessed, paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule,
|
||||
transactionMappings, charges, balances);
|
||||
transactionAmountUnprocessed = refundTransactionHorizontally(loanTransaction, ctx, transactionAmountUnprocessed,
|
||||
paymentAllocationsEntry.getValue(), futureInstallmentAllocationRule, transactionMappings, balances);
|
||||
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (scheduleProcessingType.isVertical()) {
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) {
|
||||
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, currency, installments, zero,
|
||||
transactionMappings, transactionAmountUnprocessed, futureInstallmentAllocationRule, charges, balances,
|
||||
paymentAllocationType);
|
||||
transactionAmountUnprocessed = refundTransactionVertically(loanTransaction, ctx, transactionMappings,
|
||||
transactionAmountUnprocessed, futureInstallmentAllocationRule, balances, paymentAllocationType);
|
||||
if (!transactionAmountUnprocessed.isGreaterThanZero()) {
|
||||
break;
|
||||
}
|
||||
@@ -639,7 +656,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
// Reset derived component of new loan transaction and re-process transaction
|
||||
processLatestTransaction(processTransaction, ctx);
|
||||
if (loanTransaction.isInterestWaiver()) {
|
||||
processTransaction.adjustInterestComponent(ctx.getCurrency());
|
||||
processTransaction.adjustInterestComponent();
|
||||
}
|
||||
if (isNew) {
|
||||
checkRegisteredNewTransaction(loanTransaction, ctx);
|
||||
@@ -684,7 +701,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
processTransaction.addLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
if (processTransaction.isInterestWaiver()) {
|
||||
processTransaction.adjustInterestComponent(currency);
|
||||
processTransaction.adjustInterestComponent();
|
||||
}
|
||||
if (isNew) {
|
||||
processTransaction = checkRegisteredNewTransaction(transaction, ctx);
|
||||
@@ -810,51 +827,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return changeOperations;
|
||||
}
|
||||
|
||||
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
ProgressiveTransactionCtx progressiveTransactionCtx = (ProgressiveTransactionCtx) transactionCtx;
|
||||
if (progressiveTransactionCtx.getModel() == null) {
|
||||
throw new IllegalStateException("TransactionCtx has no model");
|
||||
}
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(progressiveTransactionCtx.getCurrency());
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = progressiveTransactionCtx.getInstallments().stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(progressiveTransactionCtx.getCurrency()).isZero()).findFirst()
|
||||
.orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
|
||||
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
|
||||
}
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
|
||||
|
||||
emiCalculator.addDisbursement(progressiveTransactionCtx.getModel(), disbursementTransaction.getTransactionDate(),
|
||||
amortizableAmount);
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
progressiveTransactionCtx.getModel().repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = transactionCtx.getInstallments().stream()
|
||||
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()
|
||||
&& !ri.getDueDate().isBefore(disbursementTransaction.getTransactionDate()))
|
||||
.findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||
installment.updateObligationsMet(progressiveTransactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
// TODO: Fix this and enhance EMICalculator to support reamortization and reaging
|
||||
if (disbursementTransaction.getLoan().isInterestBearing()) {
|
||||
@@ -864,43 +836,90 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
ProgressiveLoanInterestScheduleModel model;
|
||||
if (!(transactionCtx instanceof ProgressiveTransactionCtx)
|
||||
|| (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) {
|
||||
throw new IllegalStateException("TransactionCtx has no model");
|
||||
}
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = transactionCtx.getInstallments().stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(transactionCtx.getCurrency());
|
||||
Loan loan = disbursementTransaction.getLoan();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = loan.getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||
LocalDate transactionDate = disbursementTransaction.getTransactionDate();
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = transactionCtx.getInstallments().stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(transactionCtx.getCurrency()).isZero()).findFirst().orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(transactionCtx.getCurrency(), downPaymentAmt);
|
||||
downPaymentAmount = Money.of(currency, downPaymentAmt);
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
|
||||
downPaymentInstallment.addToPrincipal(transactionDate, downPaymentAmount);
|
||||
}
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
emiCalculator.addDisbursement(model, transactionDate, amortizableAmount);
|
||||
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
model.repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = installments.stream().filter(
|
||||
ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate))
|
||||
.findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
installment.updatePrincipal(rm.getDuePrincipal().getAmount());
|
||||
installment.updateInterestCharged(rm.getDueInterest().getAmount());
|
||||
installment.updateObligationsMet(currency, transactionDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
allocateOverpayment(disbursementTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) {
|
||||
disbursementTransaction.resetDerivedComponents();
|
||||
final MathContext mc = MoneyHelper.getMathContext();
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = transactionCtx.getInstallments();
|
||||
List<LoanRepaymentScheduleInstallment> candidateRepaymentInstallments = installments.stream().filter(
|
||||
i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional())
|
||||
.toList();
|
||||
int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size();
|
||||
LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail();
|
||||
Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||
Money downPaymentAmount = Money.zero(currency);
|
||||
if (loanProductRelatedDetail.isEnableDownPayment()) {
|
||||
LoanRepaymentScheduleInstallment downPaymentInstallment = installments.stream()
|
||||
.filter(i -> i.isDownPayment() && i.getPrincipal(currency).isZero()).findFirst().orElseThrow();
|
||||
BigDecimal downPaymentAmt = MathUtil.percentageOf(disbursementTransaction.getAmount(),
|
||||
loanProductRelatedDetail.getDisbursedAmountPercentageForDownPayment(), mc);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
downPaymentAmt = Money.roundToMultiplesOf(downPaymentAmt, installmentAmountInMultiplesOf);
|
||||
}
|
||||
downPaymentAmount = Money.of(currency, downPaymentAmt);
|
||||
downPaymentInstallment.addToPrincipal(disbursementTransaction.getTransactionDate(), downPaymentAmount);
|
||||
}
|
||||
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(transactionCtx.getCurrency()).minus(downPaymentAmount);
|
||||
Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount);
|
||||
|
||||
if (amortizableAmount.isGreaterThanZero()) {
|
||||
Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext());
|
||||
MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount);
|
||||
|
||||
candidateRepaymentInstallments.forEach(i -> {
|
||||
Money previousPrincipal = i.getPrincipal(transactionCtx.getCurrency());
|
||||
Money previousPrincipal = i.getPrincipal(currency);
|
||||
Money newPrincipal = previousPrincipal.add(increasePrincipalBy);
|
||||
if (installmentAmountInMultiplesOf != null) {
|
||||
newPrincipal = Money.roundToMultiplesOf(newPrincipal, installmentAmountInMultiplesOf);
|
||||
}
|
||||
i.updatePrincipal(newPrincipal.getAmount());
|
||||
moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal));
|
||||
i.updateObligationsMet(transactionCtx.getCurrency(), disbursementTransaction.getTransactionDate());
|
||||
i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate());
|
||||
});
|
||||
// Hence the rounding, we might need to amend the last installment amount
|
||||
candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1)
|
||||
@@ -930,26 +949,30 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate currentDate,
|
||||
protected void handleWriteOff(final LoanTransaction transaction, TransactionCtx ctx) {
|
||||
super.handleWriteOff(transaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
}
|
||||
|
||||
private List<LoanRepaymentScheduleInstallment> findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(LocalDate targetDate,
|
||||
ProgressiveTransactionCtx transactionCtx) {
|
||||
return transactionCtx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.isDownPayment() && !installment.isAdditional())
|
||||
.filter(installment -> installment.isOverdueOn(currentDate))
|
||||
.filter(installment -> installment.isOverdueOn(targetDate))
|
||||
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).toList();
|
||||
}
|
||||
|
||||
private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransactionCtx ctx) {
|
||||
private void recalculateInterestForDate(LocalDate targetDate, ProgressiveTransactionCtx ctx) {
|
||||
if (ctx.getInstallments() != null && !ctx.getInstallments().isEmpty()
|
||||
&& ctx.getInstallments().get(0).getLoan().isInterestRecalculationEnabledForProduct()
|
||||
&& ctx.getInstallments().get(0).getLoan().getLoanProductRelatedDetail().isInterestRecalculationEnabled()
|
||||
&& !ctx.getInstallments().get(0).getLoan().isNpa() && !ctx.getInstallments().get(0).getLoan().isChargedOff()) {
|
||||
List<LoanRepaymentScheduleInstallment> overdueInstallmentsSortedByInstallmentNumber = findOverdueInstallmentsBeforeDateSortedByInstallmentNumber(
|
||||
currentDate, ctx);
|
||||
targetDate, ctx);
|
||||
if (!overdueInstallmentsSortedByInstallmentNumber.isEmpty()) {
|
||||
List<LoanRepaymentScheduleInstallment> normalInstallments = ctx.getInstallments().stream() //
|
||||
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()).toList();
|
||||
|
||||
Optional<LoanRepaymentScheduleInstallment> currentInstallmentOptional = normalInstallments.stream().filter(
|
||||
installment -> installment.getFromDate().isBefore(currentDate) && !installment.getDueDate().isBefore(currentDate))
|
||||
installment -> installment.getFromDate().isBefore(targetDate) && !installment.getDueDate().isBefore(targetDate))
|
||||
.findAny();
|
||||
|
||||
// get DUE installment or last installment
|
||||
@@ -962,7 +985,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
for (LoanRepaymentScheduleInstallment processingInstallment : overdueInstallmentsSortedByInstallmentNumber) {
|
||||
// add and subtract outstanding principal
|
||||
if (!overDuePrincipal.isZero()) {
|
||||
adjustOverduePrincipalForInstallment(currentDate, processingInstallment, overDuePrincipal,
|
||||
adjustOverduePrincipalForInstallment(targetDate, processingInstallment, overDuePrincipal,
|
||||
aggregatedOverDuePrincipal, ctx);
|
||||
}
|
||||
|
||||
@@ -970,11 +993,9 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
aggregatedOverDuePrincipal = aggregatedOverDuePrincipal.add(overDuePrincipal);
|
||||
}
|
||||
|
||||
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(currentDate);
|
||||
boolean adjustNeeded = !currentInstallment.equals(lastInstallment) || !lastInstallment.isOverdueOn(targetDate);
|
||||
if (adjustNeeded) {
|
||||
adjustOverduePrincipalForInstallment(currentDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal,
|
||||
ctx);
|
||||
|
||||
adjustOverduePrincipalForInstallment(targetDate, currentInstallment, overDuePrincipal, aggregatedOverDuePrincipal, ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1078,15 +1099,15 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
|
||||
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Set<LoanCharge> chargesOfInstallment,
|
||||
Balances balances, LoanRepaymentScheduleInstallment.PaymentAction action) {
|
||||
AllocationType allocationType = paymentAllocationType.getAllocationType();
|
||||
MonetaryCurrency currency = loanTransaction.getLoan().getCurrency();
|
||||
Money zero = Money.zero(currency);
|
||||
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||
Money zero = transactionAmountUnprocessed.zero();
|
||||
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment
|
||||
.getPaymentFunction(paymentAllocationType.getAllocationType(), action);
|
||||
LoanRepaymentScheduleInstallment.PaymentFunction paymentFunction = currentInstallment.getPaymentFunction(allocationType, action);
|
||||
ChargesPaidByFunction chargesPaidByFunction = getChargesPaymentFunction(action);
|
||||
Money portion = paymentFunction.accept(transactionDate, transactionAmountUnprocessed);
|
||||
|
||||
switch (paymentAllocationType.getAllocationType()) {
|
||||
switch (allocationType) {
|
||||
case PENALTY -> {
|
||||
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(portion));
|
||||
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, portion);
|
||||
@@ -1109,7 +1130,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, loanTransaction.getLoan().loanCurrency());
|
||||
currentInstallment.checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
|
||||
return portion;
|
||||
}
|
||||
|
||||
@@ -1130,13 +1151,14 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
|
||||
}
|
||||
|
||||
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, MoneyHolder overpaymentHolder) {
|
||||
if (overpaymentPortion.isGreaterThanZero()) {
|
||||
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
|
||||
MoneyHolder overpaymentHolder = transactionCtx.getOverpaymentHolder();
|
||||
if (MathUtil.isGreaterThanZero(overpaymentPortion)) {
|
||||
onLoanOverpayment(loanTransaction, overpaymentPortion);
|
||||
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().add(overpaymentPortion));
|
||||
loanTransaction.setOverPayments(overpaymentPortion);
|
||||
} else {
|
||||
overpaymentHolder.setMoneyObject(overpaymentPortion.zero());
|
||||
overpaymentHolder.setMoneyObject(Money.zero(transactionCtx.getCurrency()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1210,11 +1232,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
}
|
||||
|
||||
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
private Money refundTransactionHorizontally(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
||||
List<PaymentAllocationType> paymentAllocationTypes, FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money zero = Money.zero(currency);
|
||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
Set<LoanCharge> charges = ctx.getCharges();
|
||||
Money refundedPortion;
|
||||
outerLoop: do {
|
||||
LoanRepaymentScheduleInstallment latestPastDueInstallment = getLatestPastDueInstallmentForRefund(loanTransaction, currency,
|
||||
@@ -1284,13 +1308,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
private Money refundTransactionVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money zero,
|
||||
private Money refundTransactionVertically(LoanTransaction loanTransaction, TransactionCtx ctx,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Money transactionAmountUnprocessed,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Set<LoanCharge> charges, Balances balances,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule, Balances balances,
|
||||
PaymentAllocationType paymentAllocationType) {
|
||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
Money zero = Money.zero(currency);
|
||||
Money refundedPortion = zero;
|
||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
Set<LoanCharge> charges = ctx.getCharges();
|
||||
LoanRepaymentScheduleInstallment currentInstallment = null;
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
do {
|
||||
switch (paymentAllocationType.getDueType()) {
|
||||
@@ -1401,7 +1428,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
|
||||
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
|
||||
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx.getOverpaymentHolder());
|
||||
handleOverpayment(transactionAmountUnprocessed, loanTransaction, transactionCtx);
|
||||
}
|
||||
|
||||
private Money processPeriods(LoanTransaction transaction, Money processAmount, Set<LoanCharge> charges,
|
||||
@@ -1413,7 +1440,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
private Money processPeriods(LoanTransaction transaction, Money processAmount, LoanPaymentAllocationRule allocationRule,
|
||||
Set<LoanCharge> charges, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Balances balances,
|
||||
TransactionCtx transactionCtx) {
|
||||
MonetaryCurrency currency = transactionCtx.getCurrency();
|
||||
LoanScheduleProcessingType scheduleProcessingType = transaction.getLoan().getLoanProductRelatedDetail()
|
||||
.getLoanScheduleProcessingType();
|
||||
if (scheduleProcessingType.isHorizontal()) {
|
||||
@@ -1421,8 +1447,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
balances);
|
||||
}
|
||||
if (scheduleProcessingType.isVertical()) {
|
||||
return processPeriodsVertically(transaction, currency, transactionCtx.getInstallments(), processAmount, allocationRule,
|
||||
transactionMappings, charges, balances);
|
||||
return processPeriodsVertically(transaction, transactionCtx, processAmount, allocationRule, transactionMappings, charges,
|
||||
balances);
|
||||
}
|
||||
return processAmount;
|
||||
}
|
||||
@@ -1446,7 +1472,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
Money transactionAmountUnprocessed, List<PaymentAllocationType> paymentAllocationTypes,
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule,
|
||||
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, Set<LoanCharge> charges, Balances balances) {
|
||||
if (transactionAmountUnprocessed.isZero()) {
|
||||
if (MathUtil.isEmpty(transactionAmountUnprocessed)) {
|
||||
return transactionAmountUnprocessed;
|
||||
}
|
||||
|
||||
@@ -1589,7 +1615,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (DueType.IN_ADVANCE.equals(paymentAllocationType.getDueType())) {
|
||||
payDate = calculateNewPayDateInCaseOfInAdvancePayment(loanTransaction, installment);
|
||||
updateRepaymentPeriodBalances(paymentAllocationType, installment, model, payDate);
|
||||
updateRepaymentPeriodBalances(paymentAllocationType, installment, ctx, payDate);
|
||||
}
|
||||
|
||||
paidPortion = processPaymentAllocation(paymentAllocationType, installment, loanTransaction, transactionAmountUnprocessed,
|
||||
@@ -1597,17 +1623,16 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
if (PRINCIPAL.equals(paymentAllocationType.getAllocationType())) {
|
||||
emiCalculator.payPrincipal(model, installment.getDueDate(), payDate, paidPortion);
|
||||
updateRepaymentPeriods(loanTransaction, ctx, model);
|
||||
updateRepaymentPeriods(loanTransaction, ctx);
|
||||
} else if (INTEREST.equals(paymentAllocationType.getAllocationType())) {
|
||||
emiCalculator.payInterest(model, installment.getDueDate(), payDate, paidPortion);
|
||||
updateRepaymentPeriods(loanTransaction, ctx, model);
|
||||
updateRepaymentPeriods(loanTransaction, ctx);
|
||||
}
|
||||
return paidPortion;
|
||||
}
|
||||
|
||||
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx,
|
||||
ProgressiveLoanInterestScheduleModel model) {
|
||||
model.repaymentPeriods().forEach(rm -> {
|
||||
private void updateRepaymentPeriods(LoanTransaction loanTransaction, ProgressiveTransactionCtx ctx) {
|
||||
ctx.getModel().repaymentPeriods().forEach(rm -> {
|
||||
LoanRepaymentScheduleInstallment installment = ctx.getInstallments().stream()
|
||||
.filter(ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment()).findFirst().orElse(null);
|
||||
if (installment != null) {
|
||||
@@ -1619,8 +1644,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
}
|
||||
|
||||
private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType,
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) {
|
||||
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate);
|
||||
LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveTransactionCtx ctx, LocalDate payDate) {
|
||||
PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(ctx.getModel(), inAdvanceInstallment.getDueDate(), payDate);
|
||||
|
||||
switch (paymentAllocationType) {
|
||||
case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount());
|
||||
@@ -1637,6 +1662,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
LocalDate payDate = switch (strategy) {
|
||||
case TILL_PRE_CLOSURE_DATE -> loanTransaction.getTransactionDate();
|
||||
// TODO use isInPeriod
|
||||
case TILL_REST_FREQUENCY_DATE -> loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getFromDate()) //
|
||||
&& !loanTransaction.getTransactionDate().isAfter(inAdvanceInstallment.getDueDate()) //
|
||||
? inAdvanceInstallment.getDueDate() //
|
||||
@@ -1655,10 +1681,11 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private Money processPeriodsVertically(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed,
|
||||
private Money processPeriodsVertically(LoanTransaction loanTransaction, TransactionCtx ctx, Money transactionAmountUnprocessed,
|
||||
LoanPaymentAllocationRule paymentAllocationRule, List<LoanTransactionToRepaymentScheduleMapping> transactionMappings,
|
||||
Set<LoanCharge> charges, Balances balances) {
|
||||
MonetaryCurrency currency = ctx.getCurrency();
|
||||
List<LoanRepaymentScheduleInstallment> installments = ctx.getInstallments();
|
||||
int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments);
|
||||
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
|
||||
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
|
||||
@@ -1823,6 +1850,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
reprocessInstallmentsOrder(installments);
|
||||
}
|
||||
|
||||
protected void calculateAccrualActivity(LoanTransaction transaction, TransactionCtx ctx) {
|
||||
super.calculateAccrualActivity(transaction, ctx.getCurrency(), ctx.getInstallments());
|
||||
}
|
||||
|
||||
private void reprocessInstallmentsOrder(List<LoanRepaymentScheduleInstallment> installments) {
|
||||
AtomicInteger counter = new AtomicInteger(1);
|
||||
installments.stream().sorted(LoanRepaymentScheduleInstallment::compareToByDueDate)
|
||||
|
||||
+10
-8
@@ -84,10 +84,13 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
}
|
||||
|
||||
public Money getCalculatedDueInterest() {
|
||||
final BigDecimal interestDueTillRepaymentDueDate = getOutstandingLoanBalance()//
|
||||
.multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() //
|
||||
.divide(BigDecimal.valueOf(getLengthTillPeriodDueDate()), mc) //
|
||||
.multiply(BigDecimal.valueOf(getLength()), mc); //
|
||||
long lengthTillPeriodDueDate = getLengthTillPeriodDueDate();
|
||||
final BigDecimal interestDueTillRepaymentDueDate = lengthTillPeriodDueDate == 0 //
|
||||
? BigDecimal.ZERO //
|
||||
: getOutstandingLoanBalance() //
|
||||
.multipliedBy(getRateFactorTillPeriodDueDate(), mc).getAmount() //
|
||||
.divide(BigDecimal.valueOf(lengthTillPeriodDueDate), mc) //
|
||||
.multiply(BigDecimal.valueOf(getLength()), mc); //
|
||||
return Money.of(outstandingLoanBalance.getCurrency(), interestDueTillRepaymentDueDate, mc);
|
||||
}
|
||||
|
||||
@@ -103,8 +106,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
if (isFirstInterestPeriod()) {
|
||||
Optional<RepaymentPeriod> previousRepaymentPeriod = getRepaymentPeriod().getPrevious();
|
||||
if (previousRepaymentPeriod.isPresent()) {
|
||||
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getInterestPeriods()
|
||||
.get(previousRepaymentPeriod.get().getInterestPeriods().size() - 1);
|
||||
InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod();
|
||||
this.outstandingLoanBalance = previousInterestPeriod.getOutstandingLoanBalance()//
|
||||
.plus(previousInterestPeriod.getDisbursementAmount(), mc)//
|
||||
.plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)//
|
||||
@@ -120,7 +122,7 @@ public class InterestPeriod implements Comparable<InterestPeriod> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFirstInterestPeriod() {
|
||||
return getRepaymentPeriod().getInterestPeriods().get(0).equals(this);
|
||||
public boolean isFirstInterestPeriod() {
|
||||
return this.equals(getRepaymentPeriod().getFirstInterestPeriod());
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -83,7 +86,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
|
||||
private BigDecimal findInterestRate(final LocalDate effectiveDate) {
|
||||
return interestRates.stream() //
|
||||
.filter(ir -> !ir.effectiveFrom().isAfter(effectiveDate)) //
|
||||
.filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)) //
|
||||
.map(InterestRate::interestRate) //
|
||||
.findFirst() //
|
||||
.orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); //
|
||||
@@ -93,12 +96,12 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate));
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) {
|
||||
public Optional<RepaymentPeriod> findRepaymentPeriodByDueDate(final LocalDate repaymentPeriodDueDate) {
|
||||
if (repaymentPeriodDueDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return repaymentPeriods.stream()//
|
||||
.filter(repaymentPeriodItem -> repaymentPeriodItem.getDueDate().isEqual(repaymentPeriodDueDate))//
|
||||
.filter(repaymentPeriodItem -> DateUtils.isEqual(repaymentPeriodItem.getDueDate(), repaymentPeriodDueDate))//
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
return repaymentPeriods;
|
||||
}
|
||||
return repaymentPeriods.stream()//
|
||||
.filter(period -> !period.getDueDate().isBefore(calculateFromRepaymentPeriodDueDate))//
|
||||
.filter(period -> !DateUtils.isBefore(period.getDueDate(), calculateFromRepaymentPeriodDueDate))//
|
||||
.toList();//
|
||||
}
|
||||
|
||||
@@ -120,6 +123,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
return DateUtils.getExactDifferenceInDays(firstPeriod.getFromDate(), lastPeriod.getDueDate());
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(0).getFromDate() : null;
|
||||
}
|
||||
|
||||
public LocalDate getMaturityDate() {
|
||||
return !repaymentPeriods.isEmpty() ? repaymentPeriods.get(repaymentPeriods.size() - 1).getDueDate() : null;
|
||||
}
|
||||
@@ -135,6 +142,7 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
if (balanceChangeDate == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// TODO use isInPeriod
|
||||
return repaymentPeriods.stream()//
|
||||
.filter(repaymentPeriod -> {
|
||||
final boolean isFirstPeriod = repaymentPeriod.getPrevious().isEmpty();
|
||||
@@ -214,4 +222,10 @@ public class ProgressiveLoanInterestScheduleModel {
|
||||
public Money getTotalPaidPrincipal() {
|
||||
return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus);
|
||||
}
|
||||
|
||||
public Optional<RepaymentPeriod> findRepaymentPeriod(@NotNull LocalDate transactionDate) {
|
||||
return repaymentPeriods.stream() //
|
||||
.filter(period -> isInPeriod(transactionDate, period.getFromDate(), period.getDueDate(), period.isFirstRepaymentPeriod()))//
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
+41
-17
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.data;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.isInPeriod;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -106,6 +109,7 @@ public class RepaymentPeriod {
|
||||
return interestPeriods.stream().map(InterestPeriod::getRateFactor).reduce(BigDecimal.ONE, BigDecimal::add);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Money getCalculatedDueInterest() {
|
||||
if (calculatedDueInterestCalculation == null) {
|
||||
calculatedDueInterestCalculation = Memo.of(this::calculateCalculatedDueInterest,
|
||||
@@ -123,23 +127,6 @@ public class RepaymentPeriod {
|
||||
return calculatedDueInterest;
|
||||
}
|
||||
|
||||
private Money getZero(MathContext mc) {
|
||||
// EMI is always initiated
|
||||
return this.emi.zero(mc);
|
||||
}
|
||||
|
||||
public Money getCalculatedDuePrincipal() {
|
||||
return getEmi().minus(getCalculatedDueInterest(), mc);
|
||||
}
|
||||
|
||||
public Money getTotalPaidAmount() {
|
||||
return getPaidPrincipal().plus(getPaidInterest());
|
||||
}
|
||||
|
||||
public boolean isFullyPaid() {
|
||||
return getEmi().isEqualTo(getTotalPaidAmount());
|
||||
}
|
||||
|
||||
public Money getDueInterest() {
|
||||
if (dueInterestCalculation == null) {
|
||||
// Due interest might be the maximum paid if there is pay-off or early repayment
|
||||
@@ -150,11 +137,23 @@ public class RepaymentPeriod {
|
||||
return dueInterestCalculation.get();
|
||||
}
|
||||
|
||||
public Money getCalculatedDuePrincipal() {
|
||||
return getEmi().minus(getCalculatedDueInterest(), mc);
|
||||
}
|
||||
|
||||
public Money getDuePrincipal() {
|
||||
// Due principal might be the maximum paid if there is pay-off or early repayment
|
||||
return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false);
|
||||
}
|
||||
|
||||
public Money getTotalPaidAmount() {
|
||||
return getPaidPrincipal().plus(getPaidInterest());
|
||||
}
|
||||
|
||||
public boolean isFullyPaid() {
|
||||
return getEmi().isEqualTo(getTotalPaidAmount());
|
||||
}
|
||||
|
||||
public Money getUnrecognizedInterest() {
|
||||
return getCalculatedDueInterest().minus(getDueInterest(), mc);
|
||||
}
|
||||
@@ -193,4 +192,29 @@ public class RepaymentPeriod {
|
||||
(m1, m2) -> m1.plus(m2, mc));
|
||||
return initialBalance.add(totalDisbursedAmount, mc);
|
||||
}
|
||||
|
||||
private Money getZero(MathContext mc) {
|
||||
// EMI is always initiated
|
||||
return this.emi.zero(mc);
|
||||
}
|
||||
|
||||
public InterestPeriod getFirstInterestPeriod() {
|
||||
return getInterestPeriods().get(0);
|
||||
}
|
||||
|
||||
public InterestPeriod getLastInterestPeriod() {
|
||||
List<InterestPeriod> interestPeriods = getInterestPeriods();
|
||||
return interestPeriods.get(interestPeriods.size() - 1);
|
||||
}
|
||||
|
||||
public Optional<InterestPeriod> findInterestPeriod(@NotNull LocalDate transactionDate) {
|
||||
return interestPeriods.stream() //
|
||||
.filter(interestPeriod -> isInPeriod(transactionDate, interestPeriod.getFromDate(), interestPeriod.getDueDate(),
|
||||
isFirstRepaymentPeriod() && interestPeriod.isFirstInterestPeriod()))//
|
||||
.reduce((one, two) -> two);
|
||||
}
|
||||
|
||||
public boolean isFirstRepaymentPeriod() {
|
||||
return previous == null;
|
||||
}
|
||||
}
|
||||
|
||||
+59
-46
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
|
||||
|
||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper.findInPeriod;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -39,7 +42,6 @@ import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO;
|
||||
@@ -89,7 +91,7 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
// generate list of proposed schedule due dates
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
|
||||
periodStartDate, loanApplicationTerms, holidayDetailDTO);
|
||||
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generateInterestScheduleModel(
|
||||
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetail(),
|
||||
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
|
||||
final List<LoanScheduleModelPeriod> periods = new ArrayList<>(expectedRepaymentPeriods.size());
|
||||
@@ -147,6 +149,61 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
|
||||
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
LocalDate rescheduleFrom) {
|
||||
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
|
||||
return LoanScheduleDTO.from(null, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
|
||||
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
|
||||
}
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
LoanRepaymentScheduleInstallment actualInstallment = findInPeriod(onDate, installments).orElse(installments.get(0));
|
||||
|
||||
LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
|
||||
case TILL_PRE_CLOSURE_DATE -> onDate;
|
||||
case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
};
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), onDate);
|
||||
OutstandingDetails outstandingAmounts = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
|
||||
// TODO: We should add all the past due outstanding amounts as well
|
||||
OutstandingAmountsDTO result = new OutstandingAmountsDTO(currency) //
|
||||
.principal(outstandingAmounts.getOutstandingPrincipal()) //
|
||||
.interest(outstandingAmounts.getOutstandingInterest());//
|
||||
|
||||
installments.forEach(installment -> result //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getPeriodInterestTillDate(@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate targetDate) {
|
||||
Loan loan = installment.getLoan();
|
||||
LoanRepaymentScheduleTransactionProcessor transactionProcessor = loan.getTransactionProcessor();
|
||||
if (!(transactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
|
||||
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
|
||||
}
|
||||
if (installment.isAdditional() || installment.isDownPayment() || installment.isReAged()) {
|
||||
return Money.zero(loan.getCurrency());
|
||||
}
|
||||
ProgressiveLoanInterestScheduleModel model = processor.calculateInterestScheduleModel(loan.getId(), targetDate);
|
||||
return emiCalculator.getPeriodInterestTillDate(model, installment.getDueDate(), targetDate);
|
||||
}
|
||||
|
||||
// Private, internal methods
|
||||
|
||||
private List<DisbursementData> getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) {
|
||||
final List<DisbursementData> disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas());
|
||||
disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate));
|
||||
@@ -238,49 +295,6 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
|
||||
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
|
||||
LocalDate rescheduleFrom) {
|
||||
LoanScheduleModel model = generate(mc, loanApplicationTerms, loan.getActiveCharges(), holidayDetailDTO);
|
||||
return LoanScheduleDTO.from(null, model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
|
||||
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
|
||||
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) {
|
||||
if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) {
|
||||
throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor");
|
||||
}
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||
LoanRepaymentScheduleInstallment actualInstallment = LoanRepaymentScheduleProcessingWrapper.findInPeriod(onDate, installments)
|
||||
.orElse(installments.get(0));
|
||||
|
||||
LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) {
|
||||
case TILL_PRE_CLOSURE_DATE -> onDate;
|
||||
case TILL_REST_FREQUENCY_DATE -> actualInstallment.getDueDate(); // due date of current installment
|
||||
case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE");
|
||||
};
|
||||
|
||||
ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate,
|
||||
loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight();
|
||||
|
||||
OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate);
|
||||
// TODO: We should add all the past due outstanding amounts as well
|
||||
OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) //
|
||||
.principal(result.getOutstandingPrincipal()) //
|
||||
.interest(result.getOutstandingInterest());//
|
||||
|
||||
installments.forEach(installment -> amounts //
|
||||
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
|
||||
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
|
||||
|
||||
return amounts;
|
||||
}
|
||||
|
||||
// Private, internal methods
|
||||
private BigDecimal deriveTotalChargesDueAtTimeOfDisbursement(final Set<LoanCharge> loanCharges) {
|
||||
BigDecimal chargesDueAtTimeOfDisbursement = BigDecimal.ZERO;
|
||||
if (loanCharges != null) {
|
||||
@@ -430,5 +444,4 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
|
||||
}
|
||||
return interestCharges;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+15
-5
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
@@ -34,11 +35,14 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail
|
||||
|
||||
public interface EMICalculator {
|
||||
|
||||
ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
@NotNull
|
||||
ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
@NotNull LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
|
||||
ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc);
|
||||
@NotNull
|
||||
ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
|
||||
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
Integer installmentAmountInMultiplesOf, MathContext mc);
|
||||
|
||||
Optional<RepaymentPeriod> findRepaymentPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate dueDate);
|
||||
|
||||
@@ -56,7 +60,13 @@ public interface EMICalculator {
|
||||
void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate,
|
||||
Money principalAmount);
|
||||
|
||||
PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate);
|
||||
@NotNull
|
||||
PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate);
|
||||
|
||||
@NotNull
|
||||
Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate);
|
||||
|
||||
Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate targetDate);
|
||||
|
||||
+71
-53
@@ -18,14 +18,16 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanproduct.calc;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.Year;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
@@ -42,7 +44,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLo
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
|
||||
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -53,20 +54,36 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(final List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
final LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
final Money zeroAmount = Money.zero(loanProductRelatedDetail.getCurrency(), mc);
|
||||
final ArrayList<RepaymentPeriod> interestRepaymentModelList = new ArrayList<>(periods.size());
|
||||
RepaymentPeriod previousPeriod = null;
|
||||
for (final LoanScheduleModelRepaymentPeriod period : periods) {
|
||||
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, period.periodFromDate(), period.periodDueDate(), zeroAmount,
|
||||
mc);
|
||||
previousPeriod = currentPeriod;
|
||||
interestRepaymentModelList.add(currentPeriod);
|
||||
@NotNull
|
||||
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
|
||||
@NotNull LoanProductRelatedDetail loanProductRelatedDetail, final Integer installmentAmountInMultiplesOf,
|
||||
final MathContext mc) {
|
||||
return generateInterestScheduleModel(periods, LoanScheduleModelRepaymentPeriod::periodFromDate,
|
||||
LoanScheduleModelRepaymentPeriod::periodDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
}
|
||||
return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail,
|
||||
installmentAmountInMultiplesOf, mc);
|
||||
@Override
|
||||
@NotNull
|
||||
public ProgressiveLoanInterestScheduleModel generateInstallmentInterestScheduleModel(
|
||||
@NotNull List<LoanRepaymentScheduleInstallment> installments, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
installments = installments.stream().filter(installment -> !installment.isDownPayment() && !installment.isAdditional()).toList();
|
||||
return generateInterestScheduleModel(installments, LoanRepaymentScheduleInstallment::getFromDate,
|
||||
LoanRepaymentScheduleInstallment::getDueDate, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private <T> ProgressiveLoanInterestScheduleModel generateInterestScheduleModel(@NotNull List<T> periods, Function<T, LocalDate> from,
|
||||
Function<T, LocalDate> to, @NotNull LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
final Integer installmentAmountInMultiplesOf, final MathContext mc) {
|
||||
final Money zero = Money.zero(loanProductRelatedDetail.getCurrency(), mc);
|
||||
final AtomicReference<RepaymentPeriod> prev = new AtomicReference<>();
|
||||
List<RepaymentPeriod> repaymentPeriods = periods.stream().map(e -> {
|
||||
RepaymentPeriod rp = new RepaymentPeriod(prev.get(), from.apply(e), to.apply(e), zero, mc);
|
||||
prev.set(rp);
|
||||
return rp;
|
||||
}).toList();
|
||||
return new ProgressiveLoanInterestScheduleModel(repaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,7 +92,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
if (scheduleModel == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return scheduleModel.findRepaymentPeriod(repaymentPeriodDueDate);
|
||||
return scheduleModel.findRepaymentPeriodByDueDate(repaymentPeriodDueDate);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,13 +156,14 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
@Override
|
||||
public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
LocalDate transactionDate, Money principalAmount) {
|
||||
if (MathUtil.isEmpty(principalAmount)) {
|
||||
return;
|
||||
}
|
||||
Optional<RepaymentPeriod> repaymentPeriod = findRepaymentPeriod(scheduleModel, repaymentPeriodDueDate);
|
||||
repaymentPeriod.ifPresent(rp -> rp.addPaidPrincipalAmount(principalAmount));
|
||||
LocalDate balanceCorrectionDate = transactionDate;
|
||||
if (repaymentPeriodDueDate.isBefore(transactionDate)) {
|
||||
// If it is paid late, we need to calculate with the period due date
|
||||
balanceCorrectionDate = repaymentPeriodDueDate;
|
||||
}
|
||||
// If it is paid late, we need to calculate with the period due date
|
||||
LocalDate balanceCorrectionDate = DateUtils.isBefore(repaymentPeriodDueDate, transactionDate) ? repaymentPeriodDueDate
|
||||
: transactionDate;
|
||||
addBalanceCorrection(scheduleModel, balanceCorrectionDate, principalAmount.negated());
|
||||
|
||||
repaymentPeriod.ifPresent(rp -> {
|
||||
@@ -159,11 +177,12 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate,
|
||||
final LocalDate targetDate) {
|
||||
@NotNull
|
||||
public PeriodDueDetails getDueAmounts(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
periodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
|
||||
boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
|
||||
.count() > 1L;
|
||||
if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
@@ -176,11 +195,21 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate,
|
||||
@NotNull
|
||||
public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate,
|
||||
@NotNull LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
periodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
|
||||
return repaymentPeriod.getCalculatedDueInterest();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate,
|
||||
LocalDate targetDate) {
|
||||
ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel,
|
||||
repaymentPeriodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow();
|
||||
periodDueDate, targetDate);
|
||||
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
|
||||
|
||||
return repaymentPeriod.getOutstandingLoanBalance();
|
||||
}
|
||||
@@ -189,7 +218,7 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
|
||||
// TODO use findInterestPeriod
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
@@ -214,25 +243,26 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel,
|
||||
LocalDate repaymentPeriodDueDate, LocalDate targetDate) {
|
||||
private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(
|
||||
@NotNull ProgressiveLoanInterestScheduleModel scheduleModel, @NotNull LocalDate periodDueDate, @NotNull LocalDate targetDate) {
|
||||
MathContext mc = scheduleModel.mc();
|
||||
ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc);
|
||||
RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream()
|
||||
.filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow();
|
||||
RepaymentPeriod repaymentPeriod = scheduleModelCopy.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
|
||||
|
||||
LocalDate adjustedTargetDate = targetDate;
|
||||
InterestPeriod interestPeriod;
|
||||
if (!targetDate.isAfter(repaymentPeriod.getFromDate())) {
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().get(0);
|
||||
interestPeriod = repaymentPeriod.getFirstInterestPeriod();
|
||||
adjustedTargetDate = repaymentPeriod.getFromDate();
|
||||
} else if (targetDate.isAfter(repaymentPeriod.getDueDate())) {
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().get(repaymentPeriod.getInterestPeriods().size() - 1);
|
||||
interestPeriod = repaymentPeriod.getLastInterestPeriod();
|
||||
adjustedTargetDate = repaymentPeriod.getDueDate();
|
||||
} else {
|
||||
// TODO use findInterestPeriod
|
||||
interestPeriod = repaymentPeriod.getInterestPeriods().stream()
|
||||
.filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow();
|
||||
}
|
||||
// TODO use findInterestPeriod
|
||||
scheduleModelCopy.repaymentPeriods().stream()//
|
||||
.filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()//
|
||||
.flatMap(rp -> rp.getInterestPeriods().stream()//
|
||||
@@ -645,6 +675,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigDecimal repaymentPeriodMultiplierInDays,
|
||||
final BigDecimal repaymentEvery, final BigDecimal daysInYear, final BigDecimal actualDaysInPeriod,
|
||||
final BigDecimal calculatedDaysInPeriod, final MathContext mc) {
|
||||
if (MathUtil.isZero(calculatedDaysInPeriod)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
final BigDecimal interestFractionPerPeriod = repaymentPeriodMultiplierInDays//
|
||||
.multiply(repaymentEvery, mc)//
|
||||
.divide(daysInYear, mc);//
|
||||
@@ -661,6 +694,9 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, final BigDecimal repaymentEvery,
|
||||
final BigDecimal cumulatedPeriodRatio, final BigDecimal actualDaysInPeriod, final BigDecimal calculatedDaysInPeriod,
|
||||
final MathContext mc) {
|
||||
if (MathUtil.isZero(calculatedDaysInPeriod)) {
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
final BigDecimal interestFractionPerPeriod = repaymentEvery.multiply(cumulatedPeriodRatio);
|
||||
return interestRate//
|
||||
.multiply(interestFractionPerPeriod, mc)//
|
||||
@@ -682,24 +718,6 @@ public final class ProgressiveEMICalculator implements EMICalculator {
|
||||
return BigDecimal.ONE.add(previousFnValue.multiply(currentRateFactor, mc), mc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgressiveLoanInterestScheduleModel generateModel(LoanProductRelatedDetail loanProductRelatedDetail,
|
||||
Integer installmentAmountInMultiplesOf, List<LoanRepaymentScheduleInstallment> repaymentPeriods, MathContext mc) {
|
||||
List<LoanRepaymentScheduleInstallment> repaymentModelsWithoutDownPayment = repaymentPeriods.stream()
|
||||
.filter(period -> !period.isDownPayment() && !period.isAdditional()).toList();
|
||||
|
||||
List<RepaymentPeriod> repaymentModels = new ArrayList<>();
|
||||
RepaymentPeriod previousPeriod = null;
|
||||
for (LoanRepaymentScheduleInstallment repaymentModel : repaymentModelsWithoutDownPayment) {
|
||||
RepaymentPeriod currentPeriod = new RepaymentPeriod(previousPeriod, repaymentModel.getFromDate(), repaymentModel.getDueDate(),
|
||||
Money.zero(repaymentModel.getLoan().getCurrency(), mc), mc);
|
||||
previousPeriod = currentPeriod;
|
||||
repaymentModels.add(currentPeriod);
|
||||
}
|
||||
|
||||
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of due interests on interest periods.
|
||||
*
|
||||
|
||||
+5
-3
@@ -60,6 +60,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
@@ -95,6 +96,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
|
||||
private AdvancedPaymentScheduleTransactionProcessor underTest;
|
||||
private static final EMICalculator emiCalculator = Mockito.mock(EMICalculator.class);
|
||||
private static final LoanRepositoryWrapper loanRepositoryWrapper = Mockito.mock(LoanRepositoryWrapper.class);
|
||||
|
||||
@BeforeAll
|
||||
public static void init() {
|
||||
@@ -109,7 +111,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, null);
|
||||
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, null);
|
||||
|
||||
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
|
||||
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
|
||||
@@ -235,7 +237,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanTransaction.getTransactionDate()).thenReturn(transactionDate);
|
||||
when(charge.getAmountOutstanding(currency)).thenReturn(chargeAmountMoney);
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loan.loanCurrency()).thenReturn(currency);
|
||||
when(loan.getCurrency()).thenReturn(currency);
|
||||
when(loanTransaction.getLoan().getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
|
||||
when(loan.getDisbursementDate()).thenReturn(disbursementDate);
|
||||
@@ -451,7 +453,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
|
||||
when(loanProductRelatedDetail.isInterestRecalculationEnabled()).thenReturn(true);
|
||||
|
||||
when(loanTransaction.getLoan()).thenReturn(loan);
|
||||
when(loan.loanCurrency()).thenReturn(currency);
|
||||
when(loan.getCurrency()).thenReturn(currency);
|
||||
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
|
||||
when(loanProductRelatedDetail.getLoanScheduleProcessingType()).thenReturn(LoanScheduleProcessingType.HORIZONTAL);
|
||||
when(loan.getPaymentAllocationRules()).thenReturn(List.of(loanPaymentAllocationRule));
|
||||
|
||||
+48
-61
@@ -158,7 +158,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_generateInterestScheduleModel() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
final Integer installmentAmountInMultiplesOf = null;
|
||||
|
||||
@@ -169,8 +168,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator
|
||||
.generateInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
Assertions.assertTrue(interestScheduleModel != null);
|
||||
Assertions.assertTrue(interestScheduleModel.loanProductRelatedDetail() != null);
|
||||
@@ -183,7 +182,6 @@ class ProgressiveEMICalculatorTest {
|
||||
@Test
|
||||
@Timeout(1) // seconds
|
||||
public void test_emi_calculator_performance() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -209,8 +207,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -234,7 +232,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -254,8 +251,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -271,7 +268,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -291,8 +287,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -308,7 +304,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -328,8 +323,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -355,7 +350,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -375,8 +369,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -392,7 +386,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -414,8 +407,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -435,7 +428,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -457,8 +449,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -504,8 +496,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -529,7 +521,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -551,8 +542,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -576,7 +567,6 @@ class ProgressiveEMICalculatorTest {
|
||||
*/
|
||||
@Test
|
||||
public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -598,8 +588,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -684,7 +674,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -706,8 +695,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -723,9 +712,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest()));
|
||||
|
||||
PeriodDueDetails details = null;
|
||||
// check getdueAmounts forcast
|
||||
details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
// check getDueAmounts forcast
|
||||
PeriodDueDetails details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1));
|
||||
Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal()));
|
||||
Assertions.assertEquals(0.49, toDouble(details.getDueInterest()));
|
||||
|
||||
@@ -757,7 +745,6 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
@Test
|
||||
public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() {
|
||||
|
||||
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = new ArrayList<>();
|
||||
|
||||
expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)));
|
||||
@@ -779,8 +766,8 @@ class ProgressiveEMICalculatorTest {
|
||||
|
||||
threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15));
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -856,8 +843,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
Money disbursedAmount = toMoney(100);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -905,8 +892,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -954,8 +941,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount);
|
||||
@@ -998,8 +985,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount);
|
||||
@@ -1032,8 +1019,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(1000.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -1065,8 +1052,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -1098,8 +1085,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -1131,8 +1118,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(100.0);
|
||||
emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -1163,8 +1150,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount = toMoney(1000.0);
|
||||
emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount);
|
||||
@@ -1225,8 +1212,8 @@ class ProgressiveEMICalculatorTest {
|
||||
Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1);
|
||||
Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency);
|
||||
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods,
|
||||
loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel(
|
||||
expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
|
||||
|
||||
final Money disbursedAmount1st = toMoney(1000.0);
|
||||
final Money disbursedAmount2nd = toMoney(1000.0);
|
||||
|
||||
+27
-29
@@ -36,6 +36,7 @@ import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
||||
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -51,69 +52,66 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
for (final LoanTransactionDTO loanTransactionDTO : loanDTO.getNewLoanTransactions()) {
|
||||
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||
this.helper.checkForBranchClosures(latestGLClosure, transactionDate);
|
||||
final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType();
|
||||
|
||||
/** Handle Disbursements **/
|
||||
if (loanTransactionDTO.getTransactionType().isDisbursement()) {
|
||||
// Handle Disbursements
|
||||
if (transactionType.isDisbursement()) {
|
||||
createJournalEntriesForDisbursements(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/*** Handle Accruals ***/
|
||||
if (loanTransactionDTO.getTransactionType().isAccrual()) {
|
||||
// Handle Accruals
|
||||
if (transactionType.isAccrual() || transactionType.isAccrualAdjustment()) {
|
||||
createJournalEntriesForAccruals(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/***
|
||||
/*
|
||||
* Handle repayments, loan refunds, repayments at disbursement and reversal of Repayments and Repayments at
|
||||
* disbursement (except charge adjustment)
|
||||
***/
|
||||
else if ((loanTransactionDTO.getTransactionType().isRepaymentType()
|
||||
&& !loanTransactionDTO.getTransactionType().isChargeAdjustment())
|
||||
|| loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement()
|
||||
|| loanTransactionDTO.getTransactionType().isChargePayment()) {
|
||||
*/
|
||||
else if ((transactionType.isRepaymentType() && !transactionType.isChargeAdjustment())
|
||||
|| transactionType.isRepaymentAtDisbursement() || transactionType.isChargePayment()) {
|
||||
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, false,
|
||||
loanTransactionDTO.getTransactionType().isRepaymentAtDisbursement());
|
||||
transactionType.isRepaymentAtDisbursement());
|
||||
}
|
||||
|
||||
/** Logic for handling recovery payments **/
|
||||
else if (loanTransactionDTO.getTransactionType().isRecoveryRepayment()) {
|
||||
// Logic for handling recovery payments
|
||||
else if (transactionType.isRecoveryRepayment()) {
|
||||
createJournalEntriesForRecoveryRepayments(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/** Logic for Refunds of Overpayments **/
|
||||
else if (loanTransactionDTO.getTransactionType().isRefund()) {
|
||||
// Logic for Refunds of Overpayments
|
||||
else if (transactionType.isRefund()) {
|
||||
createJournalEntriesForRefund(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/** Logic for Credit Balance Refunds **/
|
||||
else if (loanTransactionDTO.getTransactionType().isCreditBalanceRefund()) {
|
||||
// Logic for Credit Balance Refunds
|
||||
else if (transactionType.isCreditBalanceRefund()) {
|
||||
createJournalEntriesForCreditBalanceRefund(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
|
||||
/** Handle Write Offs, waivers and their reversals **/
|
||||
else if ((loanTransactionDTO.getTransactionType().isWriteOff() || loanTransactionDTO.getTransactionType().isWaiveInterest()
|
||||
|| loanTransactionDTO.getTransactionType().isWaiveCharges())) {
|
||||
// Handle Write Offs, waivers and their reversals
|
||||
else if ((transactionType.isWriteOff() || transactionType.isWaiveInterest() || transactionType.isWaiveCharges())) {
|
||||
createJournalEntriesForRepaymentsAndWriteOffs(loanDTO, loanTransactionDTO, office, true, false);
|
||||
}
|
||||
|
||||
/** Logic for Refunds of Active Loans **/
|
||||
else if (loanTransactionDTO.getTransactionType().isRefundForActiveLoans()) {
|
||||
// Logic for Refunds of Active Loans
|
||||
else if (transactionType.isRefundForActiveLoans()) {
|
||||
createJournalEntriesForRefundForActiveLoan(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Logic for Chargebacks
|
||||
else if (loanTransactionDTO.getTransactionType().isChargeback()) {
|
||||
else if (transactionType.isChargeback()) {
|
||||
createJournalEntriesForChargeback(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Logic for Charge Adjustment
|
||||
else if (loanTransactionDTO.getTransactionType().isChargeAdjustment()) {
|
||||
else if (transactionType.isChargeAdjustment()) {
|
||||
createJournalEntriesForChargeAdjustment(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Logic for Charge-Off
|
||||
else if (loanTransactionDTO.getTransactionType().isChargeoff()) {
|
||||
else if (transactionType.isChargeoff()) {
|
||||
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Logic for Interest Payment Waiver
|
||||
else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()
|
||||
|| loanTransactionDTO.getTransactionType().isInterestRefund()) {
|
||||
else if (transactionType.isInterestPaymentWaiver() || transactionType.isInterestRefund()) {
|
||||
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
@@ -1144,7 +1142,6 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
* @param office
|
||||
*/
|
||||
private void createJournalEntriesForAccruals(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO, final Office office) {
|
||||
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
@@ -1153,10 +1150,11 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
// transaction properties
|
||||
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||
final LoanTransactionEnumData transactionType = loanTransactionDTO.getTransactionType();
|
||||
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
|
||||
final BigDecimal feesAmount = loanTransactionDTO.getFees();
|
||||
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
|
||||
final boolean isReversed = loanTransactionDTO.isReversed();
|
||||
final boolean isReversed = transactionType.isAccrualAdjustment() != loanTransactionDTO.isReversed();
|
||||
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||
|
||||
// create journal entries for recognizing interest (or reversal)
|
||||
|
||||
+1
-1
@@ -190,7 +190,7 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ChargeData> retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) {
|
||||
public List<ChargeData> retrieveLoanAccountApplicableCharges(final Long loanId, ChargeTimeType[] excludeChargeTimes) {
|
||||
final ChargeMapper rm = new ChargeMapper();
|
||||
StringBuilder excludeClause = new StringBuilder("");
|
||||
Map<String, Object> paramMap = new HashMap<>();
|
||||
|
||||
+3
-2
@@ -42,6 +42,7 @@ import jakarta.ws.rs.core.UriInfo;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
@@ -442,7 +443,7 @@ public class LoanChargesApiResource {
|
||||
final LoanChargeData loanCharge = this.loanChargeReadPlatformService.retrieveLoanChargeDetails(resolvedLoanChargeId,
|
||||
resolvedLoanId);
|
||||
|
||||
final Collection<LoanInstallmentChargeData> installmentChargeData = this.loanChargeReadPlatformService
|
||||
final List<LoanInstallmentChargeData> installmentChargeData = this.loanChargeReadPlatformService
|
||||
.retrieveInstallmentLoanCharges(resolvedLoanChargeId, true);
|
||||
|
||||
final LoanChargeData loanChargeData = new LoanChargeData(loanCharge, installmentChargeData);
|
||||
@@ -536,7 +537,7 @@ public class LoanChargesApiResource {
|
||||
ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr);
|
||||
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
|
||||
|
||||
final Collection<ChargeData> chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId,
|
||||
final List<ChargeData> chargeOptions = this.chargeReadPlatformService.retrieveLoanAccountApplicableCharges(resolvedLoanId,
|
||||
new ChargeTimeType[] { ChargeTimeType.OVERDUE_INSTALLMENT });
|
||||
final LoanChargeData loanChargeTemplate = LoanChargeData.template(chargeOptions);
|
||||
|
||||
|
||||
+2
-10
@@ -759,7 +759,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
final ScheduleGeneratorDTO scheduleGeneratorDTO = null;
|
||||
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
|
||||
|
||||
loanAccrualsProcessingService.processAccrualsForLoanForeClosure(loan, foreClosureDate, newTransactions);
|
||||
loanAccrualsProcessingService.processAccrualsOnLoanForeClosure(loan, foreClosureDate, newTransactions);
|
||||
|
||||
Money interestPayable = foreCloseDetail.getInterestCharged(currency);
|
||||
Money feePayable = foreCloseDetail.getFeeChargesCharged(currency);
|
||||
@@ -922,15 +922,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
if (interestRefundTransaction != null) {
|
||||
loan.addLoanTransaction(interestRefundTransaction);
|
||||
}
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(loan);
|
||||
}
|
||||
|
||||
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
|
||||
|
||||
// Store and flush newly created transaction to generate PK
|
||||
saveLoanTransactionWithDataIntegrityViolationChecks(refundTransaction);
|
||||
|
||||
+11
-29
@@ -18,15 +18,12 @@
|
||||
*/
|
||||
package org.apache.fineract.portfolio.loanaccount.jobs.addaccrualentries;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDate;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.infrastructure.core.exception.MultiException;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
@@ -45,30 +42,15 @@ public class AddAccrualEntriesTasklet implements Tasklet {
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
|
||||
Collection<LoanScheduleAccrualData> loanScheduleAccrualDataList = loanReadPlatformService.retriveScheduleAccrualData();
|
||||
Map<Long, Collection<LoanScheduleAccrualData>> loanDataMap = new HashMap<>();
|
||||
for (final LoanScheduleAccrualData accrualData : loanScheduleAccrualDataList) {
|
||||
if (loanDataMap.containsKey(accrualData.getLoanId())) {
|
||||
loanDataMap.get(accrualData.getLoanId()).add(accrualData);
|
||||
} else {
|
||||
Collection<LoanScheduleAccrualData> accrualDataList = new ArrayList<>();
|
||||
accrualDataList.add(accrualData);
|
||||
loanDataMap.put(accrualData.getLoanId(), accrualDataList);
|
||||
}
|
||||
}
|
||||
|
||||
List<Throwable> errors = new ArrayList<>();
|
||||
for (Map.Entry<Long, Collection<LoanScheduleAccrualData>> mapEntry : loanDataMap.entrySet()) {
|
||||
try {
|
||||
loanAccrualsProcessingService.addAccrualAccounting(mapEntry.getKey(), mapEntry.getValue());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add accrual transaction for loan {}", mapEntry.getKey(), e);
|
||||
errors.add(e);
|
||||
}
|
||||
}
|
||||
if (!errors.isEmpty()) {
|
||||
throw new JobExecutionException(errors);
|
||||
try {
|
||||
addAccruals(DateUtils.getBusinessLocalDate());
|
||||
} catch (MultiException e) {
|
||||
throw new JobExecutionException(e);
|
||||
}
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
|
||||
private void addAccruals(final LocalDate tilldate) throws MultiException {
|
||||
loanAccrualsProcessingService.addAccruals(tilldate);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -440,7 +440,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
|
||||
}
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loan.recalculateAllCharges();
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.processTransactions();
|
||||
ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions();
|
||||
|
||||
this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList);
|
||||
|
||||
|
||||
+4
-2
@@ -635,7 +635,8 @@ public final class LoanApplicationValidator {
|
||||
"error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found",
|
||||
"loanIdToClose is invalid, No Active Loan associated with the given Client ID found.");
|
||||
}
|
||||
if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) {
|
||||
if (loanToClose.isMultiDisburmentLoan()
|
||||
&& !loanToClose.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
|
||||
throw new GeneralPlatformDomainRuleException(
|
||||
"error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported",
|
||||
"Topup on loan with multi-tranche disbursal and without interest recalculation is not supported.");
|
||||
@@ -1346,7 +1347,8 @@ public final class LoanApplicationValidator {
|
||||
"error.msg.loan.loanIdToClose.no.active.loan.associated.to.client.found",
|
||||
"loanIdToClose is invalid, No Active Loan associated with the given Client ID found.");
|
||||
}
|
||||
if (loanToClose.isMultiDisburmentLoan() && !loanToClose.isInterestRecalculationEnabledForProduct()) {
|
||||
if (loanToClose.isMultiDisburmentLoan()
|
||||
&& !loanToClose.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
|
||||
throw new GeneralPlatformDomainRuleException(
|
||||
"error.msg.loan.topup.on.multi.tranche.loan.without.interest.recalculation.not.supported",
|
||||
"Topup on loan with multi-tranche disbursal and without interest recalculation is not supported.");
|
||||
|
||||
+814
-1073
File diff suppressed because it is too large
Load Diff
+11
-1
@@ -104,7 +104,7 @@ import org.apache.fineract.portfolio.rate.service.RateAssembler;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
public class LoanAssembler {
|
||||
public class LoanAssemblerImpl implements LoanAssembler {
|
||||
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final LoanRepositoryWrapper loanRepository;
|
||||
@@ -136,6 +136,7 @@ public class LoanAssembler {
|
||||
private final LoanCollateralManagementMapper loanCollateralManagementMapper;
|
||||
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
|
||||
|
||||
@Override
|
||||
public Loan assembleFrom(final Long accountId) {
|
||||
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true);
|
||||
loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
|
||||
@@ -144,11 +145,13 @@ public class LoanAssembler {
|
||||
return loanAccount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHelpers(final Loan loanAccount) {
|
||||
loanAccount.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper,
|
||||
this.loanRepaymentScheduleTransactionProcessorFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loan assembleFrom(final JsonCommand command) {
|
||||
final JsonElement element = command.parsedJson();
|
||||
|
||||
@@ -281,6 +284,7 @@ public class LoanAssembler {
|
||||
|
||||
// TODO: Review... it might be better somewhere else and rethink due to the account number generation logic is
|
||||
// intertwined with GLIM logic
|
||||
@Override
|
||||
public void accountNumberGeneration(JsonCommand command, Loan loan) {
|
||||
if (loan.isAccountNumberRequiresAutoGeneration()) {
|
||||
JsonElement element = command.parsedJson();
|
||||
@@ -371,6 +375,7 @@ public class LoanAssembler {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CodeValue findCodeValueByIdIfProvided(final Long codeValueId) {
|
||||
CodeValue codeValue = null;
|
||||
if (codeValueId != null) {
|
||||
@@ -379,6 +384,7 @@ public class LoanAssembler {
|
||||
return codeValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fund findFundByIdIfProvided(final Long fundId) {
|
||||
Fund fund = null;
|
||||
if (fundId != null) {
|
||||
@@ -387,6 +393,7 @@ public class LoanAssembler {
|
||||
return fund;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Staff findLoanOfficerByIdIfProvided(final Long loanOfficerId) {
|
||||
Staff staff = null;
|
||||
if (loanOfficerId != null) {
|
||||
@@ -419,6 +426,7 @@ public class LoanAssembler {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> updateFrom(JsonCommand command, Loan loan) {
|
||||
final Map<String, Object> changes = new HashMap<>();
|
||||
LoanProduct loanProduct;
|
||||
@@ -844,6 +852,7 @@ public class LoanAssembler {
|
||||
return changes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> updateLoanApplicationAttributesForWithdrawal(Loan loan, JsonCommand command, AppUser currentUser) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||
|
||||
@@ -868,6 +877,7 @@ public class LoanAssembler {
|
||||
return actualChanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> updateLoanApplicationAttributesForRejection(Loan loan, JsonCommand command, AppUser currentUser) {
|
||||
final Map<String, Object> actualChanges = new LinkedHashMap<>();
|
||||
|
||||
+1
-241
@@ -24,9 +24,7 @@ import java.sql.SQLException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.data.EnumOptionData;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
@@ -214,7 +212,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) {
|
||||
public List<LoanInstallmentChargeData> retrieveInstallmentLoanCharges(Long loanChargeId, boolean onlyPaymentPendingCharges) {
|
||||
final LoanInstallmentChargeMapper rm = new LoanInstallmentChargeMapper();
|
||||
String sql = "select " + rm.schema() + "where lic.loan_charge_id= ? ";
|
||||
if (onlyPaymentPendingCharges) {
|
||||
@@ -263,244 +261,6 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform
|
||||
return frequencyNumbers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanChargeData> retrieveLoanChargesForAccrual(final Long loanId) {
|
||||
|
||||
final LoanChargeAccrualMapper rm = new LoanChargeAccrualMapper();
|
||||
|
||||
final String sql = "select " + rm.schema() + " where lc.loan_id=? AND lc.is_active = true group by lc.id "
|
||||
+ " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC";
|
||||
|
||||
Collection<LoanChargeData> charges = this.jdbcTemplate.query(sql, rm, // NOSONAR
|
||||
LoanTransactionType.ACCRUAL.getValue(), loanId, loanId);
|
||||
charges = updateLoanChargesWithUnrecognizedIncome(loanId, charges);
|
||||
|
||||
Collection<LoanChargeData> removeCharges = new ArrayList<>();
|
||||
for (LoanChargeData loanChargeData : charges) {
|
||||
if (loanChargeData.isInstallmentFee()) {
|
||||
removeCharges.add(loanChargeData);
|
||||
}
|
||||
}
|
||||
charges.removeAll(removeCharges);
|
||||
for (LoanChargeData loanChargeData : removeCharges) {
|
||||
if (loanChargeData.isInstallmentFee()) {
|
||||
Collection<LoanInstallmentChargeData> installmentChargeDatas = retrieveInstallmentLoanChargesForAccrual(
|
||||
loanChargeData.getId());
|
||||
LoanChargeData modifiedChargeData = new LoanChargeData(loanChargeData, installmentChargeDatas);
|
||||
charges.add(modifiedChargeData);
|
||||
}
|
||||
}
|
||||
|
||||
return charges;
|
||||
}
|
||||
|
||||
private static final class LoanChargeAccrualMapper implements RowMapper<LoanChargeData> {
|
||||
|
||||
private final String schemaSql;
|
||||
|
||||
LoanChargeAccrualMapper() {
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append(" lc.id as id, lc.charge_id as chargeId, lc.external_id as externalId, ");
|
||||
sb.append(" lc.amount as amountDue, ");
|
||||
sb.append(" lc.amount_waived_derived as amountWaived, ");
|
||||
sb.append(" lc.charge_time_enum as chargeTime, ");
|
||||
sb.append(" sum(cp.amount) as amountAccrued, ");
|
||||
sb.append(" lc.is_penalty as penalty, ");
|
||||
sb.append(" lc.due_for_collection_as_of_date as dueAsOfDate, ");
|
||||
sb.append(" lc.submitted_on_date as submittedOnDate ");
|
||||
sb.append(" from m_loan_charge lc ");
|
||||
sb.append(" left join ( ");
|
||||
sb.append(" select lcp.loan_charge_id, lcp.amount ");
|
||||
sb.append(" from m_loan_charge_paid_by lcp ");
|
||||
sb.append(
|
||||
" inner join m_loan_transaction lt on lt.id = lcp.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ? and lt.loan_id = ? ");
|
||||
sb.append(" ) cp on cp.loan_charge_id= lc.id ");
|
||||
|
||||
schemaSql = sb.toString();
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schemaSql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
|
||||
final Long id = rs.getLong("id");
|
||||
final Long chargeId = rs.getLong("chargeId");
|
||||
final BigDecimal amount = rs.getBigDecimal("amountDue");
|
||||
final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued");
|
||||
final BigDecimal amountWaived = rs.getBigDecimal("amountWaived");
|
||||
|
||||
final int chargeTime = rs.getInt("chargeTime");
|
||||
final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(chargeTime);
|
||||
|
||||
final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate");
|
||||
final LocalDate submittedOnDate = JdbcSupport.getLocalDate(rs, "submittedOnDate");
|
||||
final boolean penalty = rs.getBoolean("penalty");
|
||||
|
||||
final String externalIdStr = rs.getString("externalId");
|
||||
final ExternalId externalId = ExternalIdFactory.produce(externalIdStr);
|
||||
|
||||
return new LoanChargeData(id, chargeId, dueAsOfDate, submittedOnDate, chargeTimeType, amount, amountAccrued, amountWaived,
|
||||
penalty, externalId);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<LoanChargeData> updateLoanChargesWithUnrecognizedIncome(final Long loanId,
|
||||
Collection<LoanChargeData> loanChargeDatas) {
|
||||
|
||||
final LoanChargeUnRecognizedIncomeMapper rm = new LoanChargeUnRecognizedIncomeMapper(loanChargeDatas);
|
||||
|
||||
final String sql = "select " + rm.schema() + " where lc.loan_id=? AND lc.is_active = true group by lc.id "
|
||||
+ " order by lc.charge_time_enum ASC, lc.due_for_collection_as_of_date ASC, lc.is_penalty ASC";
|
||||
|
||||
return this.jdbcTemplate.query(sql, rm, LoanTransactionType.WAIVE_CHARGES.getValue(), loanId, loanId); // NOSONAR
|
||||
}
|
||||
|
||||
private static final class LoanChargeUnRecognizedIncomeMapper implements RowMapper<LoanChargeData> {
|
||||
|
||||
private final String schemaSql;
|
||||
private final Map<Long, LoanChargeData> chargeDataMap;
|
||||
|
||||
LoanChargeUnRecognizedIncomeMapper(final Collection<LoanChargeData> datas) {
|
||||
this.chargeDataMap = new HashMap<>();
|
||||
for (LoanChargeData chargeData : datas) {
|
||||
this.chargeDataMap.put(chargeData.getId(), chargeData);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append("lc.id as id, ");
|
||||
sb.append(" sum(wt.unrecognized_income_portion) as amountUnrecognized ");
|
||||
sb.append(" from m_loan_charge lc ");
|
||||
sb.append("left join (");
|
||||
sb.append("select cpb.loan_charge_id, lt.unrecognized_income_portion");
|
||||
sb.append(" from m_loan_charge_paid_by cpb ");
|
||||
sb.append(
|
||||
"inner join m_loan_transaction lt on lt.id = cpb.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ? and lt.loan_id = ? ");
|
||||
sb.append(") wt on wt.loan_charge_id= lc.id ");
|
||||
|
||||
schemaSql = sb.toString();
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schemaSql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
|
||||
final Long id = rs.getLong("id");
|
||||
final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized");
|
||||
|
||||
LoanChargeData chargeData = this.chargeDataMap.get(id);
|
||||
return new LoanChargeData(amountUnrecognized, chargeData);
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<LoanInstallmentChargeData> retrieveInstallmentLoanChargesForAccrual(Long loanChargeId) {
|
||||
final LoanInstallmentChargeAccrualMapper rm = new LoanInstallmentChargeAccrualMapper();
|
||||
String sql = "select " + rm.schema()
|
||||
+ " where lic.loan_charge_id= ? group by lsi.installment, lsi.duedate, lic.amount_outstanding_derived, lic.amount, lic.is_paid_derived, lic.amount_waived_derived, lic.waived";
|
||||
Collection<LoanInstallmentChargeData> chargeDatas = this.jdbcTemplate.query(sql, rm, // NOSONAR
|
||||
LoanTransactionType.ACCRUAL.getValue(), loanChargeId);
|
||||
final Map<Integer, LoanInstallmentChargeData> installmentChargeDatas = new HashMap<>();
|
||||
for (LoanInstallmentChargeData installmentChargeData : chargeDatas) {
|
||||
installmentChargeDatas.put(installmentChargeData.getInstallmentNumber(), installmentChargeData);
|
||||
}
|
||||
chargeDatas = updateInstallmentLoanChargesWithUnrecognizedIncome(loanChargeId, installmentChargeDatas);
|
||||
for (LoanInstallmentChargeData installmentChargeData : chargeDatas) {
|
||||
installmentChargeDatas.put(installmentChargeData.getInstallmentNumber(), installmentChargeData);
|
||||
}
|
||||
return installmentChargeDatas.values();
|
||||
|
||||
}
|
||||
|
||||
private static final class LoanInstallmentChargeAccrualMapper implements RowMapper<LoanInstallmentChargeData> {
|
||||
|
||||
private final String schemaSql;
|
||||
|
||||
LoanInstallmentChargeAccrualMapper() {
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append(" lsi.installment as installmentNumber, lsi.duedate as dueAsOfDate, ");
|
||||
sb.append("lic.amount_outstanding_derived as amountOutstanding,");
|
||||
sb.append("lic.amount as amount, ");
|
||||
sb.append("lic.is_paid_derived as paid, ");
|
||||
sb.append("lic.amount_waived_derived as amountWaived, ");
|
||||
sb.append(" sum(cp.amount) as amountAccrued, ");
|
||||
sb.append("lic.waived as waived ");
|
||||
sb.append("from m_loan_installment_charge lic ");
|
||||
sb.append("join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id ");
|
||||
sb.append("left join (");
|
||||
sb.append("select lcp.loan_charge_id, lcp.amount as amount, lcp.installment_number ");
|
||||
sb.append(" from m_loan_charge_paid_by lcp ");
|
||||
sb.append(
|
||||
"inner join m_loan_transaction lt on lt.id = lcp.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ?");
|
||||
sb.append(") cp on cp.loan_charge_id= lic.loan_charge_id and cp.installment_number = lsi.installment ");
|
||||
schemaSql = sb.toString();
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schemaSql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Integer installmentNumber = rs.getInt("installmentNumber");
|
||||
final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate");
|
||||
final BigDecimal amountOutstanding = rs.getBigDecimal("amountOutstanding");
|
||||
final BigDecimal amount = rs.getBigDecimal("amount");
|
||||
final BigDecimal amountWaived = rs.getBigDecimal("amountWaived");
|
||||
final boolean paid = rs.getBoolean("paid");
|
||||
final boolean waived = rs.getBoolean("waived");
|
||||
final BigDecimal amountAccrued = rs.getBigDecimal("amountAccrued");
|
||||
|
||||
return LoanInstallmentChargeData.builder().installmentNumber(installmentNumber).dueDate(dueAsOfDate).amount(amount)
|
||||
.amountOutstanding(amountOutstanding).amountWaived(amountWaived).paid(paid).waived(waived).amountAccrued(amountAccrued)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<LoanInstallmentChargeData> updateInstallmentLoanChargesWithUnrecognizedIncome(final Long loanChargeId,
|
||||
final Map<Integer, LoanInstallmentChargeData> installmentChargeDatas) {
|
||||
final LoanInstallmentChargeUnRecognizedIncomeMapper rm = new LoanInstallmentChargeUnRecognizedIncomeMapper(installmentChargeDatas);
|
||||
String sql = "select " + rm.schema() + " where cpb.loan_charge_id = ? group by cpb.installment_number ";
|
||||
return this.jdbcTemplate.query(sql, rm, LoanTransactionType.WAIVE_CHARGES.getValue(), loanChargeId); // NOSONAR
|
||||
}
|
||||
|
||||
private static final class LoanInstallmentChargeUnRecognizedIncomeMapper implements RowMapper<LoanInstallmentChargeData> {
|
||||
|
||||
private final String schemaSql;
|
||||
private final Map<Integer, LoanInstallmentChargeData> installmentChargeDatas;
|
||||
|
||||
LoanInstallmentChargeUnRecognizedIncomeMapper(final Map<Integer, LoanInstallmentChargeData> installmentChargeDatas) {
|
||||
this.installmentChargeDatas = installmentChargeDatas;
|
||||
StringBuilder sb = new StringBuilder(50);
|
||||
sb.append(" cpb.installment_number as installmentNumber, ");
|
||||
sb.append(" sum(lt.unrecognized_income_portion) as amountUnrecognized ");
|
||||
sb.append(" from m_loan_charge_paid_by cpb ");
|
||||
sb.append(
|
||||
"inner join m_loan_transaction lt on lt.id = cpb.loan_transaction_id and lt.is_reversed = false and lt.transaction_type_enum = ?");
|
||||
schemaSql = sb.toString();
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
return this.schemaSql;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
final Integer installmentNumber = rs.getInt("installmentNumber");
|
||||
final BigDecimal amountUnrecognized = rs.getBigDecimal("amountUnrecognized");
|
||||
LoanInstallmentChargeData installmentChargeData = this.installmentChargeDatas.get(installmentNumber);
|
||||
return LoanInstallmentChargeData.builder().installmentNumber(installmentChargeData.getInstallmentNumber())
|
||||
.dueDate(installmentChargeData.getDueDate()).amount(installmentChargeData.getAmount())
|
||||
.amountOutstanding(installmentChargeData.getAmountOutstanding()).amountWaived(installmentChargeData.getAmountWaived())
|
||||
.paid(installmentChargeData.isPaid()).waived(installmentChargeData.isWaived())
|
||||
.amountAccrued(installmentChargeData.getAmountAccrued()).amountUnrecognized(amountUnrecognized).build();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanChargePaidByData> retrieveLoanChargesPaidBy(Long chargeId, final LoanTransactionType transactionType,
|
||||
final Integer installmentNumber) {
|
||||
|
||||
+1
-4
@@ -838,14 +838,11 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
||||
LoanTransactionRelationTypeEnum.CHARGE_ADJUSTMENT);
|
||||
loanChargeAdjustmentTransaction.getLoanTransactionRelations().add(loanTransactionRelation);
|
||||
|
||||
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loan.addLoanTransaction(loanChargeAdjustmentTransaction);
|
||||
if (loan.isInterestBearing() && loan.getLoanProductRelatedDetail().isInterestRecalculationEnabled()) {
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
|
||||
loan.retrieveListOfTransactionsForReprocessing(), loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||
loan.getActiveCharges());
|
||||
loan.reprocessTransactions();
|
||||
} else {
|
||||
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction,
|
||||
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
|
||||
|
||||
-12
@@ -31,14 +31,12 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||
|
||||
@@ -98,16 +96,10 @@ public interface LoanReadPlatformService {
|
||||
|
||||
DisbursementData retrieveLoanDisbursementDetail(Long loanId, Long disbursementId);
|
||||
|
||||
Collection<LoanScheduleAccrualData> retriveScheduleAccrualData();
|
||||
|
||||
LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId);
|
||||
|
||||
LoanTransactionData retrieveLoanWriteoffTemplate(Long loanId);
|
||||
|
||||
Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(LocalDate tillDate);
|
||||
|
||||
Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(LocalDate tillDate, Loan loan);
|
||||
|
||||
LoanTransactionData retrieveLoanChargeOffTemplate(Long loanId);
|
||||
|
||||
Collection<Long> fetchLoansForInterestRecalculation();
|
||||
@@ -116,10 +108,6 @@ public interface LoanReadPlatformService {
|
||||
|
||||
LoanTransactionData retrieveLoanPrePaymentTemplate(LoanTransactionType repaymentTransactionType, Long loanId, LocalDate onDate);
|
||||
|
||||
Collection<LoanTransactionData> retrieveWaiverLoanTransactions(Long loanId);
|
||||
|
||||
Collection<LoanSchedulePeriodData> fetchWaiverInterestRepaymentData(Long loanId);
|
||||
|
||||
boolean isGuaranteeRequired(Long loanId);
|
||||
|
||||
LocalDate retrieveMinimumDateOfRepaymentTransaction(Long loanId);
|
||||
|
||||
-400
@@ -29,18 +29,15 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.accounting.common.AccountingRuleType;
|
||||
import org.apache.fineract.infrastructure.codes.data.CodeValueData;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
@@ -77,7 +74,6 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService;
|
||||
import org.apache.fineract.portfolio.client.data.ClientData;
|
||||
import org.apache.fineract.portfolio.client.domain.ClientEnumerations;
|
||||
import org.apache.fineract.portfolio.client.service.ClientReadPlatformService;
|
||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||
import org.apache.fineract.portfolio.common.service.CommonEnumerations;
|
||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
|
||||
@@ -96,7 +92,6 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData;
|
||||
@@ -1742,258 +1737,6 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
return this.jdbcTemplate.queryForObject(sql, rm, loanId, disbursementId); // NOSONAR
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanScheduleAccrualData> retriveScheduleAccrualData() {
|
||||
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
|
||||
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
|
||||
return retrieveScheduleAccrualDataForChargeSubmittedDateProcessing();
|
||||
}
|
||||
return retrieveScheduleAccrualDataForDefaultProcessing();
|
||||
}
|
||||
|
||||
private Collection<LoanScheduleAccrualData> retrieveScheduleAccrualDataForDefaultProcessing() {
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
|
||||
Map<String, Object> paramMap = new HashMap<>(3);
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false and loan.is_charged_off = false and ls.duedate <= :currentDate) ");
|
||||
|
||||
if (organisationStartDate != null) {
|
||||
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
|
||||
}
|
||||
sqlBuilder.append(" order by loan.id,ls.duedate ");
|
||||
|
||||
paramMap.put("active", LoanStatus.ACTIVE.getValue());
|
||||
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
|
||||
paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate);
|
||||
paramMap.put("currentDate", DateUtils.getBusinessLocalDate());
|
||||
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
|
||||
|
||||
}
|
||||
|
||||
private Collection<LoanScheduleAccrualData> retrieveScheduleAccrualDataForChargeSubmittedDateProcessing() {
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
LoanScheduleAccrualMapper mapper = new LoanScheduleAccrualMapper();
|
||||
Map<String, Object> paramMap = new HashMap<>(3);
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or ( ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or ( ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and loan.is_npa=false and loan.is_charged_off = false) ");
|
||||
|
||||
if (organisationStartDate != null) {
|
||||
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
|
||||
}
|
||||
sqlBuilder.append(" order by loan.id,ls.duedate ");
|
||||
|
||||
paramMap.put("active", LoanStatus.ACTIVE.getValue());
|
||||
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
|
||||
paramMap.put("organisationStartDate", (organisationStartDate == null) ? DateUtils.getBusinessLocalDate() : organisationStartDate);
|
||||
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(final LocalDate tillDate) {
|
||||
return retrievePeriodicAccrualData(tillDate, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanScheduleAccrualData> retrievePeriodicAccrualData(final LocalDate tillDate, final Loan loan) {
|
||||
final String chargeAccrualDateCriteria = configurationDomainService.getAccrualDateConfigForCharge();
|
||||
if (chargeAccrualDateCriteria.equalsIgnoreCase(ACCRUAL_ON_CHARGE_SUBMITTED_ON_DATE)) {
|
||||
return retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(tillDate, loan);
|
||||
}
|
||||
return retrievePeriodicAccrualDataForDefaultProcessing(tillDate, loan);
|
||||
}
|
||||
|
||||
private Collection<LoanScheduleAccrualData> retrievePeriodicAccrualDataForDefaultProcessing(final LocalDate tillDate, final Loan loan) {
|
||||
LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper();
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and (loan.closedon_date <= :tillDate or loan.closedon_date is null)")
|
||||
.append(" and loan.is_npa=false and loan.is_charged_off = false and (ls.duedate <= :tillDate or (ls.duedate > :tillDate and ls.fromdate < :tillDate)")
|
||||
.append(" or (ls.installment = 1 and ls.fromdate = :tillDate))) ");
|
||||
Map<String, Object> paramMap = new HashMap<>(5);
|
||||
if (organisationStartDate != null) {
|
||||
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
|
||||
paramMap.put("organisationStartDate", organisationStartDate);
|
||||
}
|
||||
if (loan != null) {
|
||||
sqlBuilder.append(" and loan.id= :loanId ");
|
||||
paramMap.put("loanId", loan.getId());
|
||||
}
|
||||
sqlBuilder.append(" order by loan.id,ls.duedate ");
|
||||
paramMap.put("active", LoanStatus.ACTIVE.getValue());
|
||||
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
|
||||
paramMap.put("tillDate", tillDate);
|
||||
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
|
||||
}
|
||||
|
||||
private Collection<LoanScheduleAccrualData> retrievePeriodicAccrualDataForChargeSubmittedDateProcessing(final LocalDate tillDate,
|
||||
final Loan loan) {
|
||||
LoanSchedulePeriodicAccrualMapper mapper = new LoanSchedulePeriodicAccrualMapper();
|
||||
LocalDate organisationStartDate = this.configurationDomainService.retrieveOrganisationStartDate();
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("select ").append(mapper.schema()).append(
|
||||
" where (recaldet.is_compounding_to_be_posted_as_transaction is null or recaldet.is_compounding_to_be_posted_as_transaction = false) ")
|
||||
.append(" and (((ls.fee_charges_amount <> COALESCE(ls.accrual_fee_charges_derived, 0))")
|
||||
.append(" or (ls.penalty_charges_amount <> COALESCE(ls.accrual_penalty_charges_derived, 0))")
|
||||
.append(" or (ls.interest_amount <> COALESCE(ls.accrual_interest_derived, 0)))")
|
||||
.append(" and loan.loan_status_id=:active and mpl.accounting_type=:type and (loan.closedon_date <= :tillDate or loan.closedon_date is null)")
|
||||
.append(" and loan.is_npa=false and loan.is_charged_off = false)");
|
||||
Map<String, Object> paramMap = new HashMap<>(5);
|
||||
if (organisationStartDate != null) {
|
||||
sqlBuilder.append(" and ls.duedate > :organisationStartDate ");
|
||||
paramMap.put("organisationStartDate", organisationStartDate);
|
||||
}
|
||||
if (loan != null) {
|
||||
sqlBuilder.append(" and loan.id= :loanId ");
|
||||
paramMap.put("loanId", loan.getId());
|
||||
}
|
||||
sqlBuilder.append(" order by loan.id,ls.duedate ");
|
||||
paramMap.put("active", LoanStatus.ACTIVE.getValue());
|
||||
paramMap.put("type", AccountingRuleType.ACCRUAL_PERIODIC.getValue());
|
||||
paramMap.put("tillDate", tillDate);
|
||||
return this.namedParameterJdbcTemplate.query(sqlBuilder.toString(), paramMap, mapper);
|
||||
}
|
||||
|
||||
private static final class LoanSchedulePeriodicAccrualMapper implements RowMapper<LoanScheduleAccrualData> {
|
||||
|
||||
public String schema() {
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("loan.id as loanId , (CASE WHEN loan.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId,")
|
||||
.append("loan.accrued_till as accruedTill, loan.repayment_period_frequency_enum as frequencyEnum, ")
|
||||
.append("loan.interest_calculated_from_date as interestCalculatedFrom, ").append("loan.repay_every as repayEvery,")
|
||||
.append("ls.installment as installmentNumber, ")
|
||||
.append("ls.duedate as duedate,ls.fromdate as fromdate ,ls.id as scheduleId,loan.product_id as productId,")
|
||||
.append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,")
|
||||
.append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ")
|
||||
.append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ")
|
||||
.append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,")
|
||||
.append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,")
|
||||
.append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode")
|
||||
.append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ")
|
||||
.append(" left join m_product_loan mpl on mpl.id = loan.product_id")
|
||||
.append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id")
|
||||
.append(" left join m_currency curr on curr.code = loan.currency_code")
|
||||
.append(" left join m_loan_recalculation_details as recaldet on loan.id = recaldet.loan_id ");
|
||||
return sqlBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||
|
||||
final Long loanId = rs.getLong("loanId");
|
||||
final Long officeId = rs.getLong("officeId");
|
||||
final LocalDate accruedTill = JdbcSupport.getLocalDate(rs, "accruedTill");
|
||||
final LocalDate interestCalculatedFrom = JdbcSupport.getLocalDate(rs, "interestCalculatedFrom");
|
||||
final Integer installmentNumber = JdbcSupport.getInteger(rs, "installmentNumber");
|
||||
|
||||
final Integer frequencyEnum = JdbcSupport.getInteger(rs, "frequencyEnum");
|
||||
final Integer repayEvery = JdbcSupport.getInteger(rs, "repayEvery");
|
||||
final PeriodFrequencyType frequency = PeriodFrequencyType.fromInt(frequencyEnum);
|
||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate");
|
||||
final LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromdate");
|
||||
final Long repaymentScheduleId = rs.getLong("scheduleId");
|
||||
final Long loanProductId = rs.getLong("productId");
|
||||
final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest");
|
||||
final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges");
|
||||
final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty");
|
||||
final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived");
|
||||
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
|
||||
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
|
||||
final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty");
|
||||
final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee");
|
||||
final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty");
|
||||
|
||||
final String currencyCode = rs.getString("currencyCode");
|
||||
final String currencyName = rs.getString("currencyName");
|
||||
final String currencyNameCode = rs.getString("currencyNameCode");
|
||||
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
|
||||
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
|
||||
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
|
||||
final CurrencyData currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
|
||||
currencyDisplaySymbol, currencyNameCode);
|
||||
|
||||
return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromDate,
|
||||
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome,
|
||||
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LoanScheduleAccrualMapper implements RowMapper<LoanScheduleAccrualData> {
|
||||
|
||||
public String schema() {
|
||||
final StringBuilder sqlBuilder = new StringBuilder(400);
|
||||
sqlBuilder.append("loan.id as loanId, (CASE WHEN loan.client_id is null THEN mg.office_id ELSE mc.office_id END) as officeId,")
|
||||
.append("ls.duedate as duedate,ls.fromdate as fromdate,ls.id as scheduleId,loan.product_id as productId,")
|
||||
.append("ls.installment as installmentNumber, ")
|
||||
.append("ls.interest_amount as interest, ls.interest_waived_derived as interestWaived,")
|
||||
.append("ls.penalty_charges_amount as penalty, ").append("ls.fee_charges_amount as charges, ")
|
||||
.append("ls.credited_penalty as credited_penalty, ").append("ls.credited_fee as credited_fee, ")
|
||||
.append("ls.accrual_interest_derived as accinterest,ls.accrual_fee_charges_derived as accfeecharege,ls.accrual_penalty_charges_derived as accpenalty,")
|
||||
.append(" loan.currency_code as currencyCode,loan.currency_digits as currencyDigits,loan.currency_multiplesof as inMultiplesOf,")
|
||||
.append("curr.display_symbol as currencyDisplaySymbol,curr.name as currencyName,curr.internationalized_name_code as currencyNameCode")
|
||||
.append(" from m_loan_repayment_schedule ls ").append(" left join m_loan loan on loan.id=ls.loan_id ")
|
||||
.append(" left join m_product_loan mpl on mpl.id = loan.product_id")
|
||||
.append(" left join m_client mc on mc.id = loan.client_id ").append(" left join m_group mg on mg.id = loan.group_id")
|
||||
.append(" left join m_currency curr on curr.code = loan.currency_code")
|
||||
.append(" left join m_loan_recalculation_details as recaldet on loan.id = recaldet.loan_id ");
|
||||
return sqlBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanScheduleAccrualData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||
|
||||
final Long loanId = rs.getLong("loanId");
|
||||
final Long officeId = rs.getLong("officeId");
|
||||
final Integer installmentNumber = JdbcSupport.getInteger(rs, "installmentNumber");
|
||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "duedate");
|
||||
final LocalDate fromdate = JdbcSupport.getLocalDate(rs, "fromdate");
|
||||
final Long repaymentScheduleId = rs.getLong("scheduleId");
|
||||
final Long loanProductId = rs.getLong("productId");
|
||||
final BigDecimal interestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interest");
|
||||
final BigDecimal feeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "charges");
|
||||
final BigDecimal penaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "penalty");
|
||||
final BigDecimal creditedFee = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_fee");
|
||||
final BigDecimal creditedPenalty = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "credited_penalty");
|
||||
|
||||
final BigDecimal interestIncomeWaived = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "interestWaived");
|
||||
final BigDecimal accruedInterestIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accinterest");
|
||||
final BigDecimal accruedFeeIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accfeecharege");
|
||||
final BigDecimal accruedPenaltyIncome = JdbcSupport.getBigDecimalDefaultToNullIfZero(rs, "accpenalty");
|
||||
|
||||
final String currencyCode = rs.getString("currencyCode");
|
||||
final String currencyName = rs.getString("currencyName");
|
||||
final String currencyNameCode = rs.getString("currencyNameCode");
|
||||
final String currencyDisplaySymbol = rs.getString("currencyDisplaySymbol");
|
||||
final Integer currencyDigits = JdbcSupport.getInteger(rs, "currencyDigits");
|
||||
final Integer inMultiplesOf = JdbcSupport.getInteger(rs, "inMultiplesOf");
|
||||
final CurrencyData currencyData = new CurrencyData(currencyCode, currencyName, currencyDigits, inMultiplesOf,
|
||||
currencyDisplaySymbol, currencyNameCode);
|
||||
final LocalDate accruedTill = null;
|
||||
final PeriodFrequencyType frequency = null;
|
||||
final Integer repayEvery = null;
|
||||
final LocalDate interestCalculatedFrom = null;
|
||||
return new LoanScheduleAccrualData(loanId, officeId, installmentNumber, accruedTill, frequency, repayEvery, dueDate, fromdate,
|
||||
repaymentScheduleId, loanProductId, interestIncome, feeIncome, penaltyIncome, accruedInterestIncome, accruedFeeIncome,
|
||||
accruedPenaltyIncome, currencyData, interestCalculatedFrom, interestIncomeWaived, creditedFee, creditedPenalty);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanTransactionData retrieveRecoveryPaymentTemplate(Long loanId) {
|
||||
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||
@@ -2146,155 +1889,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanTransactionData> retrieveWaiverLoanTransactions(final Long loanId) {
|
||||
try {
|
||||
|
||||
final LoanTransactionDerivedComponentMapper rm = new LoanTransactionDerivedComponentMapper(sqlGenerator);
|
||||
|
||||
final String sql = "select " + rm.schema()
|
||||
+ " where tr.loan_id = ? and tr.transaction_type_enum = ? and tr.is_reversed=false order by tr.transaction_date, tr.created_on_utc, tr.id ";
|
||||
return this.jdbcTemplate.query(sql, rm, loanId, LoanTransactionType.WAIVE_INTEREST.getValue()); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isGuaranteeRequired(final Long loanId) {
|
||||
final String sql = "select pl.hold_guarantee_funds from m_loan ml inner join m_product_loan pl on pl.id = ml.product_id where ml.id=?";
|
||||
return TRUE.equals(this.jdbcTemplate.queryForObject(sql, Boolean.class, loanId));
|
||||
}
|
||||
|
||||
private static final class LoanTransactionDerivedComponentMapper implements RowMapper<LoanTransactionData> {
|
||||
|
||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
|
||||
LoanTransactionDerivedComponentMapper(DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
this.sqlGenerator = sqlGenerator;
|
||||
}
|
||||
|
||||
public String schema() {
|
||||
|
||||
return " tr.id as id, tr.transaction_type_enum as transactionType, tr.transaction_date as " + sqlGenerator.escape("date")
|
||||
+ ", tr.amount as total, tr.principal_portion_derived as principal, tr.interest_portion_derived as interest, "
|
||||
+ " tr.fee_charges_portion_derived as fees, tr.penalty_charges_portion_derived as penalties, "
|
||||
+ " tr.overpayment_portion_derived as overpayment, tr.outstanding_loan_balance_derived as outstandingLoanBalance, "
|
||||
+ " tr.unrecognized_income_portion as unrecognizedIncome, tr.loan_id as loanId, l.external_id as externalLoanId, "
|
||||
+ " tr.external_id as externalId from m_loan_transaction tr " + " left join m_loan l on tr.loan_id = l.id";
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanTransactionData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException {
|
||||
|
||||
final Long id = rs.getLong("id");
|
||||
final Long loanId = rs.getLong("loanId");
|
||||
final String externalLoanIdStr = rs.getString("externalLoanId");
|
||||
final ExternalId externalLoanId = ExternalIdFactory.produce(externalLoanIdStr);
|
||||
final int transactionTypeInt = JdbcSupport.getInteger(rs, "transactionType");
|
||||
final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(transactionTypeInt);
|
||||
|
||||
final LocalDate date = JdbcSupport.getLocalDate(rs, "date");
|
||||
final BigDecimal totalAmount = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "total");
|
||||
final BigDecimal principalPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principal");
|
||||
final BigDecimal interestPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interest");
|
||||
final BigDecimal feeChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "fees");
|
||||
final BigDecimal penaltyChargesPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "penalties");
|
||||
final BigDecimal overPaymentPortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "overpayment");
|
||||
final BigDecimal unrecognizedIncomePortion = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "unrecognizedIncome");
|
||||
final BigDecimal outstandingLoanBalance = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "outstandingLoanBalance");
|
||||
final String externalIdStr = rs.getString("externalId");
|
||||
final ExternalId externalId = ExternalIdFactory.produce(externalIdStr);
|
||||
|
||||
return new LoanTransactionData(id, transactionType, date, totalAmount, null, principalPortion, interestPortion,
|
||||
feeChargesPortion, penaltyChargesPortion, overPaymentPortion, unrecognizedIncomePortion, outstandingLoanBalance, false,
|
||||
externalId, loanId, externalLoanId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<LoanSchedulePeriodData> fetchWaiverInterestRepaymentData(final Long loanId) {
|
||||
try {
|
||||
|
||||
final LoanRepaymentWaiverMapper rm = new LoanRepaymentWaiverMapper();
|
||||
|
||||
final String sql = "select " + rm.getSchema()
|
||||
+ " where lrs.loan_id = ? and lrs.interest_waived_derived is not null order by lrs.installment ASC ";
|
||||
return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class LoanRepaymentWaiverMapper implements RowMapper<LoanSchedulePeriodData> {
|
||||
|
||||
private final String sqlSchema;
|
||||
|
||||
public String getSchema() {
|
||||
return this.sqlSchema;
|
||||
}
|
||||
|
||||
LoanRepaymentWaiverMapper() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("lrs.duedate as dueDate,lrs.interest_waived_derived interestWaived, lrs.installment as installment");
|
||||
sb.append(" from m_loan_repayment_schedule lrs ");
|
||||
sqlSchema = sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LoanSchedulePeriodData mapRow(ResultSet rs, @SuppressWarnings("unused") int rowNum) throws SQLException {
|
||||
|
||||
final Integer period = JdbcSupport.getInteger(rs, "installment");
|
||||
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
|
||||
final BigDecimal interestWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "interestWaived");
|
||||
|
||||
final LocalDate fromDate = null;
|
||||
final LocalDate obligationsMetOnDate = null;
|
||||
final Boolean complete = false;
|
||||
final BigDecimal principalOriginalDue = null;
|
||||
final BigDecimal principalPaid = null;
|
||||
final BigDecimal principalWrittenOff = null;
|
||||
final BigDecimal principalOutstanding = null;
|
||||
final BigDecimal interestPaid = null;
|
||||
final BigDecimal interestWrittenOff = null;
|
||||
final BigDecimal interestOutstanding = null;
|
||||
final BigDecimal feeChargesDue = null;
|
||||
final BigDecimal feeChargesPaid = null;
|
||||
final BigDecimal feeChargesWaived = null;
|
||||
final BigDecimal feeChargesWrittenOff = null;
|
||||
final BigDecimal feeChargesOutstanding = null;
|
||||
final BigDecimal penaltyChargesDue = null;
|
||||
final BigDecimal penaltyChargesPaid = null;
|
||||
final BigDecimal penaltyChargesWaived = null;
|
||||
final BigDecimal penaltyChargesWrittenOff = null;
|
||||
final BigDecimal penaltyChargesOutstanding = null;
|
||||
|
||||
final BigDecimal totalDueForPeriod = null;
|
||||
final BigDecimal totalPaidInAdvanceForPeriod = null;
|
||||
final BigDecimal totalPaidLateForPeriod = null;
|
||||
final BigDecimal totalActualCostOfLoanForPeriod = null;
|
||||
final BigDecimal outstandingPrincipalBalanceOfLoan = null;
|
||||
final BigDecimal interestDueOnPrincipalOutstanding = null;
|
||||
Long loanId = null;
|
||||
final BigDecimal totalWaived = null;
|
||||
final BigDecimal totalWrittenOff = null;
|
||||
final BigDecimal totalOutstanding = null;
|
||||
final BigDecimal totalPaid = null;
|
||||
final BigDecimal totalInstallmentAmount = null;
|
||||
final BigDecimal totalCredits = null;
|
||||
final BigDecimal totalAccruedInterest = null;
|
||||
|
||||
return LoanSchedulePeriodData.periodWithPayments(loanId, period, fromDate, dueDate, obligationsMetOnDate, complete,
|
||||
principalOriginalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
|
||||
interestDueOnPrincipalOutstanding, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesDue,
|
||||
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
|
||||
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid,
|
||||
totalPaidInAdvanceForPeriod, totalPaidLateForPeriod, totalWaived, totalWrittenOff, totalOutstanding,
|
||||
totalActualCostOfLoanForPeriod, totalInstallmentAmount, totalCredits, false, totalAccruedInterest);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDate retrieveMinimumDateOfRepaymentTransaction(Long loanId) {
|
||||
return this.jdbcTemplate.queryForObject(
|
||||
|
||||
+13
-8
@@ -25,6 +25,7 @@ import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -47,12 +48,15 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
|
||||
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
log.debug("Loan Status change for loan {}", loan.getId());
|
||||
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
|
||||
log.debug("Loan Status {} for loan {}", loan.getStatus().getCode(), loan.getId());
|
||||
loanAccrualsProcessingService.processAccrualsForLoanClosure(loan);
|
||||
}
|
||||
if (loan.isOpen()) {
|
||||
LoanStatus oldStatus = event.getOldStatus();
|
||||
LoanStatus newStatus = loan.getStatus();
|
||||
if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) {
|
||||
log.debug("Loan Status {} for loan {}", newStatus.getCode(), loan.getId());
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
|
||||
} else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
|
||||
loan.handleMaturityDateActivate();
|
||||
loanAccrualsProcessingService.processAccrualsOnLoanReopen(loan);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,10 +68,11 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
|
||||
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
|
||||
final Loan loan = event.get();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
|
||||
if (loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid()) {
|
||||
LoanStatus oldStatus = event.getOldStatus();
|
||||
LoanStatus newStatus = loan.getStatus();
|
||||
if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) {
|
||||
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
|
||||
}
|
||||
if (loan.isOpen()) {
|
||||
} else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
|
||||
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-25
@@ -597,15 +597,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
if (loan.getLoanProduct().isMultiDisburseLoan()) {
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
|
||||
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory
|
||||
.determineProcessor(loan.getTransactionProcessingStrategyCode());
|
||||
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
|
||||
allNonContraTransactionsPostDisbursement, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||
loan.getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(loan);
|
||||
}
|
||||
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
changedTransactionDetail = loan.reprocessTransactions();
|
||||
}
|
||||
loan.updateLoanSummaryDerivedFields();
|
||||
}
|
||||
@@ -1023,7 +1015,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
// FIXME - kw - update account balance to negative amount.
|
||||
handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else if (loan.getSummary().isRepaidInFull(loan.loanCurrency())) {
|
||||
} else if (loan.getSummary().isRepaidInFull(loan.getCurrency())) {
|
||||
handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine);
|
||||
statusChanged = true;
|
||||
} else {
|
||||
@@ -1077,7 +1069,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
final boolean isTransactionChronologicallyLatest = loan
|
||||
.isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction);
|
||||
|
||||
if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) {
|
||||
if (newInterestPaymentWaiverTransaction.isNotZero()) {
|
||||
loan.addLoanTransaction(newInterestPaymentWaiverTransaction);
|
||||
}
|
||||
|
||||
@@ -1133,19 +1125,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
}
|
||||
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
|
||||
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
|
||||
loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
|
||||
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
|
||||
mapEntry.getValue().updateLoan(loan);
|
||||
}
|
||||
/***
|
||||
* Commented since throwing exception if external id present for one of the transactions. for this need to save
|
||||
* the reversed transactions first and then new transactions.
|
||||
*/
|
||||
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
|
||||
return changedTransactionDetail;
|
||||
return loan.reprocessTransactions();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -1503,7 +1483,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
|
||||
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
|
||||
}
|
||||
|
||||
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero(loan.getPrincipal().getCurrency());
|
||||
boolean thereIsNewTransaction = newTransactionDetail.isGreaterThanZero();
|
||||
if (thereIsNewTransaction) {
|
||||
if (paymentDetail != null) {
|
||||
this.paymentDetailWritePlatformService.persistPaymentDetail(paymentDetail);
|
||||
|
||||
+3
-2
@@ -30,6 +30,7 @@ import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
|
||||
@@ -60,7 +61,7 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
|
||||
List<LoanTransaction> collect) {
|
||||
collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(), lt.getAmount(),
|
||||
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null));
|
||||
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (lt.getTypeOf().isDisbursement() && MathUtil.isGreaterThanZero(refundFinal.get())) {
|
||||
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
|
||||
collect.add(
|
||||
new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), REPAYMENT.getValue(), lt.getDateOf(), lt.getAmount(),
|
||||
@@ -97,7 +98,7 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
|
||||
}
|
||||
|
||||
private boolean isTransactionNeededForInterestRefundCalculations(LoanTransaction lt) {
|
||||
return lt.isNotReversed() && !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund();
|
||||
return lt.isNotReversed() && !lt.isAccrualRelated() && !lt.isInterestRefund();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-12
@@ -24,7 +24,6 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
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;
|
||||
@@ -122,8 +121,7 @@ public class LoanReAgingServiceImpl {
|
||||
}
|
||||
reverseReAgeTransaction(reAgeTransaction, command);
|
||||
loanTransactionRepository.saveAndFlush(reAgeTransaction);
|
||||
|
||||
reProcessLoanTransactions(reAgeTransaction.getLoan());
|
||||
loan.reprocessTransactions();
|
||||
loan.updateLoanScheduleDependentDerivedFields();
|
||||
persistNote(loan, command, changes);
|
||||
|
||||
@@ -179,15 +177,6 @@ public class LoanReAgingServiceImpl {
|
||||
return new LoanReAgeParameter(reAgeTransaction, periodFrequencyType, periodFrequencyNumber, startDate, numberOfInstallments);
|
||||
}
|
||||
|
||||
private void reProcessLoanTransactions(Loan loan) {
|
||||
final List<LoanTransaction> filteredTransactions = loan.retrieveListOfTransactionsForReprocessing();
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions,
|
||||
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
}
|
||||
|
||||
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
|
||||
if (command.hasParameter("note")) {
|
||||
final String note = command.stringValueOfParameterNamed("note");
|
||||
|
||||
+1
-11
@@ -24,7 +24,6 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
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;
|
||||
@@ -129,16 +128,7 @@ public class LoanReAmortizationServiceImpl {
|
||||
LoanReAmortizationApiConstants.externalIdParameterName);
|
||||
reAmortizeTransaction.reverse(reversalExternalId);
|
||||
reAmortizeTransaction.manuallyAdjustedOrReversed();
|
||||
reProcessLoanTransactions(reAmortizeTransaction.getLoan());
|
||||
}
|
||||
|
||||
private void reProcessLoanTransactions(Loan loan) {
|
||||
final List<LoanTransaction> filteredTransactions = loan.retrieveListOfTransactionsForReprocessing();
|
||||
|
||||
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||
.determineProcessor(loan.transactionProcessingStrategy());
|
||||
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions,
|
||||
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||
reAmortizeTransaction.getLoan().reprocessTransactions();
|
||||
}
|
||||
|
||||
private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) {
|
||||
|
||||
+3
-2
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
|
||||
|
||||
import java.util.List;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
|
||||
@@ -107,8 +108,8 @@ public class LoanAccountAutoStarter {
|
||||
@Bean
|
||||
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
||||
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator,
|
||||
LoanRepositoryWrapper loanRepositoryWrapper,
|
||||
@Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService) {
|
||||
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService);
|
||||
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, loanRepositoryWrapper, progressiveLoanInterestRefundService);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -102,6 +102,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanApplicationWritePla
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingServiceImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
|
||||
@@ -217,7 +218,7 @@ public class LoanAccountConfiguration {
|
||||
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
|
||||
LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper,
|
||||
LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) {
|
||||
return new LoanAssembler(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
|
||||
return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
|
||||
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
|
||||
loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService,
|
||||
workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository,
|
||||
|
||||
+2
-2
@@ -105,7 +105,7 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -191,7 +191,7 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
+10
-3
@@ -177,13 +177,20 @@ public class LoanTest {
|
||||
|
||||
final LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class);
|
||||
when(loanTransaction.isNotReversed()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction.isAccrualTransaction()).thenReturn(Boolean.FALSE);
|
||||
when(loanTransaction.isAccrual()).thenReturn(Boolean.FALSE);
|
||||
when(loanTransaction.isAccrualAdjustment()).thenReturn(Boolean.FALSE);
|
||||
final LoanTransaction loanTransaction2 = Mockito.mock(LoanTransaction.class);
|
||||
when(loanTransaction2.isNotReversed()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction2.isAccrualTransaction()).thenReturn(Boolean.FALSE);
|
||||
when(loanTransaction2.isAccrual()).thenReturn(Boolean.FALSE);
|
||||
when(loanTransaction2.isAccrualAdjustment()).thenReturn(Boolean.FALSE);
|
||||
final LoanTransaction loanTransaction3 = Mockito.mock(LoanTransaction.class);
|
||||
when(loanTransaction3.isNotReversed()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction3.isAccrualTransaction()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction3.isAccrual()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction3.isAccrualAdjustment()).thenReturn(Boolean.FALSE);
|
||||
final LoanTransaction loanTransaction4 = Mockito.mock(LoanTransaction.class);
|
||||
when(loanTransaction4.isNotReversed()).thenReturn(Boolean.TRUE);
|
||||
when(loanTransaction4.isAccrual()).thenReturn(Boolean.FALSE);
|
||||
when(loanTransaction4.isAccrualAdjustment()).thenReturn(Boolean.TRUE);
|
||||
ReflectionTestUtils.setField(loan, "loanTransactions", List.of(loanTransaction, loanTransaction2, loanTransaction3));
|
||||
final LoanTransaction userTransaction = loan.getLastUserTransaction();
|
||||
assertNotNull(userTransaction);
|
||||
|
||||
+2
-1
@@ -5145,7 +5145,8 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
|
||||
for (int i = 1; i < loanSchedule.size(); i++) {
|
||||
|
||||
retrieveDueDate = dateFormat.format(repaymentDate.getTime());
|
||||
amount = (Float) loanSchedule.get(i).get("principalOriginalDue") + (Float) loanSchedule.get(i).get("interestOriginalDue");
|
||||
amount = ((Number) loanSchedule.get(i).get("principalOriginalDue")).floatValue()
|
||||
+ ((Number) loanSchedule.get(i).get("interestOriginalDue")).floatValue();
|
||||
if (currentDate.after(repaymentDate)) {
|
||||
LOAN_TRANSACTION_HELPER.makeRepayment(retrieveDueDate, amount, loanID);
|
||||
} else {
|
||||
|
||||
-4
@@ -300,7 +300,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
||||
transaction(312.0, "Repayment", "20 February 2023", 626.0, 312.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(312.0, "Repayment", "20 March 2023", 314.0, 312.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(384.0, "Repayment", "20 April 2023", 0.0, 314.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
|
||||
transaction(70.0, "Accrual", "20 April 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
|
||||
transaction(100.0, "Chargeback", "02 May 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
|
||||
);
|
||||
|
||||
@@ -585,7 +584,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
||||
verifyTransactions(loanId, //
|
||||
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), //
|
||||
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
|
||||
transaction(100.0, "Chargeback", "02 May 2023", 50.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0) //
|
||||
);
|
||||
|
||||
@@ -651,7 +649,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
||||
verifyTransactions(loanId, //
|
||||
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), //
|
||||
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
|
||||
transaction(100.0, "Chargeback", "20 January 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
|
||||
);
|
||||
|
||||
@@ -718,7 +715,6 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
|
||||
verifyTransactions(loanId, //
|
||||
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
|
||||
transaction(1370.0, "Repayment", "20 January 2023", 0, 1250.0, 0.0, 50.0, 20.0, 0.0, 50.0), //
|
||||
transaction(70.0, "Accrual", "20 January 2023", 0.0, 0.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
|
||||
transaction(100.0, "Chargeback", "20 January 2023", 0.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
|
||||
);
|
||||
|
||||
|
||||
+16
-8
@@ -317,7 +317,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
logLoanTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(87.89, "Repayment", "01 February 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"),
|
||||
transaction(10.49, "Interest Refund", "09 February 2021"));
|
||||
transaction(10.49, "Interest Refund", "09 February 2021"), transaction(10.49, "Accrual", "09 February 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -437,7 +437,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
logLoanTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"),
|
||||
transaction(500.0, "Disbursement", "07 January 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"),
|
||||
transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021"));
|
||||
transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021"),
|
||||
transaction(9.67, "Accrual", "09 February 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -634,7 +635,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
transaction(171.29, "Repayment", "01 April 2021"), //
|
||||
transaction(171.29, "Repayment", "01 May 2021"), //
|
||||
transaction(171.29, "Repayment", "01 June 2021"), //
|
||||
transaction(171.32, "Repayment", "01 July 2021") //
|
||||
transaction(171.32, "Repayment", "01 July 2021"), //
|
||||
transaction(27.77, "Accrual", "01 July 2021") //
|
||||
); //
|
||||
});
|
||||
runAt("11 July 2021", () -> {
|
||||
@@ -654,7 +656,9 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
transaction(171.29, "Repayment", "01 June 2021"), //
|
||||
transaction(171.32, "Repayment", "01 July 2021"), //
|
||||
transaction(500.0, "Payout Refund", "11 July 2021"), //
|
||||
transaction(20.41, "Interest Refund", "11 July 2021")); //
|
||||
transaction(20.41, "Interest Refund", "11 July 2021"), //
|
||||
transaction(27.77, "Accrual", "01 July 2021") //
|
||||
); //
|
||||
});
|
||||
}
|
||||
|
||||
@@ -692,7 +696,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
transaction(500.0, "Merchant Issued Refund", "14 January 2021"), //
|
||||
transaction(1.78, "Interest Refund", "14 January 2021"), //
|
||||
transaction(500.0, "Payout Refund", "22 January 2021"), //
|
||||
transaction(2.88, "Interest Refund", "22 January 2021") //
|
||||
transaction(2.88, "Interest Refund", "22 January 2021"), //
|
||||
transaction(4.66, "Accrual", "22 January 2021") //
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -992,7 +997,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
transaction(250.0, "Payout Refund", "13 February 2021"), //
|
||||
transaction(2.96, "Interest Refund", "13 February 2021"), //
|
||||
transaction(400.0, "Merchant Issued Refund", "06 April 2021"), //
|
||||
transaction(10.11, "Interest Refund", "06 April 2021") //
|
||||
transaction(10.11, "Interest Refund", "06 April 2021"), //
|
||||
transaction(17.14, "Accrual", "06 April 2021") //
|
||||
);
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
|
||||
|
||||
@@ -1109,6 +1115,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
|
||||
transaction(85.63, "Repayment", "10 January 2021"), //
|
||||
transaction(5.7, "Accrual", "22 January 2021"), //
|
||||
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
|
||||
transaction(5.42, "Interest Refund", "22 January 2021") //
|
||||
);
|
||||
@@ -1183,7 +1190,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
|
||||
transaction(85.63, "Repayment", "10 January 2021"), //
|
||||
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
|
||||
transaction(5.42, "Interest Refund", "22 January 2021") //
|
||||
transaction(5.42, "Interest Refund", "22 January 2021"), //
|
||||
transaction(5.42, "Accrual", "22 January 2021") //
|
||||
);
|
||||
|
||||
Long repaymentId = repaymentIdRef.get();
|
||||
@@ -1194,7 +1202,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
reversedTransaction(85.63, "Repayment", "10 January 2021"), //
|
||||
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
|
||||
transaction(5.70, "Interest Refund", "22 January 2021"), //
|
||||
transaction(5.70, "Accrual", "10 January 2021") //
|
||||
transaction(5.42, "Accrual", "22 January 2021") //
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
+4
-7
@@ -569,7 +569,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
verifyTransactions(loanId.get(),
|
||||
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
|
||||
transaction(1070.0, "Repayment", repaymentDate1, 0.0, 1000.0, 0.0, 40.0, 30, 0.0, 0.0, true), //
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false)); //
|
||||
|
||||
});
|
||||
@@ -665,7 +664,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(500.0)).locale("en"));
|
||||
|
||||
verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), //
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); //
|
||||
|
||||
@@ -675,8 +673,8 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
|
||||
|
||||
verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), //
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), //
|
||||
transaction(70.0, "Accrual", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); //
|
||||
|
||||
@@ -728,7 +726,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(500.0)).locale("en"));
|
||||
|
||||
verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), //
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.0, 0.0, 80.0, false), //
|
||||
transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
|
||||
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); //
|
||||
@@ -793,6 +790,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
final String disbursementDay2 = "02 February 2023";
|
||||
final String repaymentDate1 = "15 January 2023";
|
||||
final String repaymentPeriod1DueDate = "01 February 2023";
|
||||
final String repaymentPeriod2DueDate = "01 March 2023";
|
||||
final String repaymentPeriod1CloseDate = "02 February 2023";
|
||||
final String repaymentPeriod2CloseDate = "02 March 2023";
|
||||
final String creationBusinessDay = "15 January 2023";
|
||||
@@ -830,7 +828,6 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
|
||||
verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false),
|
||||
transaction(570, "Repayment", repaymentDate1, 0.0, 500.0, 0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0, 0, 0.0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 500, 0, 0, 0, 0, 0, 0, false));
|
||||
});
|
||||
@@ -839,10 +836,10 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
|
||||
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
|
||||
|
||||
verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false),
|
||||
transaction(70.0, "Accrual", repaymentDate1, 0, 0, 0.0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(570.0, "Repayment", repaymentDate1, 0.0, 500.0, 0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false),
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 500.0, 0, 0, 0, 0, 0, 0, false));
|
||||
transaction(500.0, "Disbursement", disbursementDay2, 500.0, 0, 0, 0, 0, 0, 0, false),
|
||||
transaction(70.0, "Accrual", repaymentPeriod2DueDate, 0, 0, 0.0, 40.0, 30.0, 0, 0, false));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -465,6 +465,11 @@ public class ExternalEventConfigurationHelper {
|
||||
loanAccrualTransactionCreatedBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanAccrualTransactionCreatedBusinessEvent);
|
||||
|
||||
Map<String, Object> loanAccrualAdjustmentTransactionBusinessEvent = new HashMap<>();
|
||||
loanAccrualAdjustmentTransactionBusinessEvent.put("type", "LoanAccrualAdjustmentTransactionBusinessEvent");
|
||||
loanAccrualAdjustmentTransactionBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanAccrualAdjustmentTransactionBusinessEvent);
|
||||
|
||||
Map<String, Object> loanRescheduledDueAdjustScheduleBusinessEvent = new HashMap<>();
|
||||
loanRescheduledDueAdjustScheduleBusinessEvent.put("type", "LoanRescheduledDueAdjustScheduleBusinessEvent");
|
||||
loanRescheduledDueAdjustScheduleBusinessEvent.put("enabled", false);
|
||||
@@ -571,7 +576,6 @@ public class ExternalEventConfigurationHelper {
|
||||
defaults.add(loanTransactionInterestRefundPreBusinessEvent);
|
||||
|
||||
return defaults;
|
||||
|
||||
}
|
||||
|
||||
public static String getExternalEventConfigurationsForUpdateJSON() {
|
||||
|
||||
Reference in New Issue
Block a user