Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FINERACT-2081: Enable inline COB execution for locked loans #4171

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
import org.apache.fineract.client.services.UsersApi;
import org.apache.fineract.client.util.JSON;
import org.apache.fineract.test.data.ChargeProductType;
import org.apache.fineract.test.data.LoanRescheduleErrorMessage;
import org.apache.fineract.test.data.LoanStatus;
import org.apache.fineract.test.data.TransactionType;
import org.apache.fineract.test.factory.ClientRequestFactory;
Expand Down Expand Up @@ -506,16 +505,13 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUser(String fromDa
});
}

@When("Batch API call with created user and the following data results a {int} error and a {string} error message:")
public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobError(int errorCodeExpected, String errorMessageType,
@When("Batch API call with created user and the following data results a {int} statuscode and a {string} error message:")
public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobError(int httpCodeExpected, String errorMessageType,
DataTable table) throws IOException {
String idempotencyKey = UUID.randomUUID().toString();
Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
Long loanId = loanResponse.body().getLoanId();

LoanRescheduleErrorMessage loanRescheduleErrorMessage = LoanRescheduleErrorMessage.valueOf(errorMessageType);
String errorMessageExpected = loanRescheduleErrorMessage.getValue(loanId);

List<List<String>> data = table.asLists();
List<String> transferData = data.get(1);
String fromDateStr = transferData.get(0);
Expand All @@ -542,17 +538,10 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobErr
Boolean isEnclosingTransaction = Boolean.valueOf(enclosingTransaction);
Response<List<BatchResponse>> batchResponseList = batchApiApi.handleBatchRequests(requestList, isEnclosingTransaction, headerMap)
.execute();
String errorToString = batchResponseList.errorBody().string();
ErrorResponse errorResponse = GSON.fromJson(errorToString, ErrorResponse.class);
String errorMessageActual = errorResponse.getDeveloperMessage();
Integer errorCodeActual = errorResponse.getHttpStatusCode();

assertThat(errorCodeActual).as(ErrorMessageHelper.wrongErrorCode(errorCodeActual, errorCodeExpected)).isEqualTo(errorCodeExpected);
assertThat(errorMessageActual).as(ErrorMessageHelper.wrongErrorMessage(errorMessageActual, errorMessageExpected))
.isEqualTo(errorMessageExpected);

log.debug("ERROR CODE: {}", errorCodeActual);
log.debug("ERROR MESSAGE: {}", errorMessageActual);
BatchResponse lastBatchResponse = batchResponseList.body().get(batchResponseList.body().size() - 1);
assertThat(httpCodeExpected).isEqualTo(lastBatchResponse.getStatusCode());
// No error
assertThat(batchResponseList.errorBody()).isEqualTo(null);
}

@When("Batch API call with steps: queryDatatable, updateDatatable runs, with empty queryDatatable response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,7 @@ Feature: LoanReschedule
When Batch API call with created user and with steps: rescheduleLoan from "16 January 2024" to "31 January 2024" submitted on date: "10 January 2024", approveReschedule on date: "10 January 2024" runs with enclosingTransaction: "true"
Then Admin checks that last closed business date of loan is "09 January 2024"

@temp
@TestRailId:C3048 @AdvancedPaymentAllocation
Scenario: Verify that in case of Loan is hard locked for COB execution, BatchAPI request of Loan reschedule creation and approval will result a 409 error and a LOAN_LOCKED_BY_COB error message
When Admin sets the business date to "01 January 2024"
Expand All @@ -677,6 +678,6 @@ Feature: LoanReschedule
| CREATE_RESCHEDULELOAN |
| READ_RESCHEDULELOAN |
| REJECT_RESCHEDULELOAN |
When Batch API call with created user and the following data results a 409 error and a "LOAN_LOCKED_BY_COB" error message:
When Batch API call with created user and the following data results a 200 statuscode and a "LOAN_LOCKED_BY_COB" error message:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | approvedOnDate | enclosingTransaction |
| 16 January 2024 | 10 January 2024 | 31 January 2024 | 10 January 2024 | true |
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public interface LoanAccountLockRepository

boolean existsByLoanIdAndLockOwner(Long loanId, LockOwner lockOwner);

boolean existsByLoanIdAndLockOwnerAndErrorIsNotNull(Long loanId, LockOwner lockOwner);

@Query("""
delete from LoanAccountLock lck where lck.lockPlacedOnCobBusinessDate is not null and lck.error is not null and
lck.lockOwner in (org.apache.fineract.cob.domain.LockOwner.LOAN_COB_CHUNK_PROCESSING,org.apache.fineract.cob.domain.LockOwner.LOAN_INLINE_COB_PROCESSING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,7 @@ public interface LoanAccountLockService {

boolean isLoanHardLocked(Long loanId);

boolean isLockOverrulable(Long loanId);

void updateCobAndRemoveLocks();
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ public boolean isLoanHardLocked(Long loanId) {
|| loanAccountLockRepository.existsByLoanIdAndLockOwner(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING);
}

@Override
public boolean isLockOverrulable(Long loanId) {
return loanAccountLockRepository.existsByLoanIdAndLockOwnerAndErrorIsNotNull(loanId, LockOwner.LOAN_COB_CHUNK_PROCESSING) //
|| loanAccountLockRepository.existsByLoanIdAndLockOwnerAndErrorIsNotNull(loanId, LockOwner.LOAN_INLINE_COB_PROCESSING);
}

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateCobAndRemoveLocks() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,14 @@ private boolean isLoanHardLocked(List<Long> loanIds) {
return loanIds.stream().anyMatch(loanAccountLockService::isLoanHardLocked);
}

private boolean isLockOverrulable(Long... loanIds) {
return isLockOverrulable(Arrays.asList(loanIds));
}

private boolean isLockOverrulable(List<Long> loanIds) {
return loanIds.stream().anyMatch(loanAccountLockService::isLockOverrulable);
}

public boolean isLoanBehind(List<Long> loanIds) {
List<LoanIdAndLastClosedBusinessDate> loanIdAndLastClosedBusinessDates = new ArrayList<>();
List<List<Long>> partitions = Lists.partition(loanIds, fineractProperties.getQuery().getInClauseParameterSizeLimit());
Expand Down Expand Up @@ -217,7 +225,7 @@ private List<Long> getLoanIdsFromBatchApi(BodyCachingHttpServletRequestWrapper r
// check the body for Loan ID
Long loanId = getTopLevelLoanIdFromBatchRequest(batchRequest);
if (loanId != null) {
if (isLoanHardLocked(loanId)) {
if (isLoanHardLocked(loanId) && !isLockOverrulable(loanId)) {
throw new LoanIdsHardLockedException(loanId);
} else {
loanIds.add(loanId);
Expand All @@ -240,7 +248,7 @@ private Long getTopLevelLoanIdFromBatchRequest(BatchRequest batchRequest) throws

private List<Long> getLoanIdsFromApi(String pathInfo) {
List<Long> loanIds = getLoanIdList(pathInfo);
if (isLoanHardLocked(loanIds)) {
if (isLoanHardLocked(loanIds) && !isLockOverrulable(loanIds)) {
throw new LoanIdsHardLockedException(loanIds.get(0));
} else {
return loanIds;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
Expand Down Expand Up @@ -80,6 +81,9 @@
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.integrationtests.common.organisation.StaffHelper;
import org.apache.fineract.integrationtests.useradministration.roles.RolesHelper;
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
Expand Down Expand Up @@ -5709,6 +5713,73 @@ public void uc151() {

}

@Test
public void uc152() {
AtomicLong createdLoanId = new AtomicLong();
runAt("01 January 2024", () -> {
Long clientId = client.getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRateFrequencyType(YEARS).numberOfRepayments(4)//
.maxInterestRatePerPeriod((double) 0)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//
.allowPartialPeriodInterestCalcualtion(false)//
.multiDisburseLoan(false)//
.disallowExpectedDisbursements(null)//
.allowApprovedDisbursedAmountsOverApplied(null)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.installmentAmountInMultiplesOf(null)//
.loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
;//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "01 January 2024", 400.0,
6);

applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS).interestRatePerPeriod(BigDecimal.ZERO)
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);

PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
createdLoanId.set(loanResponse.getLoanId());

loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(400.0)).dateFormat(DATETIME_PATTERN)
.approvedOnDate("01 January 2024").locale("en"));

loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2024").dateFormat(DATETIME_PATTERN)
.transactionAmount(BigDecimal.valueOf(400.0)).locale("en"));
});

runAt("02 January 2024", () -> {
executeInlineCOB(createdLoanId.get());

final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertEquals(LocalDate.of(2024, 1, 1), loanDetails.getLastClosedBusinessDate());
final String errorMessage = Utils.uniqueRandomStringGenerator("error.", 40);
placeHardLockOnLoan(createdLoanId.get(), errorMessage);
});

runAt("03 January 2024", () -> {
Integer roleId = RolesHelper.createRole(requestSpec, responseSpec);
Map<String, Boolean> permissionMap = Map.of("REPAYMENT_LOAN", true);
RolesHelper.addPermissionsToRole(requestSpec, responseSpec, roleId, permissionMap);
final Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec);

final String operatorUser = Utils.uniqueRandomStringGenerator("user", 8);
UserHelper.createUser(this.requestSpec, this.responseSpec, roleId, staffId, operatorUser, UserHelper.SIMPLE_USER_PASSWORD,
"resourceId");

loanTransactionHelper.makeLoanRepayment(
createdLoanId.get(), new PostLoansLoanIdTransactionsRequest().transactionDate("03 January 2024")
.dateFormat("dd MMMM yyyy").locale("en").transactionAmount(200.0),
operatorUser, UserHelper.SIMPLE_USER_PASSWORD);

final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get());
assertEquals(LocalDate.of(2024, 1, 2), loanDetails.getLastClosedBusinessDate());
});
}

private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,10 @@ protected void placeHardLockOnLoan(Long loanId) {
loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING");
}

protected void placeHardLockOnLoan(Long loanId, String error) {
loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING", error);
}

protected void executeInlineCOB(Long loanId) {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,11 @@ public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId,
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
}

public PostLoansLoanIdTransactionsResponse makeLoanRepayment(final Long loanId, final PostLoansLoanIdTransactionsRequest request,
final String user, final String pass) {
return ok(newFineract(user, pass).loanTransactions.executeLoanTransaction(loanId, request, "repayment"));
}

public PostLoansLoanIdTransactionsResponse makeInterestPaymentWaiver(final Long loanId,
final PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction(loanId, request, "interestPaymentWaiver"));
Expand Down
Loading