diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index fd1c61cf8..9f3d7586d 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3878,8 +3878,8 @@ public class CommandWrapperBuilder { } public CommandWrapperBuilder updateLoanApprovedAmount(final Long loanId) { - this.actionName = "UPDATE"; - this.entityName = "LOAN_APPROVED_AMOUNT"; + this.actionName = "UPDATE_APPROVED_AMOUNT"; + this.entityName = "LOAN"; this.entityId = loanId; this.loanId = loanId; this.href = "/loans/" + loanId; @@ -3887,8 +3887,8 @@ public class CommandWrapperBuilder { } public CommandWrapperBuilder manualInterestRefund(final Long loanId, final Long transactionId) { - this.actionName = "MANUAL_INTEREST_REFUND"; - this.entityName = "LOAN_TRANSACTION"; + this.actionName = "MANUAL_INTEREST_REFUND_TRANSACTION"; + this.entityName = "LOAN"; this.loanId = loanId; this.entityId = transactionId; this.href = "/loans/" + loanId + "/transactions/" + transactionId + "?command=interest-refund"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java index 1be514b86..a88a6b496 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/LoanApprovedAmountModificationCommandHandler.java @@ -29,7 +29,7 @@ import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -@CommandType(entity = "LOAN_APPROVED_AMOUNT", action = "UPDATE") +@CommandType(entity = "LOAN", action = "UPDATE_APPROVED_AMOUNT") public class LoanApprovedAmountModificationCommandHandler implements NewCommandSourceHandler { private final LoanApprovedAmountWritePlatformService loanApprovedAmountWritePlatformService; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ManualInterestRefundCommandHandler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ManualInterestRefundCommandHandler.java index 09c9abc09..b5257eecd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ManualInterestRefundCommandHandler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/handler/ManualInterestRefundCommandHandler.java @@ -29,7 +29,7 @@ import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor -@CommandType(entity = "LOAN_TRANSACTION", action = "MANUAL_INTEREST_REFUND") +@CommandType(entity = "LOAN", action = "MANUAL_INTEREST_REFUND_TRANSACTION") public class ManualInterestRefundCommandHandler implements NewCommandSourceHandler { private final LoanWritePlatformService loanWritePlatformService; diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 1e12348ff..94ffeb11b 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -212,4 +212,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0194_fix_missing_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0194_fix_missing_permission.xml new file mode 100644 index 000000000..7319ff99f --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0194_fix_missing_permission.xml @@ -0,0 +1,568 @@ + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CLOSE_CLIENT_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CLOSE_GROUP_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CLOSE_CENTER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'REMOVESAVINGSOFFICER_SAVINGSACCOUNT_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATESAVINGSOFFICER_SAVINGSACCOUNT_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CREATE_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'ALLOCATECASHIER_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATECASHIERALLOCATION_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DELETECASHIERALLOCATION_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'ALLOCATECASHTOCASHIER_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'SETTLECASHFROMCASHIER_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DEFINEOPENINGBALANCE_JOURNALENTRY_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DELETE_TELLER_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'READ_RATE_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CREATE_RATE_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_RATE_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATEOPENINGBALANCE_JOURNALENTRY' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATEOPENINGBALANCE_JOURNALENTRY_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CREATE_STANDINGINSTRUCTION' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_STANDINGINSTRUCTION' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DELETE_STANDINGINSTRUCTION' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_COLLECTIONSHEET' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'POSTINTERESTASONDATE_SAVINGSACCOUNT' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'POSTINTERESTASONDATE_SAVINGSACCOUNT_CHECKER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'ALLOCATECASHIER_TELLER' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CAPITALIZEDINCOME_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CAPITALIZEDINCOMEADJUSTMENT_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'BUYDOWNFEEADJUSTMENT_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'BUYDOWNFEE_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CONTRACT_TERMINATION_UNDO_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_APPROVED_AMOUNT_LOAN' + + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'MANUAL_INTEREST_REFUND_TRANSACTION_LOAN' + + + + + + + + + + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'CREATE_ADDRESS_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'CREATE_ADDRESS_CHECKER'; + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'UPDATE_ADDRESS_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'UPDATE_ADDRESS_CHECKER'; + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'DELETE_ADDRESS_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'DELETE_ADDRESS_CHECKER'; + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'UNDOREJECT_CLIENT_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'UNDOREJECT_CLIENT_CHECKER'; + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'UNDOWITHDRAWAL_CLIENT_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'UNDOWITHDRAWAL_CLIENT_CHECKER'; + + + + + + SELECT COUNT(*) FROM m_permission + WHERE code = 'UPDATEDEPOSITAMOUNT_RECURRINGDEPOSITACCOUNT_CHECKER' AND can_maker_checker = 'true' + + + + UPDATE m_permission + SET can_maker_checker = 'false' + WHERE code = 'UPDATEDEPOSITAMOUNT_RECURRINGDEPOSITACCOUNT_CHECKER'; + + + + + diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index b4d01afd3..47c64147b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -82,15 +82,19 @@ import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.PostRolesRequest; +import org.apache.fineract.client.models.PostUsersRequest; import org.apache.fineract.client.models.PutGlobalConfigurationsRequest; import org.apache.fineract.client.models.PutLoansApprovedAmountRequest; import org.apache.fineract.client.models.PutLoansApprovedAmountResponse; import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountRequest; import org.apache.fineract.client.models.PutLoansAvailableDisbursementAmountResponse; import org.apache.fineract.client.models.PutLoansLoanIdResponse; +import org.apache.fineract.client.models.PutRolesRoleIdPermissionsRequest; import org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.client.util.Calls; +import org.apache.fineract.client.util.FineractClient; import org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants; import org.apache.fineract.infrastructure.event.external.data.ExternalEventResponse; import org.apache.fineract.integrationtests.client.IntegrationTest; @@ -126,12 +130,15 @@ import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.extension.ExtendWith; +import retrofit2.Call; +import retrofit2.Response; @Slf4j @ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class }) public abstract class BaseLoanIntegrationTest extends IntegrationTest { protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; + protected static final String LOCALE = "en"; static { Utils.initializeRESTAssured(); @@ -305,6 +312,86 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest { return repaymentId; } + /** + * Executes a Loan transaction request by a user created with no permissions then verifies it fails with + * authentication error. Then alters user permissions to get the given permission then executes the query again. It + * verifies that it returns with no error. + * + * @param loanId + * loan id + * @param postLoansLoanIdTransactionsRequest + * transaction request + * @param command + * the command for loan transaction + * @param permission + * the given permission related to the loan transaction + * @return Result body + */ + public PostLoansLoanIdTransactionsResponse makeLoanTransactionWithPermissionVerification(final Long loanId, + PostLoansLoanIdTransactionsRequest postLoansLoanIdTransactionsRequest, final String command, final String permission) { + return performPermissionTestForRequest(permission, fineractClient -> fineractClient.loanTransactions.executeLoanTransaction(loanId, + postLoansLoanIdTransactionsRequest, command)); + } + + /** + * Executes a Loan transaction adjustment request by a user created with no permissions then verifies it fails with + * authentication error. Then alters user permissions to get the given permission then executes the query again. It + * verifies that it returns with no error. + * + * @param loanId + * loan ID + * @param transactionIdToAdjust + * transaction ID to adjust + * @param postLoansLoanIdTransactionsRequest + * transaction request + * @param command + * the command for loan transaction + * @param permission + * the given permission related to the loan transaction + * @return Result body + */ + public PostLoansLoanIdTransactionsResponse adjustLoanTransactionWithPermissionVerification(final Long loanId, + final Long transactionIdToAdjust, PostLoansLoanIdTransactionsTransactionIdRequest postLoansLoanIdTransactionsRequest, + final String command, final String permission) { + return performPermissionTestForRequest(permission, fineractClient -> fineractClient.loanTransactions.adjustLoanTransaction(loanId, + transactionIdToAdjust, postLoansLoanIdTransactionsRequest, command)); + } + + public T performPermissionTestForRequest(final String permission, Function> callback) { + // create role + String roleName = Utils.uniqueRandomStringGenerator("TEST_ROLE_", 10); + Long roleId = Calls + .ok(fineractClient().roles.createRole(new PostRolesRequest().name(roleName).description("Test role Description"))) + .getResourceId(); + + Calls.ok(fineractClient().roles.updateRolePermissions(roleId, + new PutRolesRoleIdPermissionsRequest().putPermissionsItem(permission, false))); + // create user with role + String firstname = "Test"; + String lastname = Utils.uniqueRandomStringGenerator("User", 6); + String userName = Utils.uniqueRandomStringGenerator("testUserName", 4); + String password = "AKleRbDhK421$"; + String email = firstname + "." + lastname + "@whatever.mifos.org"; + Calls.ok(fineractClient().users + .create15(new PostUsersRequest().addRolesItem(roleId).email(email).firstname(firstname).lastname(lastname) + .repeatPassword(password).sendPasswordToEmail(false).officeId(1L).username(userName).password(password))); + + // login user + FineractClient fineractClientOfUser = newFineractClient(userName, password); + + // try to make transaction - should fail + Response responseFail = Calls.executeU(callback.apply(fineractClientOfUser)); + Assertions.assertEquals(403, responseFail.code()); + + // edit role to have permission for transaction + Calls.ok(fineractClient().roles.updateRolePermissions(roleId, + new PutRolesRoleIdPermissionsRequest().putPermissionsItem(permission, true))); + // try to make transaction - should pass + Response responseOk = Calls.executeU(callback.apply(fineractClientOfUser)); + Assertions.assertEquals(200, responseOk.code()); + return responseOk.body(); + } + private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) { // creates the user UserHelper.getSimpleUserWithoutBypassPermission(requestSpec, responseSpec); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UserLoanPermissionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UserLoanPermissionTest.java new file mode 100644 index 000000000..fb133fc36 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UserLoanPermissionTest.java @@ -0,0 +1,154 @@ +/** + * 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.integrationtests; + +import java.math.BigDecimal; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.client.models.PostLoansResponse; +import org.apache.fineract.client.models.PutLoansApprovedAmountRequest; +import org.apache.fineract.client.util.Calls; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import retrofit2.Response; + +public class UserLoanPermissionTest extends BaseLoanIntegrationTest { + + Long clientId; + Long loanProductId; + private Long loanId; + + @BeforeEach + public void setup() { + if (clientId == null) { + clientId = ClientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + } + if (loanProductId == null) { + loanProductId = loanProductHelper.createLoanProduct(create4IProgressiveWithCapitalizedIncome() + .addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) + .overAppliedCalculationType(null).overAppliedNumber(null).allowApprovedDisbursedAmountsOverApplied(false) + .enableBuyDownFee(true).buyDownFeeCalculationType(PostLoanProductsRequest.BuyDownFeeCalculationTypeEnum.FLAT) + .buyDownFeeStrategy(PostLoanProductsRequest.BuyDownFeeStrategyEnum.EQUAL_AMORTIZATION) + .buyDownFeeIncomeType(PostLoanProductsRequest.BuyDownFeeIncomeTypeEnum.FEE) + .receivableInterestAccountId(interestReceivableAccount.getAccountID().longValue()) + .receivableFeeAccountId(feeReceivableAccount.getAccountID().longValue()) + .receivablePenaltyAccountId(penaltyReceivableAccount.getAccountID().longValue()) + .buyDownExpenseAccountId(buyDownExpenseAccount.getAccountID().longValue()) + .incomeFromBuyDownAccountId(feeIncomeAccount.getAccountID().longValue()) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())).getResourceId(); + } + runAt("1 January 2025", () -> { + + PostLoansResponse postLoansResponse = loanTransactionHelper + .applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId, "1 January 2025", 10000.0, 12.0, 4, null)); + + loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(2000.0, "1 January 2025")); + + loanId = postLoansResponse.getResourceId(); + disburseLoan(loanId, BigDecimal.valueOf(1000.0), "1 January 2025"); + }); + } + + @Test + public void testCapitalizedIncomeAndCapitalizedIncomeAdjustmentPermissions() { + runAt("1 January 2025", () -> { + Long capitalizedIncomeId = makeLoanTransactionWithPermissionVerification(loanId, new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(50.0).transactionDate("01 January 2025"), + "capitalizedIncome", "CAPITALIZEDINCOME_LOAN").getResourceId(); + + adjustLoanTransactionWithPermissionVerification( + loanId, capitalizedIncomeId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN) + .locale("en").transactionAmount(50.0).transactionDate("1 January 2025"), + "capitalizedIncomeAdjustment", "CAPITALIZEDINCOMEADJUSTMENT_LOAN"); + + }); + + } + + @Test + public void testBuyDownFeeAndBuyDownFeeAdjustmentPermissions() { + runAt("1 January 2025", () -> { + final Long buyDownFeeTransactionId = makeLoanTransactionWithPermissionVerification(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2025").locale("en") + .transactionAmount(100.0d), + "buyDownFee", "BUYDOWNFEE_LOAN").getResourceId(); + + adjustLoanTransactionWithPermissionVerification( + loanId, buyDownFeeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("01 January 2025").locale("en").transactionAmount(100.0d), + "buyDownFeeAdjustment", "BUYDOWNFEEADJUSTMENT_LOAN"); + }); + } + + @Test + public void testManualInterestRefundPermission() { + runAt("1 February 2025", () -> { + final Long merchantIssuedRefundId = loanTransactionHelper + .makeMerchantIssuedRefund(loanId, + new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).locale("en") + .transactionDate("01 February 2025").transactionAmount(100.0D).interestRefundCalculation(false)) + .getResourceId(); + + performPermissionTestForRequest("MANUAL_INTEREST_REFUND_TRANSACTION_LOAN", + fineractClient -> fineractClient.loanTransactions.adjustLoanTransaction(loanId, merchantIssuedRefundId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).locale("en") + .transactionAmount(1.20D), + "interest-refund")); + }); + } + + @Test + public void testUpdateApprovedAmountPermission() { + runAt("1 January 2025", () -> { + // disbursement should be rejected upon validation error + Response response = Calls.executeU( + fineractClient().loans.stateTransitions(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2025") + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(2000.0)).locale("en"), "disburse")); + + Assertions.assertEquals(403, response.code()); + + // update approved amount + performPermissionTestForRequest("UPDATE_APPROVED_AMOUNT_LOAN", + fineractClient -> fineractClient.loans.modifyLoanApprovedAmount(loanId, + new PutLoansApprovedAmountRequest().amount(BigDecimal.valueOf(4000.0d)).locale("en"))); + + // disbursement should be performed without error + Calls.ok(fineractClient().loans.stateTransitions(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2025") + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(2000.0)).locale("en"), "disburse")); + }); + } + + @Test + public void testContractTerminationAndUndoContractTerminationPermission() { + + runAt("2 January 2025", () -> { + performPermissionTestForRequest("CONTRACT_TERMINATION_LOAN", fineractClient -> fineractClient.loans.stateTransitions(loanId, + new PostLoansLoanIdRequest().note(""), "contractTermination")); + + performPermissionTestForRequest("CONTRACT_TERMINATION_UNDO_LOAN", + fineractClient -> fineractClient.loans.stateTransitions(loanId, + new PostLoansLoanIdRequest().note("Contract Termination Undo Test Note"), "undoContractTermination")); + }); + } +}