FINERACT-2060: Progressive loan - final accrual calculation on closed loan

This commit is contained in:
Marta Jankovics
2024-12-13 16:48:07 +01:00
committed by Arnold Gálovics
parent c4cdd6f51a
commit 136bb0009a
34 changed files with 682 additions and 837 deletions
@@ -348,9 +348,12 @@ public final class ErrorMessageHelper {
} }
public static String wrongLoanStatus(Integer actual, Integer expected) { public static String wrongLoanStatus(Integer actual, Integer expected) {
String actualToStr = actual.toString(); return wrongLoanStatus(null, actual, expected);
String expectedToStr = expected.toString(); }
return String.format("Wrong Loan status ID. Actual ID is: %s - But expected ID is: %s", actualToStr, expectedToStr);
public static String wrongLoanStatus(String resourceId, Integer actual, Integer expected) {
return String.format("Wrong Loan status ID of resource %s. Actual ID is: %s - But expected ID is: %s", resourceId, actual,
expected);
} }
public static String wrongFraudFlag(Boolean actualFraudStatus, Boolean expectedFraudStatus) { public static String wrongFraudFlag(Boolean actualFraudStatus, Boolean expectedFraudStatus) {
@@ -446,109 +449,54 @@ public final class ErrorMessageHelper {
return String.format("Idempotency key is not matching: Actual value is: %s - But expected value is: %s", actual, expected); return String.format("Idempotency key is not matching: Actual value is: %s - But expected value is: %s", actual, expected);
} }
public static String wrongNumberOfLinesInRepaymentSchedule(int actual, int expected) {
return wrongNumberOfLinesInRepaymentSchedule(null, actual, expected);
}
public static String wrongNumberOfLinesInRepaymentSchedule(String resourceId, int actual, int expected) { public static String wrongNumberOfLinesInRepaymentSchedule(String resourceId, int actual, int expected) {
String actualStr = String.valueOf(actual); return String.format("Number of lines in Repayment schedule of resource %s is not correct. " //
String expectedStr = String.valueOf(expected); + "Actual value is: %s - But expected value is: %s", resourceId, actual, expected);
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) { public static String wrongValueInLineInRepaymentSchedule(String resourceId, int line, List<List<String>> actualList,
return wrongValueInLineInRepaymentSchedule(null, line, actual, expected);
}
public static String wrongValueInLineInRepaymentSchedule(String resourceId, int line, List<List<String>> actual,
List<String> expected) { List<String> expected) {
String lineStr = String.valueOf(line); String actual = actualList.stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
String expectedStr = expected.toString(); return String.format("%nWrong value in Repayment schedule of resource %s tab line %s." //
StringBuilder sb = new StringBuilder(); + "%nActual values in line (with the same due date) are: %n%s - But expected values in line: %n%s", resourceId, line,
for (List<String> innerList : actual) { actual, expected);
sb.append(innerList.toString());
sb.append(System.lineSeparator());
}
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) { public static String wrongValueInLineInTransactionsTab(String resourceId, int line, List<List<String>> actualList,
return wrongValueInLineInTransactionsTab(null, line, actual, expected); List<String> expected) {
} String actual = actualList.stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
return String.format("%nWrong value in Transactions tab of resource %s line %s." //
public static String wrongValueInLineInTransactionsTab(String resourceId, int line, List<List<String>> actual, List<String> expected) { + "%nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s", resourceId, line, actual,
String lineStr = String.valueOf(line); expected);
String expectedStr = expected.toString();
StringBuilder sb = new StringBuilder();
for (List<String> innerList : actual) {
sb.append(innerList.toString());
sb.append(System.lineSeparator());
}
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) { public static String nrOfLinesWrongInTransactionsTab(String resourceId, int actual, int expected) {
String actualStr = String.valueOf(actual); return String.format("%nNumber of lines does not match in Transactions tab and expected datatable of resource %s." //
String expectedStr = String.valueOf(expected); + "%nNumber of transaction tab lines: %s %nNumber of expected datatable lines: %s%n", resourceId, actual, expected);
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) { public static String wrongValueInLineInChargesTab(String resourceId, int line, List<List<String>> actualList, List<String> expected) {
String lineStr = String.valueOf(line); String actual = actualList.stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
String expectedStr = expected.toString(); return String.format("%nWrong value in Charges tab of resource %s line %s." //
StringBuilder sb = new StringBuilder(); + "%nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s", resourceId, line, actual,
for (List<String> innerList : actual) { expected);
sb.append(innerList.toString());
sb.append(System.lineSeparator());
}
return String.format(
"%nWrong value in Charges tab line %s. %nActual values in line (with the same date) are: %n%s %nExpected values in line: %n%s",
lineStr, sb.toString(), expectedStr);
} }
public static String wrongValueInLineInJournalEntries(int line, List<List<List<String>>> actual, List<String> expected) { public static String wrongValueInLineInJournalEntries(int line, List<List<List<String>>> actual, List<String> expected) {
String lineStr = String.valueOf(line); return wrongValueInLineInJournalEntries(null, line, actual, expected);
String expectedStr = expected.toString(); }
StringBuilder sb = new StringBuilder();
for (List<List<String>> innerList : actual) {
sb.append(innerList.toString());
sb.append(System.lineSeparator());
}
return String.format( public static String wrongValueInLineInJournalEntries(String resourceId, int line, List<List<List<String>>> actualList,
"%nWrong value in Journal entries line %s. %nActual values for the possible transactions in line (with the same date) are: %n%s %nExpected values in line: %n%s", List<String> expected) {
lineStr, sb.toString(), expectedStr); String actual = actualList.stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
return String.format("%nWrong value in Journal entries of resource %s line %s." //
+ "%nActual values for the possible transactions in line (with the same date) are: %n%s %nExpected values in line: %n%s",
resourceId, line, actual, expected);
} }
public static String wrongDataInJournalEntriesGlAccountType(int line, String actual, String expected) { public static String wrongDataInJournalEntriesGlAccountType(int line, String actual, String expected) {
return String.format("Wrong data in Journal entries, line %s / GL account type. Actual value is: %s - But expected value is: %s", return String.format("Wrong data in Journal entries, line %s / GL account type. " //
line, actual, expected); + "Actual value is: %s - But expected value is: %s", line, actual, expected);
} }
public static String wrongDataInJournalEntriesGlAccountCode(int line, String actual, String expected) { public static String wrongDataInJournalEntriesGlAccountCode(int line, String actual, String expected) {
@@ -865,7 +865,9 @@ public class BatchApiStepDef extends AbstractStepDef {
Integer statusIdActual = status.getId(); Integer statusIdActual = status.getId();
Integer statusIdExpected = LoanStatus.APPROVED.value; Integer statusIdExpected = LoanStatus.APPROVED.value;
assertThat(statusIdActual).as(ErrorMessageHelper.wrongLoanStatus(statusIdActual, statusIdExpected)).isEqualTo(statusIdExpected); String resourceId = String.valueOf(response.body().getId());
assertThat(statusIdActual).as(ErrorMessageHelper.wrongLoanStatus(resourceId, statusIdActual, statusIdExpected))
.isEqualTo(statusIdExpected);
} }
@Then("Nr. {int} Client creation was rolled back") @Then("Nr. {int} Client creation was rolled back")
@@ -59,6 +59,7 @@ public class JournalEntriesStepDef extends AbstractStepDef {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.body().getLoanId(); long loanId = loanResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
@@ -132,7 +133,8 @@ public class JournalEntriesStepDef extends AbstractStepDef {
} }
} }
assertThat(containsAnyExpected) assertThat(containsAnyExpected)
.as(ErrorMessageHelper.wrongValueInLineInJournalEntries(i, possibleActualValuesList, expectedValues)).isTrue(); .as(ErrorMessageHelper.wrongValueInLineInJournalEntries(resourceId, i, possibleActualValuesList, expectedValues))
.isTrue();
} }
} }
@@ -141,6 +143,7 @@ public class JournalEntriesStepDef extends AbstractStepDef {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT); DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.body().getLoanId(); long loanId = loanResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute(); Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
@@ -218,7 +221,8 @@ public class JournalEntriesStepDef extends AbstractStepDef {
} }
} }
assertThat(containsAnyExpected) assertThat(containsAnyExpected)
.as(ErrorMessageHelper.wrongValueInLineInJournalEntries(i, possibleActualValuesList, expectedValues)).isTrue(); .as(ErrorMessageHelper.wrongValueInLineInJournalEntries(resourceId, i, possibleActualValuesList, expectedValues))
.isTrue();
} }
} }
} }
@@ -1298,6 +1298,8 @@ public class LoanStepDef extends AbstractStepDef {
public void disburseLoan(String actualDisbursementDate, String transactionAmount) throws IOException { public void disburseLoan(String actualDisbursementDate, String transactionAmount) throws IOException {
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.body().getLoanId(); long loanId = loanResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest() PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
.actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount)); .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount));
@@ -1310,7 +1312,7 @@ public class LoanStepDef extends AbstractStepDef {
Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId()); Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId());
assertThat(statusActual)// assertThat(statusActual)//
.as(ErrorMessageHelper.wrongLoanStatus(Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))// .as(ErrorMessageHelper.wrongLoanStatus(resourceId, Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))//
.isEqualTo(statusExpected);// .isEqualTo(statusExpected);//
eventCheckHelper.disburseLoanEventCheck(loanId); eventCheckHelper.disburseLoanEventCheck(loanId);
eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse); eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse);
@@ -1320,6 +1322,8 @@ public class LoanStepDef extends AbstractStepDef {
public void disburseLoanWithoutAutoDownpayment(String actualDisbursementDate, String transactionAmount) throws IOException { public void disburseLoanWithoutAutoDownpayment(String actualDisbursementDate, String transactionAmount) throws IOException {
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanResponse.body().getLoanId(); long loanId = loanResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest() PostLoansLoanIdRequest disburseRequest = LoanRequestFactory.defaultLoanDisburseRequest()
.actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount)); .actualDisbursementDate(actualDisbursementDate).transactionAmount(new BigDecimal(transactionAmount));
@@ -1333,7 +1337,7 @@ public class LoanStepDef extends AbstractStepDef {
Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId()); Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId());
assertThat(statusActual)// assertThat(statusActual)//
.as(ErrorMessageHelper.wrongLoanStatus(Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))// .as(ErrorMessageHelper.wrongLoanStatus(resourceId, Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))//
.isEqualTo(statusExpected);// .isEqualTo(statusExpected);//
eventCheckHelper.disburseLoanEventCheck(loanId); eventCheckHelper.disburseLoanEventCheck(loanId);
eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse); eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse);
@@ -1773,6 +1777,7 @@ public class LoanStepDef extends AbstractStepDef {
public void loanChargesGivenChargeDataCheck(DataTable table) throws IOException { public void loanChargesGivenChargeDataCheck(DataTable table) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId(); long loanId = loanCreateResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "charges", "", "").execute(); Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "charges", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
@@ -1786,14 +1791,15 @@ public class LoanStepDef extends AbstractStepDef {
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues)); boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInChargesTab(1, actualValuesList, expectedValues)) assertThat(containsExpectedValues)
.isTrue(); .as(ErrorMessageHelper.wrongValueInLineInChargesTab(resourceId, 1, actualValuesList, expectedValues)).isTrue();
} }
@Then("Loan Charges tab has the following data:") @Then("Loan Charges tab has the following data:")
public void loanChargesTabCheck(DataTable table) throws IOException { public void loanChargesTabCheck(DataTable table) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId(); long loanId = loanCreateResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "charges", "", "").execute(); Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "charges", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
@@ -1808,8 +1814,8 @@ public class LoanStepDef extends AbstractStepDef {
boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues)); boolean containsExpectedValues = actualValuesList.stream().anyMatch(actualValues -> actualValues.equals(expectedValues));
assertThat(containsExpectedValues).as(ErrorMessageHelper.wrongValueInLineInChargesTab(i, actualValuesList, expectedValues)) assertThat(containsExpectedValues)
.isTrue(); .as(ErrorMessageHelper.wrongValueInLineInChargesTab(resourceId, i, actualValuesList, expectedValues)).isTrue();
} }
} }
@@ -1846,6 +1852,7 @@ public class LoanStepDef extends AbstractStepDef {
public void loanStatus(String statusExpected) throws IOException { public void loanStatus(String statusExpected) throws IOException {
Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE); Response<PostLoansResponse> loanCreateResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
long loanId = loanCreateResponse.body().getLoanId(); long loanId = loanCreateResponse.body().getLoanId();
String resourceId = String.valueOf(loanId);
Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "", "", "").execute(); Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "", "", "").execute();
ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse); ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
@@ -1855,7 +1862,7 @@ public class LoanStepDef extends AbstractStepDef {
LoanStatus loanStatusExpected = LoanStatus.valueOf(statusExpected); LoanStatus loanStatusExpected = LoanStatus.valueOf(statusExpected);
Integer loanStatusExpectedValue = loanStatusExpected.getValue(); Integer loanStatusExpectedValue = loanStatusExpected.getValue();
assertThat(loanStatusActualValue).as(ErrorMessageHelper.wrongLoanStatus(loanStatusActualValue, loanStatusExpectedValue)) assertThat(loanStatusActualValue).as(ErrorMessageHelper.wrongLoanStatus(resourceId, loanStatusActualValue, loanStatusExpectedValue))
.isEqualTo(loanStatusExpectedValue); .isEqualTo(loanStatusExpectedValue);
} }
@@ -2776,7 +2783,9 @@ public class LoanStepDef extends AbstractStepDef {
assertNotNull(loanDetails.body().getStatus()); assertNotNull(loanDetails.body().getStatus());
final Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId()); final Long statusExpected = Long.valueOf(loanDetails.body().getStatus().getId());
assertThat(statusActual).as(ErrorMessageHelper.wrongLoanStatus(Math.toIntExact(statusActual), Math.toIntExact(statusExpected))) String resourceId = String.valueOf(loanId);
assertThat(statusActual)
.as(ErrorMessageHelper.wrongLoanStatus(resourceId, Math.toIntExact(statusActual), Math.toIntExact(statusExpected)))
.isEqualTo(statusExpected); .isEqualTo(statusExpected);
eventCheckHelper.disburseLoanEventCheck(loanId); eventCheckHelper.disburseLoanEventCheck(loanId);
eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse); eventCheckHelper.loanDisbursalTransactionEventCheck(loanDisburseResponse);
@@ -5231,6 +5231,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan
| 22 January 2021 | Accrual | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false | | 22 January 2021 | Accrual | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | false |
| 22 January 2021 | Merchant Issued Refund | 1000.0 | 914.37 | 5.42 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Merchant Issued Refund | 1000.0 | 914.37 | 5.42 | 0.0 | 0.0 | 0.0 | false | true |
| 22 January 2021 | Interest Refund | 5.42 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Interest Refund | 5.42 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true |
| 22 January 2021 | Accrual Adjustment | 0.28 | 0.0 | 0.28 | 0.0 | 0.0 | 0.0 | false | false |
@TestRailId:C3302 @TestRailId:C3302
Scenario: UC18-2 - In case of repayment reversal the Interest Refund transaction needs to be recalculated Scenario: UC18-2 - In case of repayment reversal the Interest Refund transaction needs to be recalculated
@@ -5288,6 +5289,7 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan
| 22 January 2021 | Merchant Issued Refund | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Merchant Issued Refund | 1000.0 | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | true |
| 22 January 2021 | Interest Refund | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | true | | 22 January 2021 | Interest Refund | 5.7 | 0.0 | 5.7 | 0.0 | 0.0 | 0.0 | false | true |
| 23 January 2021 | Credit Balance Refund | 85.63 | 85.63 | 0.0 | 0.0 | 0.0 | 85.63 | false | true | | 23 January 2021 | Credit Balance Refund | 85.63 | 85.63 | 0.0 | 0.0 | 0.0 | 85.63 | false | true |
| 22 January 2021 | Accrual | 5.42 | 0.0 | 5.42 | 0.0 | 0.0 | 0.0 | false | false |
And In Loan Transactions the "2"th Transaction has Transaction type="Repayment" and is reverted And In Loan Transactions the "2"th Transaction has Transaction type="Repayment" and is reverted
@TestRailId:C3303 @TestRailId:C3303
@@ -1331,10 +1331,8 @@ Feature: LoanAccrualActivity
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 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 | | 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 | 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 | | 04 January 2024 | Accrual | 4.1 | 0.0 | 4.1 | 0.0 | 0.0 | 0.0 | false | false |
# --- Accrual activity --- # --- Accrual activity ---
When Admin sets the business date to "07 January 2024" When Admin sets the business date to "07 January 2024"
When Admin runs inline COB job for Loan When Admin runs inline COB job for Loan
@@ -1351,11 +1349,9 @@ Feature: LoanAccrualActivity
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 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 | | 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 | | 04 January 2024 | Accrual | 4.1 | 0.0 | 4.1 | 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 | 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 Adjustment | 2.79 | 0.0 | 2.79 | 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 | 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 | | 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false |
@@ -1481,9 +1477,8 @@ Feature: LoanAccrualActivity
| 05 January 2024 | Accrual | 0.33 | 0.0 | 0.33 | 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 | 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 | | 06 January 2024 | Accrual Activity | 1.64 | 0.0 | 1.64 | 0.0 | 0.0 | 0.0 | false | false |
| 07 January 2024 | Accrual | 0.25 | 0.0 | 0.25 | 0.0 | 0.0 | 0.0 | false | false |
| 08 January 2024 | Repayment | 753.07 | 749.4 | 3.67 | 0.0 | 0.0 | 250.6 | false | true | | 08 January 2024 | Repayment | 753.07 | 749.4 | 3.67 | 0.0 | 0.0 | 250.6 | false | true |
| 08 January 2024 | Accrual | 0.24 | 0.0 | 0.24 | 0.0 | 0.0 | 0.0 | false | false | | 08 January 2024 | Accrual | 2.46 | 0.0 | 2.46 | 0.0 | 0.0 | 0.0 | false | false |
@TestRailId:C3182 @TestRailId:C3182
Scenario: Verify accrual activity posting job Scenario: Verify accrual activity posting job
@@ -2045,9 +2040,9 @@ Feature: LoanAccrualActivity
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | Replayed | | 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 | 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 | 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 | | 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 | | 10 October 2024 | Charge Adjustment | 15.0 | 0.0 | 0.0 | 0.0 | 15.0 | 400.0 | false | true |
@TestRailId:C3274 @TestRailId:C3274
@@ -2959,6 +2959,7 @@ Feature: LoanChargeback
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 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 | | 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 | 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 | | 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 | | 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: Then Loan Transactions tab has a "CHARGEBACK" transaction with date "16 April 2024" which has the following Journal entries:
@@ -3032,6 +3033,7 @@ Feature: LoanChargeback
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 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 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 | 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 | | 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: 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 | | Type | Account code | Account name | Debit | Credit |
@@ -3065,6 +3067,7 @@ Feature: LoanChargeback
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 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 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 | 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 | | 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 | | 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: Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
@@ -3154,8 +3157,8 @@ Feature: LoanChargeback
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 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 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 | 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 | 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: 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 | | Type | Account code | Account name | Debit | Credit |
| ASSET | 112601 | Loans Receivable | 100.0 | | | ASSET | 112601 | Loans Receivable | 100.0 | |
@@ -3165,7 +3168,7 @@ Feature: LoanChargeback
| ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112601 | Loans Receivable | | 100.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries: 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 | | Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | | | ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
| INCOME | 404007 | Fee Income | | 5.0 | | INCOME | 404007 | Fee Income | | 5.0 |
@@ -3193,8 +3196,8 @@ Feature: LoanChargeback
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Overpayment | | 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 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 | 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 | 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 | | 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: 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 | | Type | Account code | Account name | Debit | Credit |
@@ -3205,7 +3208,7 @@ Feature: LoanChargeback
| ASSET | 112601 | Loans Receivable | | 100.0 | | ASSET | 112601 | Loans Receivable | | 100.0 |
| ASSET | 112603 | Interest/Fee Receivable | | 5.0 | | ASSET | 112603 | Interest/Fee Receivable | | 5.0 |
| LIABILITY | 145023 | Suspense/Clearing account | 105.0 | | | LIABILITY | 145023 | Suspense/Clearing account | 105.0 | |
Then Loan Transactions tab has a "ACCRUAL" transaction with date "15 April 2024" which has the following Journal entries: 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 | | Type | Account code | Account name | Debit | Credit |
| ASSET | 112603 | Interest/Fee Receivable | 5.0 | | | ASSET | 112603 | Interest/Fee Receivable | 5.0 | |
| INCOME | 404007 | Fee Income | | 5.0 | | INCOME | 404007 | Fee Income | | 5.0 |
@@ -3354,6 +3357,7 @@ Feature: LoanChargeback
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | | 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 | | 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 | 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 | 3.0 | 0.0 | 0.0 | 0.0 | 3.0 | 0.0 | 3.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 | | 20 April 2024 | Chargeback | 10.0 | 8.0 | 0.0 | 0.0 | 2.0 | 3.0 | 7.0 |
Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries: Then Loan Transactions tab has a "DISBURSEMENT" transaction with date "01 January 2024" which has the following Journal entries:
@@ -2473,8 +2473,8 @@ Feature: LoanRepayment
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 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 | | 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 | | 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 |
Then Loan Charges tab has the following data: Then Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | 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 | | NSF fee | true | Specified due date | 28 January 2023 | Flat | 20.0 | 0.0 | 0.0 | 20.0 |
@@ -2486,8 +2486,8 @@ Feature: LoanRepayment
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 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 | | 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 | | 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 |
| 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 |
Then Loan Charges tab has the following data: Then Loan Charges tab has the following data:
| Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding | | Name | isPenalty | Payment due at | Due as of | Calculation type | Due | Paid | Waived | Outstanding |
@@ -2507,8 +2507,8 @@ Feature: LoanRepayment
Then Loan Transactions tab has the following data: Then Loan Transactions tab has the following data:
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | | 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 | | 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 | | 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 |
| 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 | | 28 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 0.0 | 20.0 | 500.0 |
| 30 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 20.0 | 0.0 | 0.0 | | 30 January 2023 | Repayment | 520.0 | 500.0 | 0.0 | 20.0 | 0.0 | 0.0 |
| 30 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 | | 30 January 2023 | Accrual | 20.0 | 0.0 | 0.0 | 0.0 | 20.0 | 0.0 |
@@ -3656,9 +3656,9 @@ Feature: LoanRepayment
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
| 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
| 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
| 24 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
Then Loan Repayment schedule has 2 periods, with the following data for periods: Then Loan Repayment schedule has 2 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 | | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
| | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | |
@@ -3676,9 +3676,9 @@ Feature: LoanRepayment
| 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
| 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true |
| 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false |
| 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
| 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false |
| 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false |
| 24 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
| 25 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 25 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false |
Then Loan Repayment schedule has 2 periods, with the following data for periods: Then Loan Repayment schedule has 2 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 | | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
@@ -34,4 +34,5 @@ public class AccrualChargeData {
private Money chargeAmount; private Money chargeAmount;
private Money chargeAccruable; private Money chargeAccruable;
private Money chargeAccrued; private Money chargeAccrued;
private Money transactionAccrued;
} }
@@ -68,10 +68,20 @@ public class AccrualPeriodData {
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
} }
public Money getFeeTransactionAccrued() {
return charges.stream().filter(charge -> !charge.isPenalty()).map(AccrualChargeData::getTransactionAccrued).reduce(null,
MathUtil::plus);
}
public Money getPenaltyAccrued() { public Money getPenaltyAccrued() {
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus); return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getChargeAccrued).reduce(null, MathUtil::plus);
} }
public Money getPenaltyTransactionAccrued() {
return charges.stream().filter(AccrualChargeData::isPenalty).map(AccrualChargeData::getTransactionAccrued).reduce(null,
MathUtil::plus);
}
public Money getChargeAccruable() { public Money getChargeAccruable() {
return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus); return charges.stream().map(AccrualChargeData::getChargeAccruable).reduce(null, MathUtil::plus);
} }
@@ -1,201 +0,0 @@
/**
* 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.math.BigDecimal;
import java.time.LocalDate;
import java.util.Map;
import org.apache.fineract.organisation.monetary.data.CurrencyData;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
public class LoanScheduleAccrualData {
private final Long loanId;
private final Long officeId;
private final LocalDate accruedTill;
private final PeriodFrequencyType repaymentFrequency;
private final Integer repayEvery;
private final Integer installmentNumber;
private final LocalDate dueDate;
private final LocalDate fromDate;
private final Long repaymentScheduleId;
private final Long loanProductId;
private final BigDecimal interestIncome;
private final BigDecimal feeIncome;
private final BigDecimal penaltyIncome;
private final BigDecimal waivedInterestIncome;
private final BigDecimal accruedInterestIncome;
private final BigDecimal accruedFeeIncome;
private final BigDecimal accruedPenaltyIncome;
private final CurrencyData currencyData;
private final LocalDate interestCalculatedFrom;
private Map<LoanChargeData, BigDecimal> applicableCharges;
private BigDecimal dueDateFeeIncome;
private BigDecimal dueDatePenaltyIncome;
private BigDecimal accruableIncome;
private BigDecimal creditedFee;
private BigDecimal creditedPenalty;
public LoanScheduleAccrualData(final Long loanId, final Long officeId, final Integer installmentNumber, final LocalDate accruedTill,
final PeriodFrequencyType repaymentFrequency, final Integer repayEvery, final LocalDate dueDate, final LocalDate fromDate,
final Long repaymentScheduleId, final Long loanProductId, final BigDecimal interestIncome, final BigDecimal feeIncome,
final BigDecimal penaltyIncome, final BigDecimal accruedInterestIncome, final BigDecimal accruedFeeIncome,
final BigDecimal accruedPenaltyIncome, final CurrencyData currencyData, final LocalDate interestCalculatedFrom,
final BigDecimal waivedInterestIncome, BigDecimal creditedFee, BigDecimal creditedPenalty) {
this.loanId = loanId;
this.installmentNumber = installmentNumber;
this.officeId = officeId;
this.accruedTill = accruedTill;
this.dueDate = dueDate;
this.fromDate = fromDate;
this.repaymentScheduleId = repaymentScheduleId;
this.loanProductId = loanProductId;
this.interestIncome = interestIncome;
this.feeIncome = feeIncome;
this.penaltyIncome = penaltyIncome;
this.accruedFeeIncome = accruedFeeIncome;
this.accruedInterestIncome = accruedInterestIncome;
this.accruedPenaltyIncome = accruedPenaltyIncome;
this.currencyData = currencyData;
this.repaymentFrequency = repaymentFrequency;
this.repayEvery = repayEvery;
this.interestCalculatedFrom = interestCalculatedFrom;
this.waivedInterestIncome = waivedInterestIncome;
this.creditedFee = creditedFee;
this.creditedPenalty = creditedPenalty;
}
public Long getLoanId() {
return this.loanId;
}
public Long getOfficeId() {
return this.officeId;
}
public LocalDate getDueDate() {
return this.dueDate;
}
public LocalDate getDueDateAsLocaldate() {
return this.dueDate;
}
public Long getRepaymentScheduleId() {
return this.repaymentScheduleId;
}
public Long getLoanProductId() {
return this.loanProductId;
}
public BigDecimal getInterestIncome() {
return this.interestIncome;
}
public BigDecimal getFeeIncome() {
return this.feeIncome;
}
public BigDecimal getPenaltyIncome() {
return this.penaltyIncome;
}
public BigDecimal getAccruedInterestIncome() {
return this.accruedInterestIncome;
}
public BigDecimal getAccruedFeeIncome() {
return this.accruedFeeIncome;
}
public BigDecimal getAccruedPenaltyIncome() {
return this.accruedPenaltyIncome;
}
public CurrencyData getCurrencyData() {
return this.currencyData;
}
public LocalDate getAccruedTill() {
return this.accruedTill;
}
public LocalDate getFromDateAsLocaldate() {
return this.fromDate;
}
public PeriodFrequencyType getRepaymentFrequency() {
return this.repaymentFrequency;
}
public Integer getRepayEvery() {
return this.repayEvery;
}
public LocalDate getInterestCalculatedFrom() {
return this.interestCalculatedFrom;
}
public Integer getInstallmentNumber() {
return this.installmentNumber;
}
public Map<LoanChargeData, BigDecimal> getApplicableCharges() {
return this.applicableCharges;
}
public BigDecimal getDueDateFeeIncome() {
return this.dueDateFeeIncome;
}
public BigDecimal getDueDatePenaltyIncome() {
return this.dueDatePenaltyIncome;
}
public void updateChargeDetails(final Map<LoanChargeData, BigDecimal> applicableCharges, final BigDecimal dueDateFeeIncome,
final BigDecimal dueDatePenaltyIncome) {
this.applicableCharges = applicableCharges;
this.dueDateFeeIncome = dueDateFeeIncome;
this.dueDatePenaltyIncome = dueDatePenaltyIncome;
}
public BigDecimal getWaivedInterestIncome() {
return this.waivedInterestIncome;
}
public BigDecimal getAccruableIncome() {
return this.accruableIncome;
}
public void updateAccruableIncome(BigDecimal accruableIncome) {
this.accruableIncome = accruableIncome;
}
public BigDecimal getCreditedFee() {
return this.creditedFee;
}
public BigDecimal getCreditedPenalty() {
return this.creditedPenalty;
}
}
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import static org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType.PROGRESSIVE;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive; import com.google.gson.JsonPrimitive;
@@ -740,6 +742,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* is lesser than todays date. If not, the transaction date is set to todays date * is lesser than todays date. If not, the transaction date is set to todays date
*/ */
public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) { public LoanTransaction handleChargeAppliedTransaction(final LoanCharge loanCharge, final LocalDate suppliedTransactionDate) {
if (isProgressiveSchedule()) {
return null;
}
final Money chargeAmount = loanCharge.getAmount(getCurrency()); final Money chargeAmount = loanCharge.getAmount(getCurrency());
Money feeCharges = chargeAmount; Money feeCharges = chargeAmount;
Money penaltyCharges = Money.zero(getCurrency()); Money penaltyCharges = Money.zero(getCurrency());
@@ -1053,7 +1058,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
LoanCharge charge = loanCharge; LoanCharge charge = loanCharge;
// add new charges // add new charges
if (loanCharge.getId() == null) { if (loanCharge.getId() == null) {
LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = null; LoanTrancheDisbursementCharge loanTrancheDisbursementCharge;
loanCharge.update(this); loanCharge.update(this);
if (this.loanProduct.isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) { if (this.loanProduct.isMultiDisburseLoan() && loanCharge.isTrancheDisbursementCharge()) {
loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(this); loanCharge.getTrancheDisbursementCharge().getloanDisbursementDetails().updateLoan(this);
@@ -3563,4 +3568,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor() { public LoanRepaymentScheduleTransactionProcessor getTransactionProcessor() {
return transactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode); return transactionProcessorFactory.determineProcessor(transactionProcessingStrategyCode);
} }
public boolean isProgressiveSchedule() {
return getLoanProductRelatedDetail().getLoanScheduleType() == PROGRESSIVE;
}
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.service; package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate; import java.time.LocalDate;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -25,13 +26,13 @@ import org.springframework.transaction.annotation.Transactional;
public interface LoanAccrualActivityProcessingService { public interface LoanAccrualActivityProcessingService {
@Transactional @Transactional
void makeAccrualActivityTransaction(Long loanId, LocalDate currentDate); void makeAccrualActivityTransaction(@NotNull Long loanId, @NotNull LocalDate currentDate);
Loan makeAccrualActivityTransaction(Loan loan, LocalDate currentDate); void makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LocalDate currentDate);
@Transactional @Transactional
void processAccrualActivityForLoanClosure(Loan loan); void processAccrualActivityForLoanClosure(@NotNull Loan loan);
@Transactional @Transactional
void processAccrualActivityForLoanReopen(Loan loan); void processAccrualActivityForLoanReopen(@NotNull Loan loan);
} }
@@ -20,7 +20,9 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -33,8 +35,12 @@ public class LoanAccrualTransactionBusinessEventServiceImpl implements LoanAccru
@Override @Override
public void raiseBusinessEventForAccrualTransactions(Loan loan, List<Long> existingTransactionIds) { public void raiseBusinessEventForAccrualTransactions(Loan loan, List<Long> existingTransactionIds) {
for (final LoanTransaction transaction : loan.getLoanTransactions()) { for (final LoanTransaction transaction : loan.getLoanTransactions()) {
if (transaction.isNotReversed() && transaction.isAccrual() && !existingTransactionIds.contains(transaction.getId())) { if (transaction.isNotReversed() && (transaction.isAccrual() || transaction.isAccrualAdjustment())
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(transaction)); && !existingTransactionIds.contains(transaction.getId())) {
LoanTransactionBusinessEvent businessEvent = transaction.isAccrual()
? new LoanAccrualTransactionCreatedBusinessEvent(transaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(transaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
} }
} }
} }
@@ -35,16 +35,14 @@ public interface LoanAccrualsProcessingService {
void reprocessExistingAccruals(@NotNull Loan loan); void reprocessExistingAccruals(@NotNull Loan loan);
void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled); void processAccrualsOnInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled, boolean addJournal);
void addIncomePostingAndAccruals(Long loanId) throws Exception;
void processIncomePostingAndAccruals(@NotNull Loan loan); void processIncomePostingAndAccruals(@NotNull Loan loan);
void addIncomeAndAccrualTransactions(Long loanId) throws Exception;
void processAccrualsOnLoanClosure(@NotNull Loan loan); void processAccrualsOnLoanClosure(@NotNull Loan loan);
void processAccrualsOnLoanReopen(@NotNull Loan loan);
void processAccrualsOnLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate, void processAccrualsOnLoanForeClosure(@NotNull Loan loan, @NotNull LocalDate foreClosureDate,
@NotNull List<LoanTransaction> newAccrualTransactions); @NotNull List<LoanTransaction> newAccrualTransactions);
} }
@@ -30,7 +30,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -53,16 +52,6 @@ public interface LoanWritePlatformService {
CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId, CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(LoanTransactionType repaymentTransactionType, Long loanId,
JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType); JsonCommand command, boolean isRecoveryRepayment, String chargeRefundChargeType);
@Transactional
Loan reverseReplayAccrualActivityTransaction(Loan loan, LoanTransaction loanTransaction, LoanRepaymentScheduleInstallment installment,
LocalDate transactionDate);
@Transactional
Loan makeAccrualActivityTransaction(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate transactionDate);
@Transactional
Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction accrualActivityTransaction);
@Transactional @Transactional
CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command); CommandProcessingResult makeInterestPaymentWaiver(JsonCommand command);
@@ -293,10 +293,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note); this.noteRepository.save(note);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
setLoanDelinquencyTag(loan, transactionDate); setLoanDelinquencyTag(loan, transactionDate);
@@ -306,6 +304,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent); businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, isLoanToLoanTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
// disable all active standing orders linked to this loan if status // disable all active standing orders linked to this loan if status
// changes to closed // changes to closed
disableStandingInstructionsLinkedToClosedLoan(loan); disableStandingInstructionsLinkedToClosedLoan(loan);
@@ -492,13 +494,13 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note); this.noteRepository.save(note);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanChargePaymentPostBusinessEvent(newPaymentTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return newPaymentTransaction; return newPaymentTransaction;
} }
@@ -726,13 +728,15 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note); this.noteRepository.save(note);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService businessEventNotifierService
.notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction)); .notifyPostBusinessEvent(new LoanCreditBalanceRefundPostBusinessEvent(newCreditBalanceRefundTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return newCreditBalanceRefundTransaction; return newCreditBalanceRefundTransaction;
} }
@@ -778,13 +782,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.noteRepository.save(note); this.noteRepository.save(note);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRefundPostBusinessEvent(newRefundTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds, false);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
builderResult.withEntityId(newRefundTransaction.getId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId()) builderResult.withEntityId(newRefundTransaction.getId()).withOfficeId(loan.getOfficeId()).withClientId(loan.getClientId())
.withGroupId(loan.getGroupId()); .withGroupId(loan.getGroupId());
@@ -46,7 +46,7 @@ public class AddPeriodicAccrualEntriesForLoansTasklet implements Tasklet {
List<Throwable> errors = new ArrayList<>(); List<Throwable> errors = new ArrayList<>();
for (Long loanId : loanIds) { for (Long loanId : loanIds) {
try { try {
loanAccrualsProcessingService.addIncomeAndAccrualTransactions(loanId); loanAccrualsProcessingService.addIncomePostingAndAccruals(loanId);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to add income and accrual transaction for loan {}", loanId, e); log.error("Failed to add income and accrual transaction for loan {}", loanId, e);
errors.add(e); errors.add(e);
@@ -466,13 +466,13 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
} }
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// update the loan object
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, true);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRescheduledDueAdjustScheduleBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId) return new CommandProcessingResultBuilder().withCommandId(jsonCommand.commandId()).withEntityId(loanRescheduleRequestId)
.withLoanId(loanRescheduleRequest.getLoan().getId()).with(changes).withClientId(loan.getClientId()) .withLoanId(loanRescheduleRequest.getLoan().getId()).with(changes).withClientId(loan.getClientId())
.withOfficeId(loan.getOfficeId()).withGroupId(loan.getGroupId()).build(); .withOfficeId(loan.getOfficeId()).withGroupId(loan.getGroupId()).build();
@@ -18,24 +18,29 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.service; package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -46,131 +51,179 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActivityProcessingService { public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActivityProcessingService {
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanWritePlatformService loanWritePlatformService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanChargeValidator loanChargeValidator; private final LoanTransactionAssembler loanTransactionAssembler;
private final LoanAccountDomainService loanAccountDomainService;
@Override @Override
@Transactional(propagation = Propagation.REQUIRES_NEW) @Transactional(propagation = Propagation.REQUIRES_NEW)
public void makeAccrualActivityTransaction(Long loanId, final LocalDate currentDate) { public void makeAccrualActivityTransaction(@NotNull Long loanId, @NotNull LocalDate currentDate) {
Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
makeAccrualActivityTransaction(loan, currentDate); makeAccrualActivityTransaction(loan, currentDate);
} }
@Override @Override
public Loan makeAccrualActivityTransaction(Loan loan, final LocalDate currentDate) { public void makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LocalDate currentDate) {
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
// check if loan has installment due on business day return;
Optional<LoanRepaymentScheduleInstallment> first = loan.getRepaymentScheduleInstallments().stream() }
.filter(loanRepaymentScheduleInstallment -> loanRepaymentScheduleInstallment.getDueDate().isEqual(currentDate)) // check if loan has installment in the past or due on current date
.findFirst(); List<LoanRepaymentScheduleInstallment> installments = loan
if (first.isPresent()) { .getRepaymentScheduleInstallments(i -> !i.isDownPayment() && !DateUtils.isBefore(currentDate, i.getDueDate()));
final LoanRepaymentScheduleInstallment installment = first.get(); for (LoanRepaymentScheduleInstallment installment : installments) {
// check if there is no not-replayed-accrual-activity related to business date LocalDate dueDate = installment.getDueDate();
List<LoanTransaction> loanTransactions = loan.getLoanTransactions(loanTransaction -> loanTransaction.isNotReversed() // check if there is any not-replayed-accrual-activity related to business date
&& loanTransaction.isAccrualActivity() && loanTransaction.getTransactionDate().isEqual(currentDate)); ArrayList<LoanTransaction> existingActivities = new ArrayList<>(
if (loanTransactions.isEmpty()) { loan.getLoanTransactions(t -> t.isNotReversed() && t.isAccrualActivity() && t.getTransactionDate().isEqual(dueDate)));
loan = loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, currentDate); boolean hasExisting = !existingActivities.isEmpty();
} LoanTransaction existingActivity = hasExisting ? existingActivities.get(0) : null;
makeOrReplayActivity(loan, installment, existingActivity);
if (hasExisting) {
existingActivities.remove(existingActivity);
existingActivities.forEach(this::reverseAccrualActivityTransaction);
} }
} }
return loan;
} }
@Override @Override
@Transactional @Transactional
public void processAccrualActivityForLoanClosure(Loan loan) { public void processAccrualActivityForLoanClosure(@NotNull Loan loan) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
return;
}
LocalDate date = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate(); LocalDate date = loan.isOverPaid() ? loan.getOverpaidOnDate() : loan.getClosedOnDate();
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed) // reverse after closure activities
.filter(LoanTransaction::isAccrualActivity).filter(loanTransaction -> loanTransaction.getDateOf().isAfter(date)).toList(); loan.getLoanTransactions(t -> t.isAccrualActivity() && !t.isReversed() && t.getDateOf().isAfter(date))
if (!accrualActivityTransaction.isEmpty()) { .forEach(this::reverseAccrualActivityTransaction);
accrualActivityTransaction.forEach(this::reverseAccrualActivityTransaction);
}
LoanTransaction loanTransaction = assembleClosingAccrualActivityTransaction(loan, date);
if (loanTransaction.getAmount().compareTo(BigDecimal.ZERO) != 0) {
loanWritePlatformService.makeAccrualActivityTransaction(loan, loanTransaction);
}
}
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) { // calculate activity amounts
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed");
loanTransaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
private LoanTransaction assembleClosingAccrualActivityTransaction(Loan loan, LocalDate date) {
// collect fees
BigDecimal feeChargesPortion = BigDecimal.ZERO; BigDecimal feeChargesPortion = BigDecimal.ZERO;
// collect penalties
BigDecimal penaltyChargesPortion = BigDecimal.ZERO; BigDecimal penaltyChargesPortion = BigDecimal.ZERO;
// collect interests
BigDecimal interestPortion = BigDecimal.ZERO; BigDecimal interestPortion = BigDecimal.ZERO;
var currency = loan.getCurrency(); // collect installment amounts
// sum up all accruals
for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { for (LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) {
feeChargesPortion = installment.getFeeAccrued(currency).getAmount().add(feeChargesPortion); feeChargesPortion = MathUtil.add(feeChargesPortion, installment.getFeeChargesCharged());
penaltyChargesPortion = installment.getPenaltyAccrued(currency).getAmount().add(penaltyChargesPortion); penaltyChargesPortion = MathUtil.add(penaltyChargesPortion, installment.getPenaltyCharges());
interestPortion = installment.getInterestAccrued(currency).getAmount().add(interestPortion); interestPortion = MathUtil.add(interestPortion, installment.getInterestCharged());
} }
List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream().filter(LoanTransaction::isAccrualActivity) List<LoanTransaction> accrualActivities = loan.getLoanTransactions().stream().filter(LoanTransaction::isAccrualActivity)
.filter(LoanTransaction::isNotReversed).toList(); .filter(LoanTransaction::isNotReversed).toList();
// subtract already Posted accruals // subtract already posted activities
for (LoanTransaction accrualActivity : accrualActivities) { for (LoanTransaction accrualActivity : accrualActivities) {
if (accrualActivity.getFeeChargesPortion() != null) { feeChargesPortion = MathUtil.subtract(feeChargesPortion, accrualActivity.getFeeChargesPortion());
feeChargesPortion = feeChargesPortion.subtract(accrualActivity.getFeeChargesPortion()); penaltyChargesPortion = MathUtil.subtract(penaltyChargesPortion, accrualActivity.getPenaltyChargesPortion());
} interestPortion = MathUtil.subtract(interestPortion, accrualActivity.getInterestPortion());
if (accrualActivity.getPenaltyChargesPortion() != null) {
penaltyChargesPortion = penaltyChargesPortion.subtract(accrualActivity.getPenaltyChargesPortion());
}
if (accrualActivity.getInterestPortion() != null) {
interestPortion = interestPortion.subtract(accrualActivity.getInterestPortion());
}
} }
BigDecimal transactionAmount = MathUtil.add(feeChargesPortion, penaltyChargesPortion, interestPortion);
BigDecimal transactionAmount = feeChargesPortion.add(penaltyChargesPortion).add(interestPortion); if (!MathUtil.isGreaterThanZero(transactionAmount)) {
ExternalId externalId = externalIdFactory.create(); return;
}
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date, transactionAmount, null, if (MathUtil.isLessThanZero(feeChargesPortion) || MathUtil.isLessThanZero(penaltyChargesPortion)
interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId); || MathUtil.isLessThanZero(interestPortion)) {
// TODO reverse latest accrual activity if any amount is negative
return;
}
LoanTransaction newActivity = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), date,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null,
externalIdFactory.create());
makeAccrualActivityTransaction(loan, newActivity);
} }
@Override @Override
@Transactional @Transactional
public void processAccrualActivityForLoanReopen(Loan loan) { public void processAccrualActivityForLoanReopen(@NotNull Loan loan) {
LoanTransaction lastAccrualActivityMarkedToReverse = null; if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
List<LoanTransaction> accrualActivityTransaction = loan.getLoanTransactions().stream() return;
.filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity()) }
.sorted(Comparator.comparing(LoanTransaction::getDateOf)).toList();
// grab the latest AccrualActivityTransaction // grab the latest AccrualActivityTransaction
// it does not matter if it is on an installment due date or not because it was posted due to loan close // it does not matter if it is on an installment due date or not because it was posted due to loan close
if (!accrualActivityTransaction.isEmpty()) { LoanTransaction lastAccrualActivityMarkedToReverse = loan.getLoanTransactions().stream()
lastAccrualActivityMarkedToReverse = accrualActivityTransaction.get(accrualActivityTransaction.size() - 1); .filter(loanTransaction -> loanTransaction.isNotReversed() && loanTransaction.isAccrualActivity())
} .sorted(Comparator.comparing(LoanTransaction::getDateOf)).reduce((first, second) -> second).orElse(null);
final LocalDate lastAccrualActivityTransactionDate = lastAccrualActivityMarkedToReverse == null ? null final LocalDate lastAccrualActivityTransactionDate = lastAccrualActivityMarkedToReverse == null ? null
: lastAccrualActivityMarkedToReverse.getDateOf(); : lastAccrualActivityMarkedToReverse.getDateOf();
LocalDate today = DateUtils.getBusinessLocalDate(); LocalDate today = DateUtils.getBusinessLocalDate();
final List<LoanRepaymentScheduleInstallment> installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate = loan final List<LoanRepaymentScheduleInstallment> installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate = loan
.getRepaymentScheduleInstallments().stream() .getRepaymentScheduleInstallments().stream()
.filter(installment -> installment.getDueDate().isBefore(today) && (lastAccrualActivityTransactionDate == null .filter(installment -> installment.getDueDate().isBefore(today)
|| installment.getDueDate().isAfter(lastAccrualActivityTransactionDate) && (DateUtils.isAfter(installment.getDueDate(), lastAccrualActivityTransactionDate)
// if close event happened on installment due date // if close event happened on installment due date
// we should reverse replay it to calculate installment related accrual parts only // we should reverse replay it to calculate installment related accrual parts only
|| installment.getDueDate().isEqual(lastAccrualActivityTransactionDate))) || installment.getDueDate().isEqual(lastAccrualActivityTransactionDate)))
.sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList(); .sorted(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).toList();
for (LoanRepaymentScheduleInstallment installment : installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate) { for (LoanRepaymentScheduleInstallment installment : installmentsBetweenBusinessDateAndLastAccrualActivityTransactionDate) {
if (lastAccrualActivityMarkedToReverse != null) { makeOrReplayActivity(loan, installment, lastAccrualActivityMarkedToReverse);
loanWritePlatformService.reverseReplayAccrualActivityTransaction(loan, lastAccrualActivityMarkedToReverse, installment, lastAccrualActivityMarkedToReverse = null;
installment.getDueDate());
lastAccrualActivityMarkedToReverse = null;
} else {
loanWritePlatformService.makeAccrualActivityTransaction(loan, installment, installment.getDueDate());
}
} }
if (lastAccrualActivityMarkedToReverse != null) { if (lastAccrualActivityMarkedToReverse != null) {
reverseAccrualActivityTransaction(lastAccrualActivityMarkedToReverse); reverseAccrualActivityTransaction(lastAccrualActivityMarkedToReverse);
} }
} }
private void makeOrReplayActivity(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
LoanTransaction existingActivity) {
LocalDate dueDate = installment.getDueDate();
if (existingActivity == null) {
makeAccrualActivityTransaction(loan, installment, dueDate);
} else {
reverseReplayAccrualActivityTransaction(loan, existingActivity, installment, dueDate);
}
}
private LoanTransaction reverseReplayAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanTransaction loanTransaction,
@NotNull LoanRepaymentScheduleInstallment installment, @NotNull LocalDate transactionDate) {
if (validateActivityTransaction(installment, loanTransaction)) {
return loanTransaction;
}
LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (newLoanTransaction != null) {
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
reverseAccrualActivityTransaction(loanTransaction);
return newLoanTransaction;
}
private boolean validateActivityTransaction(@NotNull LoanRepaymentScheduleInstallment installment,
@NotNull LoanTransaction transaction) {
return DateUtils.isEqual(installment.getDueDate(), transaction.getDateOf())
&& MathUtil.isEqualTo(transaction.getInterestPortion(), installment.getInterestCharged())
&& MathUtil.isEqualTo(transaction.getFeeChargesPortion(), installment.getFeeChargesCharged())
&& MathUtil.isEqualTo(transaction.getPenaltyChargesPortion(), installment.getPenaltyCharges());
}
private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
private LoanTransaction makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
@NotNull LocalDate transactionDate) {
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
return newAccrualActivityTransaction == null ? null : makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}
private LoanTransaction makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
newAccrualActivityTransaction = loanAccountDomainService
.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionAccrualActivityPostBusinessEvent(newAccrualActivityTransaction));
return newAccrualActivityTransaction;
}
} }
@@ -0,0 +1,72 @@
/**
* 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 jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent;
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
public class LoanAccrualEventService {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct
public void addListeners() {
businessEventNotifierService.addPostBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
}
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
@Override
public void onBusinessEvent(LoanCloseBusinessEvent event) {
final Loan loan = event.get();
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan closure on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
}
private final class LoanBalanceChangedListener implements BusinessEventListener<LoanBalanceChangedBusinessEvent> {
@Override
public void onBusinessEvent(LoanBalanceChangedBusinessEvent event) {
final Loan loan = event.get();
LoanStatus status = loan.getStatus();
if (status.isClosedObligationsMet() || status.isOverpaid()) {
log.debug("Loan balance change on accrual for loan {}", loan.getId());
loanAccrualsProcessingService.processAccrualsOnLoanClosure(loan);
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
}
}
}
}
@@ -56,14 +56,12 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.office.domain.OfficeRepository;
import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData; import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData;
import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails;
@@ -77,8 +75,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepayme
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -102,9 +98,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanScheduleGeneratorFactory loanScheduleFactory;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final OfficeRepository officeRepository;
private final LoanChargeRepository loanChargeRepository;
private final LoanChargeValidator loanChargeValidator;
/** /**
* method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan
@@ -167,9 +160,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
*/ */
@Override @Override
public void reprocessExistingAccruals(@NotNull Loan loan) { public void reprocessExistingAccruals(@NotNull Loan loan) {
if (isProgressiveAccrual(loan)) {
return;
}
List<LoanTransaction> accrualTransactions = retrieveListOfAccrualTransactions(loan); List<LoanTransaction> accrualTransactions = retrieveListOfAccrualTransactions(loan);
if (!accrualTransactions.isEmpty()) { if (!accrualTransactions.isEmpty()) {
if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) {
@@ -185,7 +175,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
*/ */
@Override @Override
@Transactional @Transactional
public void processAccrualsForInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled) { public void processAccrualsOnInterestRecalculation(@NotNull Loan loan, boolean isInterestRecalculationEnabled, boolean addJournal) {
if (isProgressiveAccrual(loan)) { if (isProgressiveAccrual(loan)) {
return; return;
} }
@@ -194,13 +184,31 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return; return;
} }
try { try {
addPeriodicAccruals(accruedTill, loan); addAccruals(loan, accruedTill, true, false, addJournal);
} catch (Exception e) { } catch (Exception e) {
String globalisationMessageCode = "error.msg.accrual.exception"; String globalisationMessageCode = "error.msg.accrual.exception";
throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e); throw new GeneralPlatformDomainRuleException(globalisationMessageCode, e.getMessage(), e);
} }
} }
@Transactional
@Override
public void addIncomePostingAndAccruals(Long loanId) throws LoanNotFoundException {
if (loanId == null) {
return;
}
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (isProgressiveAccrual(loan)) {
return;
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
processIncomePostingAndAccruals(loan);
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
}
/** /**
* method calculates accruals for loan with interest recalculation and compounding to be posted as income * method calculates accruals for loan with interest recalculation and compounding to be posted as income
*/ */
@@ -228,26 +236,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
} }
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments); LoanRepaymentScheduleInstallment lastInstallment = LoanRepaymentScheduleInstallment.getLastNonDownPaymentInstallment(installments);
reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate()); reverseTransactionsAfter(incomeTransactions, lastInstallment.getDueDate(), false);
reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate()); reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate(), false);
}
@Transactional
@Override
public void addIncomeAndAccrualTransactions(Long loanId) throws LoanNotFoundException {
if (loanId == null) {
return;
}
Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
if (isProgressiveAccrual(loan)) {
return;
}
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
processIncomePostingAndAccruals(loan);
this.loanRepositoryWrapper.saveAndFlush(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
} }
/** /**
@@ -264,32 +254,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
processIncomeAndAccrualTransactionOnLoanClosure(loan); processIncomeAndAccrualTransactionOnLoanClosure(loan);
} }
@Override
public void processAccrualsOnLoanReopen(@NotNull Loan loan) {
if (isProgressiveAccrual(loan)) {
List<LoanTransaction> accruals = retrieveListOfAccrualTransactions(loan);
LocalDate accruedTill = loan.getAccruedTill();
// if accruedTill is null, the all the accrual transactions will be reversed here
if (reverseTransactionsAfter(accruals, accruedTill)) {
return;
}
LoanTransaction closureAccrual = accruals.stream()
.filter(t -> !t.isReversed() && t.isAccrual() && DateUtils.isEqual(t.getTransactionDate(), accruedTill))
.sorted(LoanTransactionComparator.INSTANCE).reduce((first, second) -> second).orElse(null);
if (closureAccrual == null) {
return;
}
int firstInstallmentNumber = fetchFirstNormalInstallmentNumber(loan.getRepaymentScheduleInstallments());
LoanRepaymentScheduleInstallment installment = loan.getRepaymentScheduleInstallment(
i -> isInPeriod(closureAccrual.getDateOf(), i, i.getInstallmentNumber().equals(firstInstallmentNumber)));
if (MathUtil.isGreaterThan(closureAccrual.getInterestPortion(), installment.getInterestCharged())
|| MathUtil.isGreaterThan(closureAccrual.getFeeChargesPortion(), installment.getFeeChargesCharged())
|| MathUtil.isGreaterThan(closureAccrual.getPenaltyChargesPortion(), installment.getPenaltyCharges())) {
reverseAccrual(closureAccrual);
}
}
}
/** /**
* method calculates accruals for loan on loan fore closure * method calculates accruals for loan on loan fore closure
*/ */
@@ -301,7 +265,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
&& (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) { && (loan.getAccruedTill() == null || !DateUtils.isEqual(foreClosureDate, loan.getAccruedTill()))) {
final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate); final LoanRepaymentScheduleInstallment foreCloseDetail = loan.fetchLoanForeclosureDetail(foreClosureDate);
MonetaryCurrency currency = loan.getCurrency(); MonetaryCurrency currency = loan.getCurrency();
reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate); reverseTransactionsAfter(retrieveListOfAccrualTransactions(loan), foreClosureDate, false);
HashMap<String, Object> incomeDetails = new HashMap<>(); HashMap<String, Object> incomeDetails = new HashMap<>();
@@ -331,20 +295,29 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return; return;
} }
List<LoanTransaction> existingAccruals = retrieveListOfAccrualTransactions(loan); List<LoanTransaction> existingAccruals = retrieveListOfAccrualTransactions(loan);
reverseTransactionsAfter(existingAccruals, loan.getLastLoanRepaymentScheduleInstallment().getDueDate()); LocalDate lastDueDate = loan.getLastLoanRepaymentScheduleInstallment().getDueDate();
reverseTransactionsAfter(existingAccruals, lastDueDate, addJournal);
ensureAccrualTransactionMappings(loan); ensureAccrualTransactionMappings(loan);
if (DateUtils.isAfter(tillDate, lastDueDate)) {
tillDate = lastDueDate;
}
boolean progressiveAccrual = isProgressiveAccrual(loan); boolean progressiveAccrual = isProgressiveAccrual(loan);
LocalDate accruedTill = loan.getAccruedTill(); LocalDate accruedTill = loan.getAccruedTill();
if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill) LocalDate businessDate = DateUtils.getBusinessLocalDate();
&& existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), tillDate))) { LocalDate accrualDate = isFinal
return; ? (progressiveAccrual ? (DateUtils.isBefore(lastDueDate, businessDate) ? lastDueDate : businessDate)
: getFinalAccrualTransactionDate(loan))
: tillDate;
if (progressiveAccrual && accruedTill != null && !DateUtils.isAfter(tillDate, accruedTill)) {
if (isFinal) {
reverseTransactionsAfter(existingAccruals, accrualDate, addJournal);
} else if (existingAccruals.stream().anyMatch(t -> !t.isReversed() && !DateUtils.isBefore(t.getDateOf(), accrualDate))) {
return;
}
} }
AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic); AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, tillDate, periodic);
LocalDate accrualDate = isFinal ? (progressiveAccrual ? DateUtils.getBusinessLocalDate() : getFinalAccrualTransactionDate(loan))
: tillDate;
boolean mergeTransactions = isFinal || progressiveAccrual; boolean mergeTransactions = isFinal || progressiveAccrual;
MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency(); MonetaryCurrency currency = loan.getLoanProductRelatedDetail().getCurrency();
List<LoanTransaction> accrualTransactions = new ArrayList<>(); List<LoanTransaction> accrualTransactions = new ArrayList<>();
@@ -399,17 +372,17 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return; return;
} }
if (!isFinal) { if (!isFinal || progressiveAccrual) {
loan.setAccruedTill(tillDate); loan.setAccruedTill(isFinal ? accrualDate : tillDate);
} }
ArrayList<Map<String, Object>> newTransactionMapping = new ArrayList<>(); ArrayList<Map<String, Object>> newTransactionMapping = new ArrayList<>();
for (LoanTransaction accrualTransaction : accrualTransactions) { for (LoanTransaction accrualTransaction : accrualTransactions) {
accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction); accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction);
LoanTransactionBusinessEvent businessEvent = accrualTransaction.isAccrual()
? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
if (addJournal) { if (addJournal) {
LoanTransactionBusinessEvent businessEvent = accrualTransaction.isAccrual()
? new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)
: new LoanAccrualAdjustmentTransactionBusinessEvent(accrualTransaction);
businessEventNotifierService.notifyPostBusinessEvent(businessEvent);
newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode())); newTransactionMapping.add(accrualTransaction.toMapData(currency.getCode()));
} }
} }
@@ -584,7 +557,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency); Money unrecognizedWaived = MathUtil.toMoney(calcChargeUnrecognizedWaivedAmount(paidBys, tillDate), currency);
Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived); Money transactionWaived = MathUtil.minusToZero(waived, unrecognizedWaived);
chargeData.setChargeAccrued(MathUtil.minusToZero(MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency), transactionWaived)); Money transactionAccrued = MathUtil.toMoney(calcChargeAccruedAmount(paidBys), currency);
chargeData.setTransactionAccrued(transactionAccrued);
chargeData.setChargeAccrued(MathUtil.minusToZero(transactionAccrued, transactionWaived));
duePeriod.addCharge(chargeData); duePeriod.addCharge(chargeData);
} }
@@ -720,17 +695,29 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
return; return;
} }
ensureAccrualTransactionMappings(loan); ensureAccrualTransactionMappings(loan);
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment();
boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate(); LocalDate lastDueDate = lastInstallment.getDueDate();
for (LoanRepaymentScheduleInstallment installment : installments) { if (isProgressiveAccrual(loan)) {
checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment); AccrualPeriodsData accrualPeriods = calculateAccrualAmounts(loan, lastDueDate, true);
for (AccrualPeriodData period : accrualPeriods.getPeriods()) {
Money interestAccrued = period.getTransactionAccrued();
Money feeAccrued = period.getFeeTransactionAccrued();
Money penaltyAccrued = period.getPenaltyTransactionAccrued();
LoanRepaymentScheduleInstallment installment = loan.fetchRepaymentScheduleInstallment(period.getInstallmentNumber());
installment.updateAccrualPortion(interestAccrued, feeAccrued, penaltyAccrued);
}
} else {
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
boolean isBasedOnSubmittedOnDate = !isChargeOnDueDate();
for (LoanRepaymentScheduleInstallment installment : installments) {
checkAndUpdateAccrualsForInstallment(loan, accrualTransactions, installments, isBasedOnSubmittedOnDate, installment);
}
} }
// reverse accruals after last installment // reverse accruals after last installment
LoanRepaymentScheduleInstallment lastInstallment = loan.getLastLoanRepaymentScheduleInstallment(); reverseTransactionsAfter(accrualTransactions, lastDueDate, false);
reverseTransactionsAfter(accrualTransactions, lastInstallment.getDueDate());
} }
private void reprocessNonPeriodicAccruals(Loan loan, final List<LoanTransaction> accruals) { private void reprocessNonPeriodicAccruals(Loan loan, final List<LoanTransaction> accrualTransactions) {
if (isProgressiveAccrual(loan)) { if (isProgressiveAccrual(loan)) {
return; return;
} }
@@ -738,29 +725,25 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
ExternalId externalId = ExternalId.empty(); ExternalId externalId = ExternalId.empty();
boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled(); boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled();
for (LoanTransaction loanTransaction : accruals) { for (LoanTransaction accrualTransaction : accrualTransactions) {
if (loanTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) { if (accrualTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) {
if (loanTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) { if (accrualTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, accrualTransaction.reverse();
"reversed");
loanTransaction.reverse();
if (isExternalIdAutoGenerationEnabled) { if (isExternalIdAutoGenerationEnabled) {
externalId = ExternalId.generate(); externalId = ExternalId.generate();
} }
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan, final LoanTransaction interestAccrualTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan,
interestApplied, loan.getDisbursementDate(), externalId); interestApplied, loan.getDisbursementDate(), externalId);
loan.addLoanTransaction(interestAppliedTransaction); loan.addLoanTransaction(interestAccrualTransaction);
} }
} else { } else {
Set<LoanChargePaidBy> chargePaidBies = loanTransaction.getLoanChargesPaid(); Set<LoanChargePaidBy> chargePaidBies = accrualTransaction.getLoanChargesPaid();
for (final LoanChargePaidBy chargePaidBy : chargePaidBies) { for (final LoanChargePaidBy chargePaidBy : chargePaidBies) {
LoanCharge loanCharge = chargePaidBy.getLoanCharge(); LoanCharge loanCharge = chargePaidBy.getLoanCharge();
Money chargeAmount = loanCharge.getAmount(loan.getCurrency()); Money chargeAmount = loanCharge.getAmount(loan.getCurrency());
if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(loan.getCurrency()))) { if (chargeAmount.isNotEqualTo(accrualTransaction.getAmount(loan.getCurrency()))) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), accrualTransaction.reverse();
loanTransaction, "reversed"); loan.handleChargeAppliedTransaction(loanCharge, accrualTransaction.getTransactionDate());
loanTransaction.reverse();
loan.handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate());
} }
} }
} }
@@ -786,8 +769,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
interest = interest.minus(accrualTransaction.getInterestPortion(currency)); interest = interest.minus(accrualTransaction.getInterestPortion(currency));
fee = fee.minus(accrualTransaction.getFeeChargesPortion(currency)); fee = fee.minus(accrualTransaction.getFeeChargesPortion(currency));
penalty = penalty.minus(accrualTransaction.getPenaltyChargesPortion(currency)); penalty = penalty.minus(accrualTransaction.getPenaltyChargesPortion(currency));
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(accrualTransaction.getLoan(),
accrualTransaction, "reversed");
accrualTransaction.reverse(); accrualTransaction.reverse();
} }
} }
@@ -869,8 +850,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
compoundingDetail.getAmount(), interest, fee, penalties, externalId); compoundingDetail.getAmount(), interest, fee, penalties, externalId);
loan.addLoanTransaction(transaction); loan.addLoanTransaction(transaction);
} else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) { } else if (existingIncomeTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(compoundingDetail.getAmount()) != 0) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingIncomeTransaction.getLoan(),
existingIncomeTransaction, "reversed");
existingIncomeTransaction.reverse(); existingIncomeTransaction.reverse();
LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), LoanTransaction transaction = LoanTransaction.incomePosting(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
compoundingDetail.getAmount(), interest, fee, penalties, externalId); compoundingDetail.getAmount(), interest, fee, penalties, externalId);
@@ -889,8 +868,6 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
if (existingAccrualTransaction == null if (existingAccrualTransaction == null
|| !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) { || !MathUtil.isEqualTo(existingAccrualTransaction.getAmount(), compoundingDetail.getAmount())) {
if (existingAccrualTransaction != null) { if (existingAccrualTransaction != null) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(existingAccrualTransaction.getLoan(),
existingAccrualTransaction, "reversed");
existingAccrualTransaction.reverse(); existingAccrualTransaction.reverse();
} }
LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(), LoanTransaction accrual = LoanTransaction.accrueTransaction(loan, loan.getOffice(), compoundingDetail.getEffectiveDate(),
@@ -910,8 +887,8 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
&& loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) { && loan.getStatus().isClosedObligationsMet() && !loan.isNpa() && !loan.isChargedOff()) {
LocalDate closedDate = loan.getClosedOnDate(); LocalDate closedDate = loan.getClosedOnDate();
reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate); reverseTransactionsOnOrAfter(retrieveListOfIncomePostingTransactions(loan), closedDate, false);
reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate); reverseTransactionsOnOrAfter(retrieveListOfAccrualTransactions(loan), closedDate, false);
HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>(); HashMap<String, BigDecimal> cumulativeIncomeFromInstallments = new HashMap<>();
determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments); determineCumulativeIncomeFromInstallments(loan, cumulativeIncomeFromInstallments);
@@ -1188,36 +1165,34 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
} }
} }
private boolean reverseTransactionsAfter(List<LoanTransaction> transactions, LocalDate effectiveDate) { private boolean reverseTransactionsAfter(List<LoanTransaction> accrualTransactions, LocalDate effectiveDate, boolean addEvent) {
boolean reversed = false; boolean reversed = false;
for (LoanTransaction loanTransaction : transactions) { for (LoanTransaction accrualTransaction : accrualTransactions) {
if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { if (!accrualTransaction.isReversed() && DateUtils.isAfter(accrualTransaction.getTransactionDate(), effectiveDate)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, reverseAccrual(accrualTransaction, addEvent);
"reversed");
reverseAccrual(loanTransaction);
reversed = true; reversed = true;
} }
} }
return reversed; return reversed;
} }
private boolean reverseTransactionsOnOrAfter(List<LoanTransaction> transactions, LocalDate date) { private boolean reverseTransactionsOnOrAfter(List<LoanTransaction> accrualTransactions, LocalDate date, boolean addEvent) {
boolean reversed = false; boolean reversed = false;
for (LoanTransaction loanTransaction : transactions) { for (LoanTransaction accrualTransaction : accrualTransactions) {
if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) { if (!accrualTransaction.isReversed() && !DateUtils.isBefore(accrualTransaction.getTransactionDate(), date)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, reverseAccrual(accrualTransaction, addEvent);
"reversed");
reverseAccrual(loanTransaction);
reversed = true; reversed = true;
} }
} }
return reversed; return reversed;
} }
private void reverseAccrual(LoanTransaction transaction) { private void reverseAccrual(LoanTransaction transaction, boolean addEvent) {
transaction.reverse(); transaction.reverse();
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction); if (addEvent) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
} }
private LocalDate getFinalAccrualTransactionDate(Loan loan) { private LocalDate getFinalAccrualTransactionDate(Loan loan) {
@@ -1229,7 +1204,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
} }
public boolean isProgressiveAccrual(@NotNull Loan loan) { public boolean isProgressiveAccrual(@NotNull Loan loan) {
return loan.getLoanProductRelatedDetail().getLoanScheduleType() == LoanScheduleType.PROGRESSIVE; return loan.isProgressiveSchedule();
} }
private void setSetHelpers(Loan loan) { private void setSetHelpers(Loan loan) {
@@ -301,17 +301,18 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && isAppliedOnBackDate
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) { && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge));
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withEntityId(loanCharge.getId()) // .withEntityId(loanCharge.getId()) //
.withEntityExternalId(loanCharge.getExternalId()) // .withEntityExternalId(loanCharge.getExternalId()) //
@@ -825,14 +826,14 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && runInterestRecalculation
&& loan.isFeeCompoundingEnabledForInterestRecalculation()) { && loan.isFeeCompoundingEnabledForInterestRecalculation()) {
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, true, false);
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
} }
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
} }
} }
@@ -1050,9 +1051,11 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|| (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled() || (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()
&& DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate()))) { && DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate()))) {
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null); final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null);
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction); if (applyLoanChargeTransaction != null) {
businessEventNotifierService this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction)); businessEventNotifierService
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));
}
} }
return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate()); return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate());
} }
@@ -32,7 +32,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService { public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlatformService {
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService; private final LoanAccrualActivityProcessingService loanAccrualActivityProcessingService;
@PostConstruct @PostConstruct
@@ -42,21 +41,14 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
new LoanAccrualActivityPostingLoanStatusChangedListener()); new LoanAccrualActivityPostingLoanStatusChangedListener());
} }
private final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> { private static final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
@Override @Override
public void onBusinessEvent(LoanStatusChangedBusinessEvent event) { public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
final Loan loan = event.get(); final Loan loan = event.get();
log.debug("Loan Status change for loan {}", loan.getId()); log.debug("Loan Status change for loan {}", loan.getId());
LoanStatus oldStatus = event.getOldStatus(); if (loan.isOpen()) {
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(); loan.handleMaturityDateActivate();
loanAccrualsProcessingService.processAccrualsOnLoanReopen(loan);
} }
} }
} }
@@ -70,9 +62,7 @@ public class LoanStatusChangePlatformServiceImpl implements LoanStatusChangePlat
if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) { if (loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
LoanStatus oldStatus = event.getOldStatus(); LoanStatus oldStatus = event.getOldStatus();
LoanStatus newStatus = loan.getStatus(); LoanStatus newStatus = loan.getStatus();
if (oldStatus.isActive() && (newStatus.isClosedObligationsMet() || newStatus.isOverpaid())) { if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanClosure(loan);
} else if ((oldStatus.isClosed() || oldStatus.isOverpaid()) && newStatus.isActive()) {
loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan); loanAccrualActivityProcessingService.processAccrualActivityForLoanReopen(loan);
} }
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.service; package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.validation.constraints.NotNull;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Map; import java.util.Map;
@@ -26,7 +27,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -68,16 +69,17 @@ public class LoanTransactionAssembler {
txnExternalId, null); txnExternalId, null);
} }
public LoanTransaction assembleAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment, public LoanTransaction assembleAccrualActivityTransaction(@NotNull Loan loan, @NotNull LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) { @NotNull LocalDate transactionDate) {
MonetaryCurrency currency = loan.getCurrency();
ExternalId externalId = externalIdFactory.create(); ExternalId externalId = externalIdFactory.create();
BigDecimal interestPortion = installment.getInterestCharged(currency).getAmount(); BigDecimal interestPortion = installment.getInterestCharged();
BigDecimal feeChargesPortion = installment.getFeeChargesCharged(currency).getAmount(); BigDecimal feeChargesPortion = installment.getFeeChargesCharged();
BigDecimal penaltyChargesPortion = installment.getPenaltyChargesCharged(currency).getAmount(); BigDecimal penaltyChargesPortion = installment.getPenaltyCharges();
BigDecimal transactionAmount = interestPortion.add(feeChargesPortion).add(penaltyChargesPortion); BigDecimal transactionAmount = MathUtil.add(interestPortion, feeChargesPortion, penaltyChargesPortion);
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), transactionDate, return MathUtil.isGreaterThanZero(transactionAmount)
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId); ? new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.ACCRUAL_ACTIVITY.getValue(), transactionDate,
transactionAmount, null, interestPortion, feeChargesPortion, penaltyChargesPortion, null, false, null, externalId)
: null;
} }
} }
@@ -95,8 +95,6 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanWithdra
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeOffPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanDisbursalTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionAccrualActivityPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
@@ -230,7 +228,6 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final LoanTransactionValidator loanTransactionValidator; private final LoanTransactionValidator loanTransactionValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer; private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
@@ -499,9 +496,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
createNote(loan, command, changes); createNote(loan, command, changes);
// auto create standing instruction // auto create standing instruction
createStandingInstruction(loan); createStandingInstruction(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
} }
final Set<LoanCharge> loanCharges = loan.getActiveCharges(); final Set<LoanCharge> loanCharges = loan.getActiveCharges();
@@ -526,8 +520,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
// Post Dated Checks // Post Dated Checks
@@ -551,6 +545,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction));
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(loan.getId()) // .withEntityId(loan.getId()) //
@@ -869,8 +866,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
updateRecurringCalendarDatesForInterestRecalculation(loan); updateRecurringCalendarDatesForInterestRecalculation(loan);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), true);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalBusinessEvent(loan));
} }
@@ -1085,8 +1082,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan, private ChangedTransactionDetail recalculateLoanWithInterestPaymentWaiverTxn(Loan loan,
LoanTransaction newInterestPaymentWaiverTransaction) { LoanTransaction newInterestPaymentWaiverTransaction) {
LocalDate recalculateFrom = null; LocalDate recalculateFrom = null;
LocalDate transactionDate = newInterestPaymentWaiverTransaction.getTransactionDate();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
recalculateFrom = newInterestPaymentWaiverTransaction.getTransactionDate(); recalculateFrom = transactionDate;
} }
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null); final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null);
@@ -1102,14 +1100,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor();
final LoanRepaymentScheduleInstallment currentInstallment = loan final LoanRepaymentScheduleInstallment currentInstallment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(transactionDate);
.fetchLoanRepaymentScheduleInstallmentByDueDate(newInterestPaymentWaiverTransaction.getTransactionDate());
boolean reprocess = true; boolean reprocess = true;
if (!loan.isForeclosure() && isTransactionChronologicallyLatest && DateUtils.isEqualBusinessDate(transactionDate)
if (!loan.isForeclosure() && isTransactionChronologicallyLatest && currentInstallment != null && currentInstallment.getTotalOutstanding(loan.getCurrency())
&& DateUtils.isEqualBusinessDate(newInterestPaymentWaiverTransaction.getTransactionDate()) && currentInstallment != null
&& currentInstallment.getTotalOutstanding(loan.getCurrency())
.isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) { .isEqualTo(newInterestPaymentWaiverTransaction.getAmount(loan.getCurrency()))) {
reprocess = false; reprocess = false;
} }
@@ -1155,66 +1150,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return loan.reprocessTransactions(); return loan.reprocessTransactions();
} }
@Transactional
@Override
public Loan reverseReplayAccrualActivityTransaction(Loan loan, final LoanTransaction loanTransaction,
final LoanRepaymentScheduleInstallment installment, final LocalDate transactionDate) {
LoanTransaction newLoanTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf())
|| !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction,
"reversed");
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
loanTransaction, LoanTransactionRelationTypeEnum.REPLAYED));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newLoanTransaction);
loan.addLoanTransaction(newLoanTransaction);
LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction);
data.setNewTransactionDetail(newLoanTransaction);
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
}
return loan;
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, final LoanRepaymentScheduleInstallment installment,
final LocalDate transactionDate) {
LoanTransaction newAccrualActivityTransaction = loanTransactionAssembler.assembleAccrualActivityTransaction(loan, installment,
transactionDate);
if (newAccrualActivityTransaction.getAmount().compareTo(BigDecimal.ZERO) == 0) {
return loan;
}
return makeAccrualActivityTransaction(loan, newAccrualActivityTransaction);
}
@Transactional
@Override
public Loan makeAccrualActivityTransaction(Loan loan, LoanTransaction newAccrualActivityTransaction) {
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionAccrualActivityPreBusinessEvent(loan));
loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(newAccrualActivityTransaction);
loan.addLoanTransaction(newAccrualActivityTransaction);
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionAccrualActivityPostBusinessEvent(newAccrualActivityTransaction));
return loan;
}
@Transactional @Transactional
@Override @Override
public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) { public CommandProcessingResult makeInterestPaymentWaiver(final JsonCommand command) {
loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command); loanTransactionValidator.validateLoanTransactionInterestPaymentWaiver(command);
final Long loanId = command.getLoanId(); final Long loanId = command.getLoanId();
@@ -1245,10 +1183,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
saveNote(noteText, loan, newInterestPaymentWaiverTransaction); saveNote(noteText, loan, newInterestPaymentWaiverTransaction);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
// disable all active standing orders linked to this loan if status changes to closed // disable all active standing orders linked to this loan if status changes to closed
@@ -1264,6 +1200,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent); businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withLoanId(loan.getId()) // .withLoanId(loan.getId()) //
.withEntityId(newInterestPaymentWaiverTransaction.getId()) // .withEntityId(newInterestPaymentWaiverTransaction.getId()) //
@@ -1564,11 +1503,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.updateLoanSummaryAndStatus(); loan.updateLoanSummaryAndStatus();
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -1586,6 +1522,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData)); businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(eventData));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(entityId) // .withEntityId(entityId) //
@@ -1647,9 +1586,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
writeOffTransaction.reverse(); writeOffTransaction.reverse();
} }
if (loan.isClosedObligationsMet() || loan.isClosedWrittenOff() || loan.isClosedWithOutstandingAmountMarkedForReschedule()) { loan.updateLoanSummaryAndStatus();
loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION, loan);
}
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) { if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
changedTransactionDetail = loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, changedTransactionDetail = loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan,
@@ -1850,14 +1787,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note); this.noteRepository.save(note);
} }
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveInterestBusinessEvent(waiveInterestTransaction));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(waiveInterestTransaction.getId()) // .withEntityId(waiveInterestTransaction.getId()) //
@@ -1918,44 +1856,48 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.WRITE_OFF_OUTSTANDING, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.WRITE_OFF_OUTSTANDING,
writtenOffOnLocalDate); writtenOffOnLocalDate);
final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); CommandProcessingResultBuilder builder = new CommandProcessingResultBuilder() //
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, writeOff, noteText);
this.noteRepository.save(note);
}
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
return new CommandProcessingResultBuilder() //
.withCommandId(command.commandId()) // .withCommandId(command.commandId()) //
.withEntityId(writeOff.getId()) //
.withEntityExternalId(writeOff.getExternalId()) //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
.withGroupId(loan.getGroupId()) // .withGroupId(loan.getGroupId()) //
.withLoanId(loanId) // .withLoanId(loanId) //
.with(changes).build(); .with(changes);
final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
if (changedTransactionDetail != null) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
}
LoanTransaction writeOff = changedTransactionDetail.getNewTransactionMappings().remove(0L);
this.loanTransactionRepository.saveAndFlush(writeOff);
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue());
this.accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue());
}
saveLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
if (StringUtils.isNotBlank(noteText)) {
changes.put("note", noteText);
final Note note = Note.loanTransactionNote(loan, writeOff, noteText);
this.noteRepository.save(note);
}
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanWrittenOffPostBusinessEvent(writeOff));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
builder.withEntityId(writeOff.getId()).withEntityExternalId(writeOff.getExternalId());
}
return builder.build();
} }
@Transactional @Transactional
@@ -2020,12 +1962,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.noteRepository.save(note); this.noteRepository.save(note);
} }
if (possibleClosingTransaction != null) {
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
}
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
@@ -2037,6 +1976,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// disable all active standing instructions linked to the loan // disable all active standing instructions linked to the loan
this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan);
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
CommandProcessingResult result; CommandProcessingResult result;
if (possibleClosingTransaction != null) { if (possibleClosingTransaction != null) {
@@ -2654,15 +2596,17 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
if (writeOffTransaction != null) {
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
}
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -2811,11 +2755,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
createLoanScheduleArchive(loan, scheduleGeneratorDTO); createLoanScheduleArchive(loan, scheduleGeneratorDTO);
} }
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate());
return new CommandProcessingResultBuilder() // return new CommandProcessingResultBuilder() //
.withOfficeId(loan.getOfficeId()) // .withOfficeId(loan.getOfficeId()) //
.withClientId(loan.getClientId()) // .withClientId(loan.getClientId()) //
@@ -2879,11 +2825,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail);
} }
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan,
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanInterestRecalculationBusinessEvent(loan));
return loan; return loan;
} }
@@ -3396,8 +3343,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
// Create journal entries for the new transaction(s)
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
// Calculate and apply delinquency tag (if applicable) // Calculate and apply delinquency tag (if applicable)
loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate);
// disable all active standing orders linked to this loan if status // disable all active standing orders linked to this loan if status
@@ -3414,11 +3359,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, refundTransaction); loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, refundTransaction);
} }
// Raise business events // Raise business events
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan,
loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled(), false);
loan.repaymentScheduleDetail().isInterestRecalculationEnabled());
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
// Create journal entries for the new transaction(s)
postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds);
Long entityId = refundTransaction.getId(); Long entityId = refundTransaction.getId();
ExternalId entityExternalId = refundTransaction.getExternalId(); ExternalId entityExternalId = refundTransaction.getExternalId();
Long subEntityId = interestRefundTransaction != null ? interestRefundTransaction.getId() : null; Long subEntityId = interestRefundTransaction != null ? interestRefundTransaction.getId() : null;
@@ -100,6 +100,7 @@ import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlat
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformService;
import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoWritePlatformServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualActivityProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventServiceImpl;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
@@ -332,9 +333,16 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanStatusChangePlatformService.class) @ConditionalOnMissingBean(LoanStatusChangePlatformService.class)
public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService, public LoanStatusChangePlatformService loanStatusChangePlatformService(BusinessEventNotifierService businessEventNotifierService,
LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualActivityProcessingService);
}
@Bean
@ConditionalOnMissingBean(LoanAccrualEventService.class)
public LoanAccrualEventService loanAccrualEventService(BusinessEventNotifierService businessEventNotifierService,
LoanAccrualsProcessingService loanAccrualsProcessingService, LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) { LoanAccrualActivityProcessingService loanAccrualActivityProcessingService) {
return new LoanStatusChangePlatformServiceImpl(businessEventNotifierService, loanAccrualsProcessingService, return new LoanAccrualEventService(businessEventNotifierService, loanAccrualsProcessingService,
loanAccrualActivityProcessingService); loanAccrualActivityProcessingService);
} }
@@ -351,8 +359,8 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class) @ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory, public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context,
PlatformSecurityContext context, LoanTransactionValidator loanTransactionValidator, LoanTransactionValidator loanTransactionValidator,
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper, LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository, LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository, LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
@@ -381,18 +389,17 @@ public class LoanAccountConfiguration {
LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator,
LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService, LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService,
LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) { LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator, return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer,
loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository,
loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository,
calendarInstanceRepository, paymentDetailWritePlatformService, holidayRepository, configurationDomainService, paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository,
workingDaysRepository, accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountAssociationsReadPlatformService,
accountAssociationsReadPlatformService, loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanScheduleHistoryWritePlatformService,
loanScheduleHistoryWritePlatformService, loanApplicationValidator, accountAssociationRepository, loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService,
accountTransferDetailRepository, businessEventNotifierService, guarantorDomainService, loanUtilService, loanSummaryWrapper, guarantorDomainService, loanUtilService, loanSummaryWrapper, entityDatatableChecksWritePlatformService,
entityDatatableChecksWritePlatformService, transactionProcessingStrategy, codeValueRepository, transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository,
cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler, repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository,
postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService); loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService);
@@ -128,6 +128,9 @@ class LoanChargeWritePlatformServiceImplTest {
@Mock @Mock
private LoanChargeValidator loanChargeValidator; private LoanChargeValidator loanChargeValidator;
@Mock
private LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan); when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
@@ -240,8 +240,6 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList()); when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any()); doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
when(loan.isClosedWrittenOff()).thenReturn(false); when(loan.isClosedWrittenOff()).thenReturn(false);
when(loan.isClosedObligationsMet()).thenReturn(false);
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true); when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act // Act
@@ -287,8 +285,6 @@ class LoanWritePlatformServiceJpaRepositoryImplTest {
when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList()); when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList());
doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any()); doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any());
when(loan.isClosedWrittenOff()).thenReturn(false); when(loan.isClosedWrittenOff()).thenReturn(false);
when(loan.isClosedObligationsMet()).thenReturn(false);
when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true); when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act // Act
@@ -553,7 +553,7 @@ public abstract class BaseLoanIntegrationTest {
protected void verifyTransactions(Long loanId, Transaction... transactions) { protected void verifyTransactions(Long loanId, Transaction... transactions) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
if (transactions == null || transactions.length == 0) { if (transactions == null || transactions.length == 0) {
Assertions.assertTrue(loanDetails.getTransactions().isEmpty(), "No transaction is expected"); Assertions.assertTrue(loanDetails.getTransactions().isEmpty(), "No transaction is expected on loan " + loanId);
} else { } else {
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size()); Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
Arrays.stream(transactions).forEach(tr -> { Arrays.stream(transactions).forEach(tr -> {
@@ -562,12 +562,13 @@ public abstract class BaseLoanIntegrationTest {
&& Objects.equals(item.getType().getValue(), tr.type) // && Objects.equals(item.getType().getValue(), tr.type) //
&& Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter))) && Objects.equals(item.getDate(), LocalDate.parse(tr.date, dateTimeFormatter)))
.findFirst(); .findFirst();
Assertions.assertTrue(optTx.isPresent(), "Required transaction not found: " + tr); Assertions.assertTrue(optTx.isPresent(), "Required transaction not found: " + tr + " on loan " + loanId);
GetLoansLoanIdTransactions tx = optTx.get(); GetLoansLoanIdTransactions tx = optTx.get();
if (tr.reversed != null) { if (tr.reversed != null) {
Assertions.assertEquals(tr.reversed, tx.getManuallyReversed(), "Transaction is not reversed: " + tr); Assertions.assertEquals(tr.reversed, tx.getManuallyReversed(),
"Transaction is not reversed: " + tr + " on loan " + loanId);
} }
}); });
} }
@@ -576,9 +577,9 @@ public abstract class BaseLoanIntegrationTest {
protected void verifyTransactions(Long loanId, TransactionExt... transactions) { protected void verifyTransactions(Long loanId, TransactionExt... transactions) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
if (transactions == null || transactions.length == 0) { if (transactions == null || transactions.length == 0) {
assertNull(loanDetails.getTransactions(), "No transaction is expected"); assertNull(loanDetails.getTransactions(), "No transaction is expected on loan " + loanId);
} else { } else {
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size()); Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size(), "Number of transactions on loan " + loanId);
Arrays.stream(transactions).forEach(tr -> { Arrays.stream(transactions).forEach(tr -> {
boolean found = loanDetails.getTransactions().stream().anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) // boolean found = loanDetails.getTransactions().stream().anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) //
&& Objects.equals(item.getType().getValue(), tr.type) // && Objects.equals(item.getType().getValue(), tr.type) //
@@ -591,7 +592,7 @@ public abstract class BaseLoanIntegrationTest {
&& Objects.equals(item.getOverpaymentPortion(), tr.overpaymentPortion) // && Objects.equals(item.getOverpaymentPortion(), tr.overpaymentPortion) //
&& Objects.equals(item.getUnrecognizedIncomePortion(), tr.unrecognizedPortion) // && Objects.equals(item.getUnrecognizedIncomePortion(), tr.unrecognizedPortion) //
); );
Assertions.assertTrue(found, "Required transaction not found: " + tr); Assertions.assertTrue(found, "Required transaction not found: " + tr + " on loan " + loanId);
}); });
} }
} }
@@ -27,6 +27,7 @@ import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -78,15 +79,20 @@ public class LoanChargeProgressiveTest extends BaseLoanIntegrationTest {
addLoanCharge(loanId, chargeResponse.getResourceId(), "03 October 2024", 20.0d); addLoanCharge(loanId, chargeResponse.getResourceId(), "03 October 2024", 20.0d);
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertTrue( Assertions.assertTrue(
loanDetails.getTransactions().stream().anyMatch(t -> t.getType().getAccrual() && t.getAmount().equals(20.0d))); loanDetails.getTransactions().stream().noneMatch(t -> t.getType().getAccrual() && t.getAmount().equals(20.0d)));
}); });
runAt("04 October 2024", () -> { runAt("04 October 2024", () -> {
globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY, globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY,
false); false);
executeInlineCOB(loanId); executeInlineCOB(loanId);
final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertTrue( Assertions.assertTrue(loanDetails.getTransactions().stream()
loanDetails.getTransactions().stream().anyMatch(t -> t.getType().getAccrual() && t.getAmount().equals(20.0d))); .anyMatch(t -> t.getType().getAccrual() && t.getFeeChargesPortion().equals(20.0d)));
}); });
} }
@AfterEach
public void afterEach() {
globalConfigurationHelper.manageConfigurations(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY, false);
}
} }
@@ -300,6 +300,7 @@ 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 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(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(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) // transaction(100.0, "Chargeback", "02 May 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
); );
@@ -584,6 +585,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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) // transaction(100.0, "Chargeback", "02 May 2023", 50.0, 100.0, 0.0, 0.0, 0.0, 0.0, 50.0) //
); );
@@ -649,6 +651,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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) // transaction(100.0, "Chargeback", "20 January 2023", 30.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
); );
@@ -715,6 +718,7 @@ public class LoanChargebackWithCreditAllocationsIntegrationTests extends BaseLoa
verifyTransactions(loanId, // verifyTransactions(loanId, //
transaction(1250.0, "Disbursement", "01 January 2023", 1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // 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(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) // transaction(100.0, "Chargeback", "20 January 2023", 0.0, 30.0, 0.0, 50.0, 20.0, 0.0, 50.0) //
); );
@@ -210,9 +210,11 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), transaction(5.74, "Accrual", "22 January 2021"), transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.74, "Interest Refund", "22 January 2021")); transaction(5.74, "Accrual", "22 January 2021"), //
transaction(5.74, "Interest Refund", "22 January 2021") //
);
}); });
} }
@@ -241,9 +243,11 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(5.74, "Accrual", "22 January 2021"), transaction(1000.0, "Payout Refund", "22 January 2021"), //
transaction(5.74, "Interest Refund", "22 January 2021")); transaction(5.74, "Accrual", "22 January 2021"), //
transaction(5.74, "Interest Refund", "22 January 2021") //
);
}); });
} }
@@ -272,9 +276,10 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Payout Refund", "01 February 2021"), transaction(8.48, "Accrual", "01 February 2021"), transaction(1000.0, "Payout Refund", "01 February 2021"), //
transaction(8.48, "Interest Refund", "01 February 2021")); transaction(8.48, "Accrual", "01 February 2021"), //
transaction(8.48, "Interest Refund", "01 February 2021")); //
}); });
} }
@@ -315,9 +320,12 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), 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(87.89, "Repayment", "01 February 2021"), //
transaction(10.49, "Interest Refund", "09 February 2021"), transaction(10.49, "Accrual", "09 February 2021")); transaction(1000.0, "Payout Refund", "09 February 2021"), //
transaction(10.49, "Interest Refund", "09 February 2021"), //
transaction(10.49, "Accrual", "09 February 2021") //
);
}); });
} }
@@ -350,9 +358,12 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(750.0, "Disbursement", "01 January 2021"), verifyTransactions(loanId, transaction(750.0, "Disbursement", "01 January 2021"), //
transaction(250.0, "Disbursement", "01 January 2021"), transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(250.0, "Disbursement", "01 January 2021"), //
transaction(5.74, "Accrual", "22 January 2021"), transaction(5.74, "Interest Refund", "22 January 2021")); transaction(1000.0, "Payout Refund", "22 January 2021"), //
transaction(5.74, "Accrual", "22 January 2021"), //
transaction(5.74, "Interest Refund", "22 January 2021") //
);
}); });
} }
@@ -386,9 +397,12 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(750.0, "Disbursement", "04 January 2021"), verifyTransactions(loanId, transaction(750.0, "Disbursement", "04 January 2021"), //
transaction(250.0, "Disbursement", "01 January 2021"), transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(250.0, "Disbursement", "01 January 2021"), //
transaction(5.13, "Accrual", "22 January 2021"), transaction(5.13, "Interest Refund", "22 January 2021")); transaction(1000.0, "Payout Refund", "22 January 2021"), //
transaction(5.13, "Accrual", "22 January 2021"), //
transaction(5.13, "Interest Refund", "22 January 2021") //
);
}); });
} }
@@ -435,10 +449,13 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), 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(500.0, "Disbursement", "07 January 2021"), //
transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"), //
transaction(9.67, "Accrual", "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") //
);
}); });
} }
@@ -1115,9 +1132,10 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(85.63, "Repayment", "10 January 2021"), // transaction(85.63, "Repayment", "10 January 2021"), //
transaction(5.7, "Accrual", "22 January 2021"), // transaction(5.70, "Accrual", "22 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 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(0.28, "Accrual Adjustment", "22 January 2021") //
); );
}); });
} }
@@ -1202,7 +1220,8 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
reversedTransaction(85.63, "Repayment", "10 January 2021"), // reversedTransaction(85.63, "Repayment", "10 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), // transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.70, "Interest Refund", "22 January 2021"), // transaction(5.70, "Interest Refund", "22 January 2021"), //
transaction(5.42, "Accrual", "22 January 2021") // transaction(5.42, "Accrual", "22 January 2021"), //
transaction(0.28, "Accrual", "22 January 2021") //
); );
}); });
@@ -569,8 +569,8 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
verifyTransactions(loanId.get(), verifyTransactions(loanId.get(),
transaction(1000.0, "Disbursement", disbursementDay, 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), // 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(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)); // transaction(70.0, "Accrual Activity", repaymentPeriod1DueDate, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false)); //
}); });
} }
@@ -652,32 +652,36 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1); addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1);
verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), // verifyTransactions(loanId.get(),
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
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(70.0, "Accrual", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), //
transaction(70.0, "Accrual Activity", repaymentDate1, 0.0, 0.0, 0.0, 40.0, 30.0, 0.0, 0.0, false), // transaction(70.0, "Accrual Activity", repaymentDate1, 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)); // );
}); });
runAt(disbursementDay2, () -> { runAt(disbursementDay2, () -> {
loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay2) loanTransactionHelper.disburseLoan(loanId.get(), new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay2)
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(500.0)).locale("en")); .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), // verifyTransactions(loanId.get(),
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), //
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); // 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) //
);
}); });
runAt(repaymentPeriod1CloseDate, () -> { runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get())); 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), // verifyTransactions(loanId.get(),
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false), //
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", 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(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)); //
}); });
} }
@@ -727,6 +731,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.0, false), // verifyTransactions(loanId.get(), transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 0.0, 40.0, 30.0, 0.0, 80.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", disbursementDay2, 420.0, 0.0, 0.0, 0.0, 0.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(70.0, "Accrual Activity", 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)); // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, false)); //
@@ -828,6 +833,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false), 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(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(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)); transaction(500.0, "Disbursement", disbursementDay2, 500, 0, 0, 0, 0, 0, 0, false));
}); });
@@ -837,9 +843,9 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false), verifyTransactions(loanId.get(), transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false),
transaction(570.0, "Repayment", repaymentDate1, 0.0, 500.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", 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(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));
}); });
} }
@@ -867,18 +873,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate); chargeFee(loanId.get(), 40.0, repaymentPeriod1DueDate);
addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1); addRepaymentForLoan(loanId.get(), 650.0, repaymentDate1);
verifyTransactions(loanId.get(), verifyTransactions(loanId.get(),
transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), // transaction(109.45, transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), //
// "Accrual",
// repaymentDate1,
// 0.0,
// 0,
// 39.45,
// 40.0,
// 30.0,
// 0,
// 0,
// false),
// //
transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), //
transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), //
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) //
@@ -887,18 +882,7 @@ public class LoanTransactionAccrualActivityPostingTest extends BaseLoanIntegrati
runAt(repaymentPeriod1CloseDate, () -> { runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get())); inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
verifyTransactions(loanId.get(), verifyTransactions(loanId.get(),
transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), // transaction(109.45, transaction(650.0, "Repayment", repaymentDate1, 0.0, 500.0, 39.45, 40.0, 30.0, 0.0, 40.55, false), //
// "Accrual",
// repaymentDate1,
// 0.0,
// 0,
// 39.45,
// 40.0,
// 30.0,
// 0,
// 0,
// false),
// //
transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // transaction(109.45, "Accrual Activity", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), //
transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), // transaction(109.45, "Accrual", repaymentDate1, 0.0, 0.0, 39.45, 40.0, 30.0, 0.0, 0.0, false), //
transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) // transaction(500.0, "Disbursement", disbursementDay, 500.0, 0, 0, 0, 0, 0, 0, false) //