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

Merged
Merged
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 @@ -555,6 +555,44 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobErr
log.debug("ERROR MESSAGE: {}", errorMessageActual);
}

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

List<List<String>> data = table.asLists();
List<String> transferData = data.get(1);
String fromDateStr = transferData.get(0);
String submittedOnDate = transferData.get(1);
String toDateStr = transferData.get(2);
String approvedOnDate = transferData.get(3);
String enclosingTransaction = transferData.get(4);

Map<String, String> headerMap = new HashMap<>();

Response<PostUsersResponse> createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.body().getResourceId();
Response<GetUsersUserIdResponse> user = usersApi.retrieveOne31(createdUserId).execute();
ErrorHelper.checkSuccessfulApiCall(user);
String authorizationString = user.body().getUsername() + ":" + PWD_USER_WITH_ROLE;
Base64 base64 = new Base64();
headerMap.put("Authorization",
"Basic " + new String(base64.encode(authorizationString.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8));
List<BatchRequest> requestList = new ArrayList<>();
requestList.add(createLoanReschedule(1L, loanId, fromDateStr, toDateStr, submittedOnDate, idempotencyKey, null));
requestList.add(approveLoanReschedule(2L, idempotencyKey, approvedOnDate, 1L));

Boolean isEnclosingTransaction = Boolean.valueOf(enclosingTransaction);
Response<List<BatchResponse>> batchResponseList = batchApiApi.handleBatchRequests(requestList, isEnclosingTransaction, headerMap)
.execute();
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")
public void runBatchApiQueryDatatableUpdateDatatable() throws IOException {
String idempotencyKey = UUID.randomUUID().toString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ Feature: LoanReschedule
Then Admin checks that last closed business date of loan is "09 January 2024"

@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
Scenario: Verify that in case of Loan is hard locked for COB execution without error message, 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"
When Admin creates a client with random data
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
Expand All @@ -671,7 +671,7 @@ Feature: LoanReschedule
When Admin sets the business date to "02 January 2024"
When Admin runs inline COB job for Loan
When Admin sets the business date to "10 January 2024"
When Admin places a lock on loan account with an error message
When Admin places a lock on loan account WITHOUT an error message
When Admin creates new user with "NO_BYPASS_AUTOTEST" username, "NO_BYPASS_AUTOTEST_ROLE" role name and given permissions:
| APPROVE_RESCHEDULELOAN |
| CREATE_RESCHEDULELOAN |
Expand All @@ -680,3 +680,26 @@ Feature: LoanReschedule
When Batch API call with created user and the following data results a 409 error 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 |

@TestRailId:C3049 @AdvancedPaymentAllocation
Scenario: Verify that in case of Loan is hard locked for COB execution with error message, BatchAPI request of Loan reschedule creation and approval will result a 200 statuscode without error message
When Admin sets the business date to "01 January 2024"
When Admin creates a client with random data
When Admin set "LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION" loan product "DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation rule
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 01 January 2024 | 1000 | 0 | FLAT | SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 45 | DAYS | 15 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
And Admin successfully approves the loan on "01 January 2024" with "1000" amount and expected disbursement date on "01 January 2024"
When Admin successfully disburse the loan on "01 January 2024" with "1000" EUR transaction amount
When Admin sets the business date to "02 January 2024"
When Admin runs inline COB job for Loan
When Admin sets the business date to "10 January 2024"
When Admin places a lock on loan account with an error message
When Admin creates new user with "NO_BYPASS_AUTOTEST" username, "NO_BYPASS_AUTOTEST_ROLE" role name and given permissions:
| APPROVE_RESCHEDULELOAN |
| CREATE_RESCHEDULELOAN |
| READ_RESCHEDULELOAN |
| REJECT_RESCHEDULELOAN |
When Batch API call with created user and the following data results a 200 statuscode WITHOUT 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