From f7bc606a9d07d53353a349fbbc099ff6076c4f2a Mon Sep 17 00:00:00 2001 From: danhandev Date: Wed, 28 Oct 2026 15:01:26 +0900 Subject: [PATCH] FINERACT-2982: Add office-hierarchy check for deposit account activation Activating a savings, fixed deposit or recurring deposit account can post an initial deposit, but activation loaded the account without checking the user's office hierarchy, so a user from an unrelated office could activate it. Load the account through the in-user-scope assemblers added in FINERACT-2639, so such a user gets 403 and the account stays approved. GSIM activation and bulk import go through the same methods and are covered as well. Assisted-By: claude-opus-5-5 --- ...WritePlatformServiceJpaRepositoryImpl.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 2 +- ...ePlatformServiceJpaRepositoryImplTest.java | 14 +++ ...tOfficeHierarchyAccessIntegrationTest.java | 114 ++++++++++++++++++ ...tOfficeHierarchyAccessIntegrationTest.java | 52 +++++++- ...sOfficeHierarchyAccessIntegrationTest.java | 38 +++++- 6 files changed, 215 insertions(+), 9 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositOfficeHierarchyAccessIntegrationTest.java diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index a19735d7108..3cff7633da7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -143,7 +143,7 @@ public CommandProcessingResult activateFDAccount(final Long savingsId, final Jso final boolean postReversals = false; this.depositAccountTransactionDataValidator.validateActivation(command); final MathContext mc = MathContext.DECIMAL64; - final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFromInUserScope(savingsId, DepositAccountType.FIXED_DEPOSIT); checkClientOrGroupActive(account); @@ -250,7 +250,7 @@ public CommandProcessingResult activateRDAccount(final Long savingsId, final Jso final boolean postReversals = false; this.depositAccountTransactionDataValidator.validateActivation(command); - final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + final RecurringDepositAccount account = (RecurringDepositAccount) this.depositAccountAssembler.assembleFromInUserScope(savingsId, DepositAccountType.RECURRING_DEPOSIT); checkClientOrGroupActive(account); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java index d3fdc28ea4c..4e042cfb285 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java @@ -203,7 +203,7 @@ public CommandProcessingResult activate(final Long savingsId, final JsonCommand this.savingsAccountTransactionDataValidator.validateActivation(command); - final SavingsAccount account = this.savingAccountAssembler.assembleFrom(savingsId, false); + final SavingsAccount account = this.savingAccountAssembler.assembleFromInUserScope(savingsId, false); checkClientOrGroupActive(account); final Set existingTransactionIds = new HashSet<>(); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImplTest.java index e8c51afeb78..cef9dca963d 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImplTest.java @@ -54,6 +54,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.infrastructure.security.exception.NoAuthorizationException; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.holiday.domain.HolidayRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; @@ -389,6 +390,19 @@ void undoTransaction_shouldSkipUserScopeForAccountTransferReversal() { verify(savingAccountAssembler, never()).assembleFromInUserScope(anyLong(), anyBoolean()); } + @Test + void activate_shouldRejectUserOutsideAccountOfficeHierarchy() { + Long savingsId = 1L; + JsonCommand command = mock(JsonCommand.class); + + when(savingAccountAssembler.assembleFromInUserScope(savingsId, false)) + .thenThrow(new NoAuthorizationException("The user doesn't have enough permissions to access the resource.")); + + assertThatThrownBy(() -> service.activate(savingsId, command)).isInstanceOf(NoAuthorizationException.class); + verify(savingAccountAssembler, never()).assembleFrom(anyLong(), anyBoolean()); + verify(savingAccountRepositoryWrapper, never()).saveAndFlush(any()); + } + @Test void postInterest_shouldValidateRequestAndUpdateManualInterestPostingExternalId() { Long savingsId = 1L; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositOfficeHierarchyAccessIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositOfficeHierarchyAccessIntegrationTest.java new file mode 100644 index 00000000000..9272b1f020f --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositOfficeHierarchyAccessIntegrationTest.java @@ -0,0 +1,114 @@ +/** + * 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 static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.feign.util.FeignCalls; +import org.apache.fineract.client.models.PostClientsRequest; +import org.apache.fineract.client.models.PostFixedDepositAccountsAccountIdResponse; +import org.apache.fineract.client.models.PostUsersRequest; +import org.apache.fineract.integrationtests.client.feign.FeignDepositTestBase; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignOfficeHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignRoleHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignUserHelper; +import org.apache.fineract.integrationtests.client.feign.modules.DepositRequestBuilders; +import org.apache.fineract.integrationtests.client.feign.modules.DepositTestData; +import org.apache.fineract.integrationtests.client.feign.modules.DepositTestValidators; +import org.apache.fineract.integrationtests.client.feign.modules.SavingsTestData; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.junit.jupiter.api.Test; + +/** + * FINERACT-2982: activating a fixed deposit account, which posts the initial deposit, is only allowed for users whose + * office is the account's office or one of its parents. + */ +public class FixedDepositOfficeHierarchyAccessIntegrationTest extends FeignDepositTestBase { + + private static final String PASSWORD = "A1b2c3d4e5f$"; + private static final LocalDate OFFICE_OPENING_DATE = LocalDate.of(2010, 1, 1); + private static final String HIERARCHY_ERROR_MESSAGE = "The user doesn't have enough permissions to access the resource."; + private static final Map ACTIVATION_PERMISSIONS = Map.of("ACTIVATE_FIXEDDEPOSITACCOUNT", true, + "READ_FIXEDDEPOSITACCOUNT", true); + + private final FeignOfficeHelper officeHelper = new FeignOfficeHelper(FineractFeignClientHelper.getFineractFeignClient()); + + @Test + public void testFixedDepositActivationIsRestrictedToUserOfficeHierarchy() { + LocalDate today = Utils.getLocalDateOfTenant(); + String validFrom = Utils.dateFormatter.format(today.minusMonths(3)); + String validTo = Utils.dateFormatter.format(today.plusYears(10)); + String startDate = Utils.dateFormatter.format(today.minusMonths(1)); + Long accountOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + Long siblingOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + + Long clientId = createClientInOffice(accountOfficeId); + Long productId = fixedDepositProductHelper.createProduct(DepositRequestBuilders.withChart( + DepositRequestBuilders.fixedDepositProduct().accountingRule(SavingsTestData.AccountingRule.NONE), validFrom, validTo, + DepositTestData.periodRangeChartSlabs())).getResourceId(); + Long accountId = fixedDepositHelper.submitApplication(DepositRequestBuilders.fixedDepositAccount(clientId, productId, startDate, + DepositTestData.PreClosurePenalInterestOnType.WHOLE_TERM)).getSavingsId(); + fixedDepositHelper.approve(accountId, startDate); + + FineractFeignClient siblingOfficeUser = createUserInOffice(siblingOfficeId); + FineractFeignClient accountOfficeUser = createUserInOffice(accountOfficeId); + + CallFailedRuntimeException failure = FeignCalls.failVoid(() -> activate(siblingOfficeUser, accountId, startDate)); + assertEquals(403, failure.getStatus()); + assertTrue(failure.getResponseBody().contains(HIERARCHY_ERROR_MESSAGE), + "Expected office hierarchy error but was: " + failure.getResponseBody()); + DepositTestValidators.verifyFixedDepositIsApproved(fixedDepositHelper.getAccount(accountId).getStatus()); + + assertNotNull(ok(() -> activate(accountOfficeUser, accountId, startDate)).getResourceId(), + "User from the account's office must be able to activate the account"); + DepositTestValidators.verifyFixedDepositIsActive(fixedDepositHelper.getAccount(accountId).getStatus()); + assertEquals(0, DepositTestData.DEPOSIT_AMOUNT.compareTo(fixedDepositHelper.getSummary(accountId).getTotalDeposits()), + "Activation must post the deposit amount as the initial deposit"); + } + + private Long createClientInOffice(Long officeId) { + return clientHelper.createClient(new PostClientsRequest().officeId(officeId).legalFormId(1L) + .firstname(Utils.randomFirstNameGenerator()).lastname(Utils.randomLastNameGenerator()).active(true) + .activationDate(ClientHelper.DEFAULT_DATE).dateFormat(SavingsTestData.DATETIME_PATTERN).locale(SavingsTestData.LOCALE)) + .getClientId(); + } + + private FineractFeignClient createUserInOffice(Long officeId) { + String username = Utils.uniqueRandomStringGenerator("usr", 8); + Long roleId = FeignRoleHelper.createRole(); + FeignRoleHelper.addPermissionsToRole(roleId, ACTIVATION_PERMISSIONS); + FeignUserHelper.createUser(new PostUsersRequest().username(username).firstname("Test").lastname("User").email("test@localhost") + .officeId(officeId).roles(List.of(roleId)).password(PASSWORD).repeatPassword(PASSWORD).sendPasswordToEmail(false)); + return FineractFeignClientHelper.createNewFineractFeignClient(username, PASSWORD); + } + + private static PostFixedDepositAccountsAccountIdResponse activate(FineractFeignClient client, Long accountId, String date) { + return client.fixedDepositAccount().handleCommandsFixedDepositAccount(accountId, DepositRequestBuilders.activateFixedDeposit(date), + "activate"); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RecurringDepositOfficeHierarchyAccessIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RecurringDepositOfficeHierarchyAccessIntegrationTest.java index 3bd47b194c1..2fe22f84c00 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RecurringDepositOfficeHierarchyAccessIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RecurringDepositOfficeHierarchyAccessIntegrationTest.java @@ -30,6 +30,7 @@ import org.apache.fineract.client.feign.util.CallFailedRuntimeException; import org.apache.fineract.client.feign.util.FeignCalls; import org.apache.fineract.client.models.PostClientsRequest; +import org.apache.fineract.client.models.PostRecurringDepositAccountsAccountIdResponse; import org.apache.fineract.client.models.PostRecurringDepositAccountsRecurringDepositAccountIdTransactionsResponse; import org.apache.fineract.client.models.PostUsersRequest; import org.apache.fineract.integrationtests.client.feign.FeignDepositTestBase; @@ -38,6 +39,7 @@ import org.apache.fineract.integrationtests.client.feign.helpers.FeignUserHelper; import org.apache.fineract.integrationtests.client.feign.modules.DepositRequestBuilders; import org.apache.fineract.integrationtests.client.feign.modules.DepositTestData; +import org.apache.fineract.integrationtests.client.feign.modules.DepositTestValidators; import org.apache.fineract.integrationtests.client.feign.modules.SavingsTestData; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; @@ -46,13 +48,17 @@ /** * FINERACT-2639: monetary recurring deposit transactions are only allowed for users whose office is the account's - * office or one of its parents. + * office or one of its parents. FINERACT-2982: the same applies to account activation. */ public class RecurringDepositOfficeHierarchyAccessIntegrationTest extends FeignDepositTestBase { private static final String PASSWORD = "A1b2c3d4e5f$"; private static final LocalDate OFFICE_OPENING_DATE = LocalDate.of(2010, 1, 1); private static final String HIERARCHY_ERROR_MESSAGE = "The user doesn't have enough permissions to access the resource."; + private static final Map DEPOSIT_PERMISSIONS = Map.of("DEPOSIT_RECURRINGDEPOSITACCOUNT", true, + "READ_RECURRINGDEPOSITACCOUNT", true); + private static final Map ACTIVATION_PERMISSIONS = Map.of("ACTIVATE_RECURRINGDEPOSITACCOUNT", true, + "READ_RECURRINGDEPOSITACCOUNT", true); private final FeignOfficeHelper officeHelper = new FeignOfficeHelper(FineractFeignClientHelper.getFineractFeignClient()); @@ -75,8 +81,8 @@ public void testRecurringDepositIsRestrictedToUserOfficeHierarchy() { recurringDepositHelper.activate(accountId, startDate); BigDecimal depositAmount = recurringDepositHelper.getAccount(accountId).getMandatoryRecommendedDepositAmount(); - FineractFeignClient siblingOfficeUser = createUserInOffice(siblingOfficeId); - FineractFeignClient accountOfficeUser = createUserInOffice(accountOfficeId); + FineractFeignClient siblingOfficeUser = createUserInOffice(siblingOfficeId, DEPOSIT_PERMISSIONS); + FineractFeignClient accountOfficeUser = createUserInOffice(accountOfficeId, DEPOSIT_PERMISSIONS); CallFailedRuntimeException failure = FeignCalls.failVoid(() -> deposit(siblingOfficeUser, accountId, startDate, depositAmount)); assertEquals(403, failure.getStatus()); @@ -87,6 +93,37 @@ public void testRecurringDepositIsRestrictedToUserOfficeHierarchy() { "User from the account's office must be able to make a deposit"); } + @Test + public void testRecurringDepositActivationIsRestrictedToUserOfficeHierarchy() { + LocalDate today = Utils.getLocalDateOfTenant(); + String validFrom = Utils.dateFormatter.format(today.minusMonths(3)); + String validTo = Utils.dateFormatter.format(today.plusYears(10)); + String startDate = Utils.dateFormatter.format(today.minusMonths(1)); + Long accountOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + Long siblingOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + + Long clientId = createClientInOffice(accountOfficeId); + Long productId = recurringDepositProductHelper.createProduct(DepositRequestBuilders.withChart( + DepositRequestBuilders.recurringDepositProduct().accountingRule(SavingsTestData.AccountingRule.NONE), validFrom, validTo, + DepositTestData.recurringChartSlabsFor("period"))).getResourceId(); + Long accountId = recurringDepositHelper.submitApplication(DepositRequestBuilders.recurringDepositAccount(clientId, productId, + startDate, startDate, DepositTestData.PreClosurePenalInterestOnType.WHOLE_TERM)).getSavingsId(); + recurringDepositHelper.approve(accountId, startDate); + + FineractFeignClient siblingOfficeUser = createUserInOffice(siblingOfficeId, ACTIVATION_PERMISSIONS); + FineractFeignClient accountOfficeUser = createUserInOffice(accountOfficeId, ACTIVATION_PERMISSIONS); + + CallFailedRuntimeException failure = FeignCalls.failVoid(() -> activate(siblingOfficeUser, accountId, startDate)); + assertEquals(403, failure.getStatus()); + assertTrue(failure.getResponseBody().contains(HIERARCHY_ERROR_MESSAGE), + "Expected office hierarchy error but was: " + failure.getResponseBody()); + DepositTestValidators.verifyRecurringDepositIsApproved(recurringDepositHelper.getAccount(accountId).getStatus()); + + assertNotNull(ok(() -> activate(accountOfficeUser, accountId, startDate)).getResourceId(), + "User from the account's office must be able to activate the account"); + DepositTestValidators.verifyRecurringDepositIsActive(recurringDepositHelper.getAccount(accountId).getStatus()); + } + private Long createClientInOffice(Long officeId) { return clientHelper.createClient(new PostClientsRequest().officeId(officeId).legalFormId(1L) .firstname(Utils.randomFirstNameGenerator()).lastname(Utils.randomLastNameGenerator()).active(true) @@ -94,15 +131,20 @@ private Long createClientInOffice(Long officeId) { .getClientId(); } - private FineractFeignClient createUserInOffice(Long officeId) { + private FineractFeignClient createUserInOffice(Long officeId, Map permissions) { String username = Utils.uniqueRandomStringGenerator("usr", 8); Long roleId = FeignRoleHelper.createRole(); - FeignRoleHelper.addPermissionsToRole(roleId, Map.of("DEPOSIT_RECURRINGDEPOSITACCOUNT", true, "READ_RECURRINGDEPOSITACCOUNT", true)); + FeignRoleHelper.addPermissionsToRole(roleId, permissions); FeignUserHelper.createUser(new PostUsersRequest().username(username).firstname("Test").lastname("User").email("test@localhost") .officeId(officeId).roles(List.of(roleId)).password(PASSWORD).repeatPassword(PASSWORD).sendPasswordToEmail(false)); return FineractFeignClientHelper.createNewFineractFeignClient(username, PASSWORD); } + private static PostRecurringDepositAccountsAccountIdResponse activate(FineractFeignClient client, Long accountId, String date) { + return client.recurringDepositAccount().handleCommandsRecurringDepositAccount(accountId, + DepositRequestBuilders.activateRecurringDeposit(date), "activate"); + } + private static PostRecurringDepositAccountsRecurringDepositAccountIdTransactionsResponse deposit(FineractFeignClient client, Long accountId, String date, BigDecimal amount) { return client.recurringDepositAccountTransactions().transactionRecurringDepositAccountTransaction(accountId, diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsOfficeHierarchyAccessIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsOfficeHierarchyAccessIntegrationTest.java index 65e300fd974..c6977d2cc19 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsOfficeHierarchyAccessIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsOfficeHierarchyAccessIntegrationTest.java @@ -25,6 +25,7 @@ import com.fasterxml.jackson.databind.JsonNode; import feign.Headers; import feign.RequestLine; +import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -33,6 +34,7 @@ import org.apache.fineract.client.feign.util.FeignCalls; import org.apache.fineract.client.models.PostClientsRequest; import org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse; +import org.apache.fineract.client.models.PostSavingsAccountsAccountIdResponse; import org.apache.fineract.client.models.PostUsersRequest; import org.apache.fineract.integrationtests.client.feign.FeignSavingsTestBase; import org.apache.fineract.integrationtests.client.feign.helpers.FeignOfficeHelper; @@ -48,15 +50,17 @@ /** * FINERACT-2639: monetary savings transactions are only allowed for users whose office is the account's office or one - * of its parents. + * of its parents. FINERACT-2982: the same applies to account activation, which can post the initial deposit. */ public class SavingsOfficeHierarchyAccessIntegrationTest extends FeignSavingsTestBase { private static final String PASSWORD = "A1b2c3d4e5f$"; private static final String DEPOSIT_AMOUNT = "100"; + private static final String OPENING_BALANCE = "100"; private static final LocalDate OFFICE_OPENING_DATE = LocalDate.of(2010, 1, 1); private static final String HIERARCHY_ERROR_MESSAGE = "The user doesn't have enough permissions to access the resource."; private static final Map DEPOSIT_PERMISSIONS = Map.of("DEPOSIT_SAVINGSACCOUNT", true, "READ_SAVINGSACCOUNT", true); + private static final Map ACTIVATION_PERMISSIONS = Map.of("ACTIVATE_SAVINGSACCOUNT", true, "READ_SAVINGSACCOUNT", true); private static final Map COLLECTION_SHEET_PERMISSIONS = Map.of("SAVE_COLLECTIONSHEET", true, "READ_SAVINGSACCOUNT", true); @@ -106,6 +110,34 @@ public void testCollectionSheetSavingsDepositIsRestrictedToUserOfficeHierarchy() "User from the account's office must be able to save a collection sheet deposit"); } + @Test + public void testSavingsActivationIsRestrictedToUserOfficeHierarchy() { + String today = Utils.dateFormatter.format(Utils.getLocalDateOfTenant()); + Long accountOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + Long siblingOfficeId = officeHelper.createOffice(OFFICE_OPENING_DATE).getOfficeId(); + + Long clientId = createClientInOffice(accountOfficeId); + Long productId = createSavingsProduct( + SavingsRequestBuilders.defaultSavingsProduct().minRequiredOpeningBalance(new BigDecimal(OPENING_BALANCE))).getResourceId(); + Long savingsId = submitSavingsApplication(clientId, productId, today).getSavingsId(); + approveSavings(savingsId, today); + + FineractFeignClient siblingOfficeUser = createUserInOffice(siblingOfficeId, ACTIVATION_PERMISSIONS); + FineractFeignClient accountOfficeUser = createUserInOffice(accountOfficeId, ACTIVATION_PERMISSIONS); + + CallFailedRuntimeException failure = FeignCalls.failVoid(() -> activate(siblingOfficeUser, savingsId, today)); + assertEquals(403, failure.getStatus()); + assertTrue(failure.getResponseBody().contains(HIERARCHY_ERROR_MESSAGE), + "Expected office hierarchy error but was: " + failure.getResponseBody()); + verifySavingsStatus(savingsId, status -> status.getApproved()); + + assertNotNull(ok(() -> activate(accountOfficeUser, savingsId, today)).getResourceId(), + "User from the account's office must be able to activate the account"); + verifySavingsStatus(savingsId, status -> status.getActive()); + assertEquals(0, new BigDecimal(OPENING_BALANCE).compareTo(getSavingsDetails(savingsId).getSummary().getAccountBalance()), + "Activation must post the minimum opening balance as the initial deposit"); + } + interface IndividualCollectionSheetApi { @RequestLine("POST /v1/collectionsheet?command=saveCollectionSheet") @@ -137,6 +169,10 @@ private FineractFeignClient createUserInOffice(Long officeId, Map