FINERACT-2060: Accrual reverse replay logic and Handling

This commit is contained in:
Marta Jankovics
2024-11-29 19:17:23 +01:00
committed by Adam Saghy
parent 8d25b0c765
commit d57e72ec2f
80 changed files with 2882 additions and 2940 deletions
@@ -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)
@@ -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));
}
}
@@ -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
*
@@ -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, //
@@ -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) {
@@ -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,
@@ -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);
}
@@ -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
@@ -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 |
@@ -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
@@ -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);
}
});
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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() {
@@ -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
@@ -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);
}
@@ -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();
}
}
@@ -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() {
@@ -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) {
@@ -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);
@@ -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);
}
@@ -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);
}
}
@@ -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) {
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
@@ -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
@@ -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);
}
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
@@ -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");
};
}
@@ -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>
@@ -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>
@@ -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)
@@ -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,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();
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
@@ -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.
*
@@ -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));
@@ -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);
@@ -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)
@@ -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<>();
@@ -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);
@@ -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);
@@ -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);
}
}
@@ -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);
@@ -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.");
@@ -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<>();
@@ -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) {
@@ -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(),
@@ -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);
@@ -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(
@@ -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);
}
}
@@ -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);
@@ -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
@@ -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");
@@ -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) {
@@ -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);
}
}
@@ -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,
@@ -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));
@@ -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);
@@ -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 {
@@ -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) //
);
@@ -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") //
);
});
@@ -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));
});
}
@@ -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() {