FINERACT-2081: Enable inline COB execution for locked loans

This commit is contained in:
Jose Alberto Hernandez
2024-11-27 10:09:49 +01:00
committed by Adam Saghy
parent 2e89c40e6e
commit 240a6dffc8
9 changed files with 163 additions and 4 deletions
@@ -555,6 +555,44 @@ public class BatchApiStepDef extends AbstractStepDef {
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();
@@ -659,7 +659,30 @@ 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
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 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 |
| 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:
| 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
@@ -677,6 +700,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 WITHOUT error message:
| rescheduleFromDate | submittedOnDate | adjustedDueDate | approvedOnDate | enclosingTransaction |
| 16 January 2024 | 10 January 2024 | 31 January 2024 | 10 January 2024 | true |
@@ -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)
@@ -27,5 +27,7 @@ public interface LoanAccountLockService {
boolean isLoanHardLocked(Long loanId);
boolean isLockOverrulable(Long loanId);
void updateCobAndRemoveLocks();
}
@@ -49,6 +49,12 @@ public class LoanAccountLockServiceImpl implements LoanAccountLockService {
|| 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() {
@@ -186,6 +186,14 @@ public class LoanCOBFilterHelper implements InitializingBean {
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());
@@ -217,7 +225,7 @@ public class LoanCOBFilterHelper implements InitializingBean {
// 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);
@@ -240,7 +248,7 @@ public class LoanCOBFilterHelper implements InitializingBean {
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;
@@ -37,6 +37,7 @@ import java.util.ArrayList;
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;
@@ -80,6 +81,9 @@ import org.apache.fineract.integrationtests.common.accounting.PeriodicAccrualAcc
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;
@@ -5709,6 +5713,73 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
}
@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 ---------------------------------------");
@@ -600,6 +600,10 @@ public abstract class BaseLoanIntegrationTest {
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));
}
@@ -602,6 +602,11 @@ public class LoanTransactionHelper extends IntegrationTest {
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"));